阅读本文:
如需简单使用?:SpringBoot集成SpringSecurity做安全框架、附源码
你能收获:?
你能大致明白 SpringSecurity
鉴权流程。
能够 Debug 一步一步能够画出 SpringSecurity
鉴权流程图。
对于 SpringSecurity
框架会有更深一步的理解,能够在使用时做到更高程度的定制化。
以及对 SpringSecurity
更深一步的思考
一、前言:
xdm,不知道你们在使用SpringSecurity安全框架的时候,有没有想过 debug 一步一步看它是如何实现判断是否可以访问的?
如下:
@PreAuthorize("hasRole('ROLE_ADMIN')")@RequestMapping("/role/admin1")Stringadmin(){return"role: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)
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}
接着看 void invoke(FilterInvocation filterInvocation)
publicvoidinvoke(FilterInvocationfilterInvocation)throwsIOException,ServletException{if(isApplied(filterInvocation)&&this.observeOncePerRequest){//过滤器已应用于此请求,用户希望我们观察每个请求处理一次,因此不要重新进行安全检查filterInvocation.getChain().doFilter(filterInvocation.getRequest(),filterInvocation.getResponse());return;}//第一次调用这个请求,所以执行安全检查if(filterInvocation.getRequest()!=null&&this.observeOncePerRequest){filterInvocation.getRequest().setAttribute(FILTER_APPLIED,Boolean.TRUE);}//调用beforeInvocation(filterInvocation)方法跟着这个方法往下看InterceptorStatusTokentoken=super.beforeInvocation(filterInvocation);try{//每个过滤器都有这么一步filterInvocation.getChain().doFilter(filterInvocation.getRequest(),filterInvocation.getResponse());}finally{//在安全对象调用完成后清理AbstractSecurityInterceptor的工作。//无论安全对象调用是否成功返回,都应该在安全对象调用之后和afterInvocation之前调用此方法(即它应该在finally块中完成)。super.finallyInvocation(token);}//当调用afterInvocation(InterceptorStatusToken,Object)时,AbstractSecurityInterceptor不会采取进一步的操作。super.afterInvocation(token,null);}
2)进入:AbstractSecurityInterceptor
授权检查 beforeInvocation() 方法
第二步:super.beforeInvocation(filterInvocation); 一些打印信息被精简了,太长不适合阅读
protectedInterceptorStatusTokenbeforeInvocation(Objectobject){//检查操作Assert.notNull(object,"Objectwasnull");if(!getSecureObjectClass().isAssignableFrom(object.getClass())){//....}//这里获取的信息看下图示1://object就是调用处传过来的参数FilterInvocationfilterInvocation,它本身其实就是HttpServletRequest和HttpServletResponse的增强//object:filterinvocation[GET/role/admin1]"//然后我们获取到的就是受保护调用的列表Collection<ConfigAttribute>attributes=this.obtainSecurityMetadataSource().getAttributes(object);if(CollectionUtils.isEmpty(attributes)){//...returnnull;//nofurtherworkpost-invocation}//在SecurityContext中未找到身份验证对象,会发事件抛异常if(SecurityContextHolder.getContext().getAuthentication()==null){credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","AnAuthenticationobjectwasnotfoundintheSecurityContext"),object,attributes);}//在这里拿到了Authentication对象登录的信息,后文会简单说是如何拿到的Authenticationauthenticated=authenticateIfRequired();if(this.logger.isTraceEnabled()){this.logger.trace(LogMessage.format("Authorizing%swithattributes%s",object,attributes));}//Attemptauthorization:尝试授权这步本文重点,用我的话来说,这就是鉴权的入口重点关注,下文继续attemptAuthorization(object,attributes,authenticated);//...//AttempttorunasadifferentuserAuthenticationrunAs=this.runAsManager.buildRunAs(authenticated,object,attributes);if(runAs!=null){//...}//无后续动作returnnewInterceptorStatusToken(SecurityContextHolder.getContext(),false,attributes,object);}
关于 Collection< ConfigAttribute > attributes = this.obtainSecurityMetadataSource().getAttributes(object);
这段代码。
第一次访问这里的时候,FilterSecurityInterceptor
是从 SecurityMetadataSource
的子类 DefaultFilterInvocationSecurityMetadataSource
获取到当前的是这样的数据。它和我们第二次来执行这里有很大的区别。这里的表达式是 authenticated
,翻译过来就是认证过的。
在后文会进行比较的。
我们接着往下看:Authentication authenticateIfRequired() 获取身份信息
//如果Authentication.isAuthenticated()返回false或属性alwaysReauthenticate已设置为true,//则检查当前的身份验证令牌并将其传递给AuthenticationManager进行身份验证privateAuthenticationauthenticateIfRequired(){Authenticationauthentication=SecurityContextHolder.getContext().getAuthentication();if(authentication.isAuthenticated()&&!this.alwaysReauthenticate){returnauthentication;}authentication=this.authenticationManager.authenticate(authentication);SecurityContextHolder.getContext().setAuthentication(authentication);returnauthentication;}
3)尝试授权: attemptAuthorization()
第三步:尝试授权: attemptAuthorization()
privatevoidattemptAuthorization(Objectobject,Collection<ConfigAttribute>attributes,Authenticationauthenticated){try{//接着套娃我们去看AccessDecisionManager下的decide()方法this.accessDecisionManager.decide(authenticated,object,attributes);}catch(AccessDeniedExceptionex){if(this.logger.isTraceEnabled()){this.logger.trace(LogMessage.format("Failedtoauthorize%swithattributes%susing%s",object,attributes,this.accessDecisionManager));}elseif(this.logger.isDebugEnabled()){this.logger.debug(LogMessage.format("Failedtoauthorize%swithattributes%s",object,attributes));}publishEvent(newAuthorizationFailureEvent(object,attributes,authenticated,ex));throwex;}}
AccessDecisionManager 决策器说明:
this.accessDecisionManager 其实是个接口。我们一起看看它的源码
publicinterfaceAccessDecisionManager{/**为传递的参数解析访问控制决策。参数:身份验证-调用方法的调用者(非空)object–被调用的安全对象configAttributes–与被调用的安全对象关联的配置属性*/voiddecide(Authenticationauthentication,Objectobject,Collection<ConfigAttribute>configAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;//下面这两个方法主要起辅助作用的。大都执行检查操作booleansupports(ConfigAttributeattribute);booleansupports(Class<?>clazz);}
我们先看看这个接口结构,之后再看它的实现类内部鉴权机制是如何执行的,需要获取那些信息,又是如何判断它是否可以通过的。
我们可以看到这个 AccessDecisionManager
接口,接口下有一个抽象类,然后再有了三个实现类。
他们分别代表不同的机制。
AffirmativeBased:如果任何AccessDecisionVoter返回肯定响应,则授予访问权限。即有一票同意,就可以通过,默认是它。
ConsensusBased:少数服从于多数。多数票同意通过,即可以通过。如民主选举制一样。
UnanimousBased:要求所有选民弃权或授予访问权限。简称一票反对。只要有一票反对就不能通过。
一起看看默认用的 AffirmativeBased:
publicclassAffirmativeBasedextendsAbstractAccessDecisionManager{publicAffirmativeBased(List<AccessDecisionVoter<?>>decisionVoters){super(decisionVoters);}/**这个具体的实现只是轮询所有配置的AccessDecisionVoter并在任何AccessDecisionVoter投赞成票时授予访问权限。仅当存在拒绝投票且没有赞成票时才拒绝访问。如果每个AccessDecisionVoter放弃投票,则决策将基于isAllowIfAllAbstainDecisions()属性(默认为false)。*/@Override@SuppressWarnings({"rawtypes","unchecked"})publicvoiddecide(Authenticationauthentication,Objectobject,Collection<ConfigAttribute>configAttributes)throwsAccessDeniedException{intdeny=0;for(AccessDecisionVotervoter:getDecisionVoters()){intresult=voter.vote(authentication,object,configAttributes);switch(result){caseAccessDecisionVoter.ACCESS_GRANTED:return;caseAccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if(deny>0){thrownewAccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied","Accessisdenied"));}//Togetthisfar,everyAccessDecisionVoterabstainedcheckAllowIfAllAbstainDecisions();}}
到这里又会牵扯到 AccessDecisionVoter
出来,也就是能够投票的选民们。
AccessDecisionVoter 投票观众接口
我们先一起来看它的源码,再看看它的实现类:
//表示一个类负责对授权决定进行投票。//投票的协调(即轮询AccessDecisionVoter,统计他们的响应,并做出最终授权决定)由AccessDecisionManager执行。publicinterfaceAccessDecisionVoter<S>{intACCESS_GRANTED=1;intACCESS_ABSTAIN=0;intACCESS_DENIED=-1;//这两个用来执行check操作,判断参数是否合法等等booleansupports(ConfigAttributeattribute);booleansupports(Class<?>clazz);/**指示是否授予访问权限。决定必须是肯定的(ACCESS_GRANTED)、否定的(ACCESS_DENIED)或者AccessDecisionVoter可以弃权(ACCESS_ABSTAIN)投票。在任何情况下,实现类都不应返回任何其他值。如果需要对结果进行加权,则应改为在自定义AccessDecisionManager处理。除非AccessDecisionVoter由于传递的方法调用或配置属性参数而专门用于对访问控制决策进行投票,否则它必须返回ACCESS_ABSTAIN。这可以防止协调AccessDecisionManager计算来自那些AccessDecisionVoter的选票,而这些AccessDecisionVoter对访问控制决策没有合法利益。虽然安全对象(例如MethodInvocation)作为参数传递以最大限度地提高访问控制决策的灵活性,但实现类不应修改它或导致所表示的调用发生(例如,通过调用MethodInvocation.proceed()).*/intvote(Authenticationauthentication,Sobject,Collection<ConfigAttribute>attributes);}
我们看看它的结构:
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
,这个构造器会根据你在配置文件中的配置进行逻辑处理得出投票结果。
所以我们在执行第一次循环时,也是在这里处理的。
publicclassWebExpressionVoterimplementsAccessDecisionVoter<FilterInvocation>{privateSecurityExpressionHandler<FilterInvocation>expressionHandler=newDefaultWebSecurityExpressionHandler();@Overridepublicintvote(Authenticationauthentication,FilterInvocationfilterInvocation,Collection<ConfigAttribute>attributes){//...执行的一些检查//WebExpressionConfigAttributewebExpressionConfigAttribute=findConfigAttribute(attributes);if(webExpressionConfigAttribute==null){returnACCESS_ABSTAIN;}//允许对EvaluationContext进行后处理。实现可能会返回一个新的EvaluationContext实例或修改传入的EvaluationContext。EvaluationContextctx=webExpressionConfigAttribute.postProcess(//调用内部模板方法来创建StandardEvaluationContext和SecurityExpressionRoot对象。this.expressionHandler.createEvaluationContext(authentication,filterInvocation),filterInvocation);//针对指定的根对象评估默认上下文中的表达式。如果评估结果与预期结果类型不匹配(并且无法转换为),则将返回异常。booleangranted=ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(),ctx);//投赞同票,返回if(granted){returnACCESS_GRANTED;}returnACCESS_DENIED;}//循环判断privateWebExpressionConfigAttributefindConfigAttribute(Collection<ConfigAttribute>attributes){for(ConfigAttributeattribute:attributes){if(attributeinstanceofWebExpressionConfigAttribute){return(WebExpressionConfigAttribute)attribute;}}returnnull;}//...}
在这里的数据也是如此,和我们上文就互相对应上了。
4)返回过程
4.1、先返回至AffirmativeBased.decide()方法处,投票通过,继续 retrun
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}0
4.2、返回至 AbstractSecurityInterceptor 方法调用处,这里是无返回值,直接回到 beforeInvocation
方法中。
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}1
4.3、再返回至beforeInvocation
方法中,
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}2
4.4、回到了我们梦开始的地方了:FilterSecurityInterceptor.invoke() 方法
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}3
四、后半部分
对方法注解的鉴权,是真的一步一步看它如何执行的,一直扒,真的是历经千辛万苦。
默认大家都能看的懂这个图了,我们直接转到 MethodSecurityInterceptor 里来看看它做了什么吧
4.1、入口:MethodSecurityInterceptor
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}4
MethodInvocation
:doc注释是"方法调用的描述,在方法调用时提供给拦截器。方法调用是一个连接点,可以被方法拦截器拦截".
4.2、进入 AbstractSecurityInterceptor
授权检查 beforeInvocation() 方法
另外在这里debug获取到的值也是不一样的,这点上文我刚刚也说过了。
获取资源访问策略:FilterSecurityInterceptor
会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection< ConfigAttribute >。 SecurityMetadataSource 其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}5
中间的过程同上半部分差不多,就不多说了。我们直接看 AffirmativeBased 情况如何。
4.3、转战:AffirmativeBasedl;
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}6
接着往下,到此处就同之前稍有不同了,我们之前用到的是 WebExpressionVoter,在这里我们使用的是: PreInvocationAuthorizationAdviceVoter
我们接着进入:PreInvocationAuthorizationAdviceVoter,它的类上的doc注释如下:
Voter 使用从 @PreFilter 和 @PreAuthorize 注释生成的 PreInvocationAuthorizationAdvice 实现来执行操作。 在实践中,如果使用这些注解,它们通常会包含所有必要的访问控制逻辑,因此基于投票者的系统并不是真正必要的,包含相同逻辑的单个AccessDecisionManager就足够了。 然而,这个类很容易与 Spring Security 使用的传统的基于投票者的AccessDecisionManager实现相适应。
我们可以很容易的看出,这个就是处理方法上注解的那个类。接着看下它的源码。
//过滤器链实际调用的方法。简单地委托给invoke(FilterInvocation)方法。@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{invoke(newFilterInvocation(request,response,chain));}7
简单看一下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_问题
今天的文章就到这里了。
你好,我是博主
宁在春。
如若在文章中遇到疑惑,请留言或私信,或者加主页联系方式,都会尽快回复。
如若发现文章中存在问题,望你能够指正,不胜感谢。
如果觉得对你有所帮助的话,请点个赞再走吧!