本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
纸上得来终觉浅,绝知此事要躬行。
阅读本文:
如需简单使用👉:SpringBoot集成SpringSecurity做安全框架、附源码
你能收获:🛴
- 你能大致明白
SpringSecurity
鉴权流程。 - 能够 Debug 一步一步能够画出
SpringSecurity
鉴权流程图。 - 对于
SpringSecurity
框架会有更深一步的理解,能够在使用时做到更高程度的定制化。 - 以及对
SpringSecurity
更深一步的思考
一、前言:
xdm,我还是没有学会写小故事😭,我只可以在这里请你们喝可乐🥤,请 xdm 赏个一键三连😁。
xdm,不知道你们在使用SpringSecurity安全框架的时候,有没有想过 debug 一步一步看它是如何实现判断是否可以访问的?
如下:
1 | java复制代码@PreAuthorize("hasRole('ROLE_ADMIN')") |
为什么我们写上这个注解可以了呢?如何进行判断的呢?
前面写过一次👨💻 SpringSecurity 登录流程分析,写那篇文章是为了写👩💻 SpringSecurity 实现多种登录方式做铺垫。
那么这次写这个文章的原因呢?
在掘金看到了掘友的 和耳朵 写的 SpringSecurity 动态鉴权流程分析,才发觉用注解其实也不是个非常好的事情,直接固定在项目,无法做到动态的更改,是个要不得的事情(捂脸),之前只考虑到这么写蛮好的,看完文章才恍然大悟。这两天也准备实现一下Security的动态鉴权的小demo。
xdm,一定要记得,
纸上得来终觉浅,绝知此事要躬行
,尤其是一路 debug 的文章,亲身踩坑。
对于一门技术,会使用是说明我们对它已经有了一个简单了解,把脉络、细节都掌握清楚,我们才能更好的使用。
接下来就让👨🏫来带大家一起看看吧。
二、流程图:
下图是在百度找的一张关于 Security 原理图
我接下来画的流程图是基于用户已经登录的状态下的画的。
整个认证的过程其实一直在围绕图中过滤链的绿色部分,而我们今天要说的鉴权主要是围绕其橙色部分,也就是图上标的:FilterSecurityInterceptor
。
这也就是我流程图的开始,如下图:
上图如有不妥之处,请大家批正,在此郑重感谢。
关于上图的粗略解释,后文再一一道来:
1、登录后,用户访问一个需要权限的接口,经过一连串过滤器,到达 FilterSecurityInterceptor
, FilterSecurityInterceptor 的invoke()方法执行具体拦截行为,具体是 beforeInvocation、finallyInvocation、afterInvocation 这三个方法,这三个方法是定义在父类 AbstractSecurityInterceptor
中。
2、调用 AbstractSecurityInterceptor 的 beforeInvocation 方法。AbstractSecurityInterceptor
将确保安全拦截器的正确启动配置。它还将实现对安全对象调用的正确处理,即:
- 获取访问当前资源所需要的权限
SecurityMetadataSource..getAttributes(object)
;返回个 Collection< ConfigAttribute > attributes - 从SecurityContextHolder获取Authentication对象。 `Authentication authenticated = authenticateIfRequired();
- 尝试授权
attemptAuthorization(object, attributes, authenticated);
调用 AccessDecisionManager 接口 decide 方法,执行鉴权,鉴权不成功,会直接抛异常。 - 返回一个InterceptorStatusToken。
3、经过千辛万苦后,到达MethodSecurityInterceptor,由它再次重新调用起 AbstractSecurityInterceptor.beforeInvocation(mi) 方法,来进行权限的验证
- 鉴权的时候,投票者会换成
PreInvocationAuthorizationAdviceVoter
进入正题前先放张图片缓一缓:
当乌云和白云相遇
👨💻
三、前半部分
前半部分作用是在检测用户的状态,并非就是执行鉴权,不过两次都十分相近。关于方法上注解的检测是在后半部分。
1)入口:FilterSecurityInterceptor
第一步:FilterSecurityInterceptor void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
1 | java复制代码//过滤器链实际调用的方法。 简单地委托给invoke(FilterInvocation)方法。 |
接着看 void invoke(FilterInvocation filterInvocation)
1 | java复制代码public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException { |
2)进入:AbstractSecurityInterceptor
授权检查 beforeInvocation() 方法
第二步:super.beforeInvocation(filterInvocation); 一些打印信息被精简了,太长不适合阅读
1 | java复制代码protected InterceptorStatusToken beforeInvocation(Object object) { |
关于 Collection< ConfigAttribute > attributes = this.obtainSecurityMetadataSource().getAttributes(object);
这段代码。
第一次访问这里的时候,FilterSecurityInterceptor
是从 SecurityMetadataSource
的子类 DefaultFilterInvocationSecurityMetadataSource
获取到当前的是这样的数据。它和我们第二次来执行这里有很大的区别。这里的表达式是 authenticated
,翻译过来就是认证过的。
在后文会进行比较的。
我们接着往下看:Authentication authenticateIfRequired() 获取身份信息
1 | java复制代码//如果Authentication.isAuthenticated()返回 false 或属性alwaysReauthenticate已设置为 true, |
3)尝试授权: attemptAuthorization()
第三步:尝试授权: attemptAuthorization()
1 | java复制代码private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, |
AccessDecisionManager 决策器说明:
this.accessDecisionManager 其实是个接口。我们一起看看它的源码
1 | java复制代码public interface AccessDecisionManager { |
我们先看看这个接口结构,之后再看它的实现类内部鉴权机制是如何执行的,需要获取那些信息,又是如何判断它是否可以通过的。
我们可以看到这个 AccessDecisionManager
接口,接口下有一个抽象类,然后再有了三个实现类。
他们分别代表不同的机制。
- AffirmativeBased:如果任何AccessDecisionVoter返回肯定响应,则授予访问权限。即有一票同意,就可以通过,默认是它。
- ConsensusBased:少数服从于多数。多数票同意通过,即可以通过。如民主选举制一样。
- UnanimousBased:要求所有选民弃权或授予访问权限。简称一票反对。只要有一票反对就不能通过。
一起看看默认用的 AffirmativeBased:
1 | java复制代码public class AffirmativeBased extends AbstractAccessDecisionManager { |
到这里又会牵扯到 AccessDecisionVoter
出来,也就是能够投票的选民们。
AccessDecisionVoter 投票观众接口
我们先一起来看它的源码,再看看它的实现类:
1 | java复制代码//表示一个类负责对授权决定进行投票。 |
我们看看它的结构:
RoleVoter
主要用来判断当前请求是否具备该接口所需要的角色RoleHierarchyVoter
是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承WebExpressionVoter
这是一个基于表达式权限控制的投票器Jsr250Voter
处理 Jsr-250 权限注解的投票器,如@PermitAll
,@DenyAll
等。AuthenticatedVoter
用于判断 ConfigAttribute 上是否拥有 IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY 三种角色。AbstractAclVoter
提供编写域对象 ACL 选项的帮助方法,没有绑定到任何特定的 ACL 系统。PreInvocationAuthorizationAdviceVoter
使用 @PreFilter 和 @PreAuthorize 注解处理的权限,通过PreInvocationAuthorizationAdvice
来授权。
AffirmativeBased
默认传入的构造器只有一个 WebExpressionVoter
,这个构造器会根据你在配置文件中的配置进行逻辑处理得出投票结果。
所以我们在执行第一次循环时,也是在这里处理的。
1 | java复制代码public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> { |
在这里的数据也是如此,和我们上文就互相对应上了。
4)返回过程
4.1、先返回至AffirmativeBased.decide()方法处,投票通过,继续 retrun
1 | java复制代码for (AccessDecisionVoter voter : getDecisionVoters()) { |
4.2、返回至 AbstractSecurityInterceptor 方法调用处,这里是无返回值,直接回到 beforeInvocation
方法中。
1 | java复制代码private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, |
4.3、再返回至beforeInvocation
方法中,
1 | java复制代码protected InterceptorStatusToken beforeInvocation(Object object) { |
4.4、回到了我们梦开始的地方了:FilterSecurityInterceptor.invoke() 方法
1 | java复制代码 public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException { |
四、后半部分
对方法注解的鉴权,是真的一步一步看它如何执行的,一直扒,真的是历经千辛万苦。
默认大家都能看的懂这个图了,我们直接转到 MethodSecurityInterceptor 里来看看它做了什么吧
4.1、入口:MethodSecurityInterceptor
1 | java复制代码//提供对基于 AOP 联盟的方法调用的安全拦截。 |
MethodInvocation
:doc注释是”方法调用的描述,在方法调用时提供给拦截器。方法调用是一个连接点,可以被方法拦截器拦截”.
4.2、进入 AbstractSecurityInterceptor
授权检查 beforeInvocation() 方法
另外在这里debug获取到的值也是不一样的,这点上文我刚刚也说过了。
获取资源访问策略:FilterSecurityInterceptor
会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection< ConfigAttribute >。 SecurityMetadataSource 其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
1 | java复制代码protected void configure(HttpSecurity http) throws Exception { |
中间的过程同上半部分差不多,就不多说了。我们直接看 AffirmativeBased 情况如何。
4.3、转战:AffirmativeBasedl;
1 | java复制代码attemptAuthorization(object, attributes, authenticated); |
接着往下,到此处就同之前稍有不同了,我们之前用到的是 WebExpressionVoter,在这里我们使用的是: PreInvocationAuthorizationAdviceVoter
我们接着进入:PreInvocationAuthorizationAdviceVoter,它的类上的doc注释如下:
Voter 使用从 @PreFilter 和 @PreAuthorize 注释生成的 PreInvocationAuthorizationAdvice 实现来执行操作。
在实践中,如果使用这些注解,它们通常会包含所有必要的访问控制逻辑,因此基于投票者的系统并不是真正必要的,包含相同逻辑的单个AccessDecisionManager就足够了。 然而,这个类很容易与 Spring Security 使用的传统的基于投票者的AccessDecisionManager实现相适应。
我们可以很容易的看出,这个就是处理方法上注解的那个类。接着看下它的源码。
1 | java复制代码public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVoter<MethodInvocation> { |
简单看一下PreInvocationAuthorizationAdvice接口的before方法的默认实现:
before方法的说明是:应该执行的“before”建议以执行任何必要的过滤并决定方法调用是否被授权。
我们先说说它的参数:(Authentication authentication,MethodInvocation mi,PreInvocationAttribute attr)
,第一个就是当前登录的用户,二就是要执行的方法,三就是方法上的注解信息。
我们可以很简单的看出这段代码的含义,就是在比较已经登录的用户,是否拥有这个方法上所需要的权限。
另外简单说明一下:
- createEvaluationContext 的dco注释:提供评估上下文,在其中评估调用类型的安全表达式(即 SpEL 表达式)。我个人对这块没有特别深入过,没法说清楚,大家可以查一查。
- 另外我们看一下debug的详细信息,大家应该就差不多能懂啦。
接下来就是一步一步返回啦
最后就是:
这里的 result 就是方法执行的返回结果。紧接着就是一步一步返回过滤器链啦。
对于这里 proceed
方法就不再深入了。这个点拉出来说,怕是直接可以写上一篇完整的文章啦。
内部很多动态代理啊、反射啊这些相关的,一层套一层的,不是咱研究重点。溜啦溜啦。
五、小结
这张图是在百度上搜到的,大致流程其实就是如此。
其实内部还有很多很多值得推敲的东西,不是在这一篇简单的文章中能够写出来的。
六、自我感受
还记得我第一次说要看源码是在准备研究 Mybatis 的时候,那时候上头看了大概几天吧,看着看着就看不下去了,找不到一个合适的方法,什么都想看,没有一个非常具体的目标,导致连续受挫,结果就是不了了之了。
第二次真正意义看源码就是看 Security 。原因是当时在写项目的时,我的前端小伙伴说,现在大部分网站都有多种登录方式,你能实现不?
男人肯定是不能说不行,然后我就一口答应下来了。结果就是疯狂百度、google,到处看博客。互联网这么庞大,当然也有找到非常多的例子,也有源码解析。但是找到的文章,要么只贴出了核心代码,要么就是不合适(庞大,难以抽取),总之一句话没法运行。就很烦操。
不过文章中都提到了要理解 Security 的登录过程,然后进行仿写,俗称抄作业。最后,真就是一步一步 debug 去看 Security 的登录过程,写出了 第一篇 Security登录认证流程分析,紧接着又去用 SpringSecurity实现多种登录方式,如邮件验证码、电话号码登录。这次即是机缘巧合,也是心有所念,耗费不少时间写出了这篇文章。感觉还是非常不错的。
希望大家能够喜欢,如果 xdm 对此也感兴趣,希望大家在有时间的情况,debug 几次,记忆会深刻很多。并竟 纸上得来终觉浅,绝知此事要躬行。
相关文章:
- SpringBoot集成SpringSecurity做安全框架
- Security的登录流程详解
- Security实现多种登录方式、邮件验证码、手机验证码登录。
- SpringSecurity权限命名ROLE_问题
今天的文章就到这里了。
你好,我是博主
宁在春
:主页如若在文章中遇到疑惑,请留言或私信,或者加主页联系方式,都会尽快回复。
如若发现文章中存在问题,望你能够指正,不胜感谢。
如果觉得对你有所帮助的话,请点个赞再走吧!
本文转载自: 掘金