一、背景知识

Spring实战篇系列----Security4.1.3认证过程源码分析和Spring实战篇系列----Security4.1.3实现根据请求跳转不同登录页以及登录后根据权限跳转到不同页配置中均有提到,每一次请求都会走Security Filter,鉴权的过滤器为FilterSecurityInterceptor,其中会判断是否要对请求进行鉴权,以及需要鉴权的会基于投票的AccessDecisionManager实现鉴权操作。

1)判断是否鉴权是根据,配置文件中是否有如下配置:

<intercept-url pattern="/order/**" access="hasRole('ROLE_USER')"/>
<intercept-url pattern="/manager" access="hasRole('ROLE_MANAGER')"/>

关键代码:AbstractSecurityInterceptor.java

protected InterceptorStatusToken beforeInvocation(Object object) {Assert.notNull(object, "Object was null");final boolean debug = logger.isDebugEnabled();if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {throw new IllegalArgumentException("Security invocation attempted for object "+ object.getClass().getName()+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "+ getSecureObjectClass());}Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);//这里获取上面属性access的值,根据是否有值判断是否需要鉴权if (attributes == null || attributes.isEmpty()) {if (rejectPublicInvocations) {throw new IllegalArgumentException("Secure object invocation "+ object+ " was denied as public invocations are not allowed via this interceptor. "+ "This indicates a configuration error because the "+ "rejectPublicInvocations property is set to 'true'");}if (debug) {logger.debug("Public object - authentication not attempted");}publishEvent(new PublicInvocationEvent(object));return null; // no further work post-invocation}if (debug) {logger.debug("Secure object: " + object + "; Attributes: " + attributes);}if (SecurityContextHolder.getContext().getAuthentication() == null) {credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","An Authentication object was not found in the SecurityContext"),object, attributes);}Authentication authenticated = authenticateIfRequired();// Attempt authorizationtry {this.accessDecisionManager.decide(authenticated, object, attributes);   //需要鉴权的请求有accessDecisionManager进行鉴权}catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));throw accessDeniedException;}if (debug) {logger.debug("Authorization successful");}if (publishAuthorizationSuccess) {publishEvent(new AuthorizedEvent(object, attributes, authenticated));}// Attempt to run as a different userAuthentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);if (runAs == null) {if (debug) {logger.debug("RunAsManager did not change Authentication object");}// no further work post-invocationreturn new InterceptorStatusToken(SecurityContextHolder.getContext(), false,attributes, object);}else {if (debug) {logger.debug("Switching to RunAs Authentication: " + runAs);}SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);// need to revert to token.Authenticated post-invocationreturn new InterceptorStatusToken(origCtx, true, attributes, object);}}

2)需要鉴权的有accessDecisionManager进行鉴权

二、鉴权的过程

1)基本知识:

Spring Security是通过拦截器来控制受保护对象的访问的,如方法调用和Web请求。在正式访问受保护对象之前,Spring Security将使用AccessDecisionManager来鉴定当前用户是否有访问对应受保护对象的权限。

AccessDecisionManager是由AbstractSecurityInterceptor调用的,它负责鉴定用户是否有访问对应资源(方法或URL)的权限。AccessDecisionManager是一个接口,其中只定义了三个方法,其定义如下。

public interface AccessDecisionManager {/*** 通过传递的参数来决定用户是否有访问对应受保护对象的权限** @param authentication 当前正在请求受包含对象的Authentication* @param object 受保护对象,其可以是一个MethodInvocation、JoinPoint或FilterInvocation。* @param configAttributes 与正在请求的受保护对象相关联的配置属性**/void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)throws AccessDeniedException, InsufficientAuthenticationException;/*** 表示当前AccessDecisionManager是否支持对应的ConfigAttribute*/boolean supports(ConfigAttribute attribute);/*** 表示当前AccessDecisionManager是否支持对应的受保护对象类型*/boolean supports(Class<?> clazz);}

decide()方法用于决定authentication是否符合受保护对象要求的configAttributes。supports(ConfigAttribute attribute)方法是用来判断AccessDecisionManager是否能够处理对应的ConfigAttribute的。supports(Class<?> clazz)方法用于判断配置的AccessDecisionManager是否支持对应的受保护对象类型。

2)投票系统

Spring Security已经内置了几个基于投票的AccessDecisionManager,当然如果需要你也可以实现自己的AccessDecisionManager。以下是Spring Security官方文档提供的一个图,其展示了与基于投票的AccessDecisionManager实现相关的类。还有比较重要的WebExpressionVoter

使用这种方式,一系列的AccessDecisionVoter将会被AccessDecisionManager用来对Authentication是否有权访问受保护对象进行投票,然后再根据投票结果来决定是否要抛出AccessDeniedException。AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。

public interface AccessDecisionVoter<S> {intACCESS_GRANTED = 1;intACCESS_ABSTAIN = 0;intACCESS_DENIED = -1;boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);}

vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意,ACCESS_DENIED表示拒绝,ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。

Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。

AffirmativeBased的逻辑是这样的:

(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

(2)如果全部弃权也表示通过;

(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

ConsensusBased的逻辑是这样的:

(1)如果赞成票多于反对票则表示通过。

(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。

(3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。

(4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。

UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。UnanimousBased的逻辑具体来说是这样的:

(1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException。

(2)如果没有反对票,但是有赞成票,则表示通过。

(3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。

投票者:

RoleVoter是Spring Security内置的一个AccessDecisionVoter,其会将ConfigAttribute简单的看作是一个角色名称,在投票的时如果拥有该角色即投赞成票。如果ConfigAttribute是以“ROLE_”开头的,则将使用RoleVoter进行投票。当用户拥有的权限中有一个或多个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute时其将投赞成票;如果用户拥有的权限中没有一个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute,则RoleVoter将投反对票;如果受保护对象配置的ConfigAttribute中没有以“ROLE_”开头的,则RoleVoter将弃权。

AuthenticatedVoter也是Spring Security内置的一个AccessDecisionVoter实现。其主要用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户。完全认证的用户是指由系统提供的登录入口进行成功登录认证的用户。

AuthenticatedVoter可以处理的ConfigAttribute有IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED和IS_AUTHENTICATED_ANONYMOUSLY。如果ConfigAttribute不在这三者范围之内,则AuthenticatedVoter将弃权。否则将视ConfigAttribute而定,如果ConfigAttribute为IS_AUTHENTICATED_ANONYMOUSLY,则不管用户是匿名的还是已经认证的都将投赞成票;如果是IS_AUTHENTICATED_REMEMBERED则仅当用户是由Remember-Me自动登录,或者是通过登录入口进行登录认证时才会投赞成票,否则将投反对票;而当ConfigAttribute为IS_AUTHENTICATED_FULLY时仅当用户是通过登录入口进行登录的才会投赞成票,否则将投反对票。

AuthenticatedVoter是通过AuthenticationTrustResolver的isAnonymous()方法和isRememberMe()方法来判断SecurityContextHolder持有的Authentication是否为AnonymousAuthenticationToken或RememberMeAuthenticationToken的,即是否为IS_AUTHENTICATED_ANONYMOUSLY和IS_AUTHENTICATED_REMEMBERED。

自定义Voter及重要的WebExpressionVoter

当然,用户也可以通过实现AccessDecisionVoter来实现自己的投票逻辑。

以上参考:http://elim.iteye.com/blog/2247057

三:源码分析

那么怎么选择accessDecisionManager及Voter,看下最初配置FilterSecurityInterceptor的源码

private void createFilterSecurityInterceptor(BeanReference authManager) {boolean useExpressions = FilterInvocationSecurityMetadataSourceParser.isUseExpressions(httpElt);RootBeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser.createSecurityMetadataSource(interceptUrls, addAllAuth, httpElt, pc);RootBeanDefinition accessDecisionMgr;ManagedList<BeanDefinition> voters = new ManagedList<BeanDefinition>(2);if (useExpressions) {BeanDefinitionBuilder expressionVoter = BeanDefinitionBuilder.rootBeanDefinition(WebExpressionVoter.class);// Read the expression handler from the FISMSRuntimeBeanReference expressionHandler = (RuntimeBeanReference) securityMds.getConstructorArgumentValues().getArgumentValue(1, RuntimeBeanReference.class).getValue();expressionVoter.addPropertyValue("expressionHandler", expressionHandler);voters.add(expressionVoter.getBeanDefinition());}else {voters.add(new RootBeanDefinition(RoleVoter.class));voters.add(new RootBeanDefinition(AuthenticatedVoter.class));}accessDecisionMgr = new RootBeanDefinition(AffirmativeBased.class);accessDecisionMgr.getConstructorArgumentValues().addGenericArgumentValue(voters);accessDecisionMgr.setSource(pc.extractSource(httpElt));// Set up the access manager reference for httpString accessManagerId = httpElt.getAttribute(ATT_ACCESS_MGR);if (!StringUtils.hasText(accessManagerId)) {accessManagerId = pc.getReaderContext().generateBeanName(accessDecisionMgr);pc.registerBeanComponent(new BeanComponentDefinition(accessDecisionMgr,accessManagerId));}BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterSecurityInterceptor.class);builder.addPropertyReference("accessDecisionManager", accessManagerId);builder.addPropertyValue("authenticationManager", authManager);if ("false".equals(httpElt.getAttribute(ATT_ONCE_PER_REQUEST))) {builder.addPropertyValue("observeOncePerRequest", Boolean.FALSE);}builder.addPropertyValue("securityMetadataSource", securityMds);BeanDefinition fsiBean = builder.getBeanDefinition();String fsiId = pc.getReaderContext().generateBeanName(fsiBean);pc.registerBeanComponent(new BeanComponentDefinition(fsiBean, fsiId));// Create and register a DefaultWebInvocationPrivilegeEvaluator for use with// taglibs etc.BeanDefinition wipe = new RootBeanDefinition(DefaultWebInvocationPrivilegeEvaluator.class);wipe.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference(fsiId));pc.registerBeanComponent(new BeanComponentDefinition(wipe, pc.getReaderContext().generateBeanName(wipe)));this.fsi = new RuntimeBeanReference(fsiId);}

以上如果实用了useExpressions(有属性use-expressions指定,默认的也是true)即SPEL表达式,则选择WebExpressionVoter,否则选择RoleVoter及AuthenticatedVoter

如果配置了access-decision-manager-ref属性则将accessDecisionManager设置为配置的,否则默认为AffirmativeBased

根据这里描述,本系列的Security配置如下:

<?xml version="1.0" encoding="UTF-8"?><beans:beans xmlns="http://www.springframework.org/schema/security"xmlns:beans="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/securityhttp://www.springframework.org/schema/security/spring-security.xsd"><http auto-config="true" use-expressions="true" entry-point-ref="myAuthenticationEntryPoint" ><form-login login-page="/login"login-processing-url="/**/login"authentication-failure-handler-ref="myAuthenticationFailureHandler"authentication-success-handler-ref="myAuthenticationSuccessHandler" />   <!-- 认证成功用自定义类myAuthenticationSuccessHandler处理 --><logout logout-url="/logout" logout-success-url="/" invalidate-session="true"delete-cookies="JSESSIONID"/><!-- 登录成功后拒绝访问跳转的页面 -->        <access-denied-handler error-page="/security/deny" /><csrf disabled="true" /><intercept-url pattern="/order/**" access="hasRole('ROLE_USER')"/><intercept-url pattern="/manager" access="hasRole('ROLE_MANAGER')"/></http><!-- 使用自定义类myUserDetailsService从数据库获取用户信息 --><authentication-manager>  <authentication-provider user-service-ref="myUserDetailsService">  <!-- 加密 --><password-encoder hash="md5"></password-encoder></authentication-provider></authentication-manager><!-- 被认证请求根据所需权限跳转到不同的登录界面 --><beans:bean id="myAuthenticationEntryPoint" class="com.mango.jtt.springSecurity.MyAuthenticationEntryPoint"><beans:property name="authEntryPointMap" ref="loginFormsMap"></beans:property><beans:constructor-arg name="loginFormUrl" value="/login"></beans:constructor-arg></beans:bean><!-- 根据不同请求所需权限跳转到不同的登录界面 --><beans:bean id="loginFormsMap" class="java.util.HashMap"><beans:constructor-arg><beans:map><beans:entry key="/user/**" value="/login" /><beans:entry key="/manager/**" value="/manager/login" /><beans:entry key="/**" value="/login" /></beans:map></beans:constructor-arg></beans:bean><!-- 授权成功后控制 --><beans:bean id="myAuthenticationSuccessHandler" class="com.mango.jtt.springSecurity.MyAuthenticationSuccessHandler"><beans:property name="authDispatcherMap" ref="dispatcherMap"></beans:property></beans:bean><!-- 根据不同的权限,跳转到不同的页面(直接点击登录页面用) --><beans:bean id="dispatcherMap" class="java.util.HashMap"><beans:constructor-arg><beans:map><beans:entry key="ROLE_USER" value="/"/><beans:entry key="ROLE_MANAGER" value="/manager"/></beans:map></beans:constructor-arg></beans:bean><!-- 登录失败后控制 --><beans:bean id="myAuthenticationFailureHandler" class="com.mango.jtt.springSecurity.MyAuthenticationFailureHandler"><beans:property name="loginEntry" ref="myAuthenticationEntryPoint"></beans:property></beans:bean>
</beans:beans>

本系列实用的accessDecisionManager为AffirmativeBased,Voter为WebExpressionVoter

AffirmativeBased.java

/*** This concrete implementation simply polls all configured* {@link AccessDecisionVoter}s and grants access if any* <code>AccessDecisionVoter</code> voted affirmatively. Denies access only if there* was a deny vote AND no affirmative votes.* <p>* If every <code>AccessDecisionVoter</code> abstained from voting, the decision will* be based on the {@link #isAllowIfAllAbstainDecisions()} property (defaults to* false).* </p>** @param authentication the caller invoking the method* @param object the secured object* @param configAttributes the configuration attributes associated with the method* being invoked** @throws AccessDeniedException if access is denied*/public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {int deny = 0;for (AccessDecisionVoter voter : getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);if (logger.isDebugEnabled()) {logger.debug("Voter: " + voter + ", returned: " + result);}switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if (deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions();}

有一个同意则通过

WebExpressionVoter.java

public int vote(Authentication authentication, FilterInvocation fi,Collection<ConfigAttribute> attributes) {assert authentication != null;assert fi != null;assert attributes != null;WebExpressionConfigAttribute weca = findConfigAttribute(attributes);if (weca == null) {return ACCESS_ABSTAIN;}EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,fi);ctx = weca.postProcess(ctx, fi);return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED: ACCESS_DENIED;}

最终权限的判断交给SecurityExpressionRoot.java

public final boolean hasRole(String role) {return hasAnyRole(role);}public final boolean hasAnyRole(String... roles) {return hasAnyAuthorityName(defaultRolePrefix, roles);}private boolean hasAnyAuthorityName(String prefix, String... roles) {Set<String> roleSet = getAuthoritySet();for (String role : roles) {String defaultedRole = getRoleWithDefaultPrefix(prefix, role);if (roleSet.contains(defaultedRole)) {return true;}}return false;}

getAuthoritySet()获取到目前用户具有的权限,判断其中是否包含请求所需要的权限,如果包含则返回true,否则false。

总结:

Security的鉴权过程设计的别具一格,设计之美令人叹服!Security的学习也到此为止,当然还有很多内容没有涉及到(总体来说,Security包含"authentication" and "authorization" (or"access-control"). 认证和授权)登录的过程是认证,鉴权的过程是授权。



【Spring实战】----Security4.1.3鉴权之美--基于投票的AccessDecisionManager实现及源码分析相关推荐

  1. 【Spring】IOC:基于注解的IOC容器初始化源码分析

    从 Spring2.0 以后的版本中,Spring 也引入了基于注解(Annotation)方式的配置,注解(Annotation)是 JDK1.5 中引入的一个新特性,用于简化 Bean 的配置,可 ...

  2. Spring Security(四) —— 核心过滤器源码分析

    摘要: 原创出处 https://www.cnkirito.moe/spring-security-4/ 「老徐」欢迎转载,保留摘要,谢谢! 4 过滤器详解 前面的部分,我们关注了Spring Sec ...

  3. Spring类型转换源码分析

    前言 本文基于 spring 4.3.13 版本 在项目中我们经常使用 spring 提供的 IOC 功能,目前主要有两种方式:xml.注解,而这两种方式的原理是不同的,xml 的注入主要依赖 Bea ...

  4. spring boot实战(第六篇)加载application资源文件源码分析

    前言 在上一篇中了解了spring配置资源的加载过程,本篇在此基础上学习spring boot如何默认加载application.xml等文件信息的. ConfigFileApplicationLis ...

  5. 【鉴权/授权】基于角色的简单授权认证

    微信公众号:趣编程ACE 关注可了解.NET日常开发技巧.如需源码,请公众号留言 源码; 上文回顾 [鉴权/授权]一步一步实现一个简易JWT鉴权 [鉴权/授权]自定义一个身份认证Handler 授权小 ...

  6. 【实战】Spring生成beanName冲突的解决之道:附源码分析

    一.问题描述 最近公司项目打算模块化,其实一个原因也是为了能够整合公司多个业务的代码,比如一个资源xxx,两个业务中都有对这个资源的管理,虽然是一个资源,但是是完全不同的定义.完全不同的表.不同的处理 ...

  7. Spring Boot - security 实战与源码分析

    2019独角兽企业重金招聘Python工程师标准>>> 一.实现步骤 1.在application.yml中添加起步依赖 2.自定义安全类 package com.example.d ...

  8. Spring Cloud源码分析(二)Ribbon(续)

    因文章长度限制,故分为两篇.上一篇:<Spring Cloud源码分析(二)Ribbon> 负载均衡策略 通过上一篇对Ribbon的源码解读,我们已经对Ribbon实现的负载均衡器以及其中 ...

  9. zookeeper服务发现实战及原理--spring-cloud-zookeeper源码分析

    1.为什么要服务发现? 服务实例的网络位置都是动态分配的.由于扩展.失败和升级,服务实例会经常动态改变,因此,客户端代码需要使用更加复杂的服务发现机制. 2.常见的服务发现开源组件 etcd-用于共享 ...

最新文章

  1. js判断一个对象是否为空
  2. 摄影技巧的种类之一             ——街拍技巧
  3. hibernate查询缓存_Hibernate查询缓存如何工作
  4. php怎么循环输出二维数组,PHP中遍历二维数组—以不同形式的输出操作
  5. Percona-tookit学习笔记(一)
  6. 大数据认证为什么学python_大数据为什么需要学python?
  7. 关于float与double区别
  8. matlab与dsp程序,Matlab生成dsp程序——官方例程学习(4)
  9. 一个完整的、全面k8s化的集群稳定架构(值得借鉴)
  10. 还不知道切图吗,工作实战手把手教你PS切图,0基础轻松掌握
  11. 全球与中国乳制品替代杏仁制品市场深度研究分析报告
  12. 计算机四级大一能考吗,大一新生不让报考四六级?凭什么啊!!!
  13. 职工个人所得税的计算方法
  14. 在线生成条形码(39码、EAN-13)
  15. H5 PCM转WAV实时音频直播代码实现细节
  16. 《高效能人士的七个好习惯》读书笔记
  17. android手机做路由器,手机充当无线路由 给电脑无线信号的方法
  18. 环境因素对车体的影响
  19. c语言实现小学生作业,C语言实现小学生考试系统
  20. 美国网红python微博_Python有趣|微博网红大比拼 !

热门文章

  1. PHP将图片验证码转换成base64格式
  2. 营销策划方案示范文本
  3. 对于毛星云opencv教程中的方法汇总
  4. 进制转换(16转10)
  5. 经典语录 -- 心情down或者hot的时候 来瞅瞅 有新的收获 同时也要留下足迹
  6. 网易云音乐 推荐算法
  7. Google Flutter 1.0正式发布
  8. 小数点化分数的过程_分数和小数互化教学评语
  9. 粉丝问我,写CSDN博客到底为了什么?
  10. webstorm -- 2017 激活破解