• Spring Security源码(三) 授权分析


    通过前面的分析我们知道经过filterchain的层层赛选后,请求来到了FilterSecurityInterceptor进行权限校验,那么其底层是如何实现的呢,通过本文带你了解其底层实现原理

    一 授权流程整体分析

    在这里插入图片描述

    • 当客户端向某个资源发起请求,请求到达FilterSecurityInterceptor,然后会调用其父类AbstractSecurityInterceptor
      beforeInvocation方法做授权之前的准备工作

    • 在beforeInvocation法中通过SecurityMetadataSource…getAttributes(object);获得资源所需要的访问权限 ,通过SecurityContextHolder.getContext().getAuthentication()获取当前认证用户的认证信息,即包含了认证信息和权限信息的Authentication对象

    • 然后FilterSecurityInterceptor通过调用AccessDecisionManager.decide(authenticated, object, attributes);进行授权(authenticated中有用户的权限列表,attributes是资源需要的权限),该方法使用投票器投票来决定用户是否有资源访问权限
      AccessDecisionManager接口有三个实现类,他们通过通过AccessDecisionVoter投票器完成投票,三种投票策略如下:
      AffirmativeBased : 只需有一个投票赞成即可通过
      ConsensusBased:需要大多数投票赞成即可通过,平票可以配置
      UnanimousBased:需要所有的投票赞成才能通过

    • 投票通过,访问到对应的站点

    二 源码分析

    2.1 FilterSecurityInterceptor的创建

    通过package org.springframework.security.config.http.HttpConfigurationBuilder中createFilterSecurityInterceptor()创建FilterSecurityInterceptor

    void createFilterSecurityInterceptor(BeanReference authManager) {  
        //判断是否配置了use-expressions属性  
      
        //使用 Spring 表达式语言配置访问控制 
        //注意下方备注
        boolean useExpressions = FilterInvocationSecurityMetadataSourceParser.isUseExpressions(httpElt);  
        //根据intercept-url标签列表创建授权需要的元数据信息。httpElt里面有要去的资源路径。根据它们得出securityMds。上面有说明。后面仔细分析  
        BeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser.createSecurityMetadataSource(interceptUrls, httpElt, pc);  
      
        RootBeanDefinition accessDecisionMgr;  
        //创建voter列表,方便使用投票器  
        ManagedList<BeanDefinition> voters =  new ManagedList<BeanDefinition>(2);  
        //根据不同情况使用不同投票器
        //如果是使用了表达式,使用WebExpressionVoter  
        //没使用表达式,就使用RoleVoter、AuthenticatedVoter  
        if (useExpressions) {  
            voters.add(new RootBeanDefinition(WebExpressionVoter.class));  
        } else {  
            voters.add(new RootBeanDefinition(RoleVoter.class));  
            voters.add(new RootBeanDefinition(AuthenticatedVoter.class));  
        }  
        //通过改变BeanDefinition里面的信息,来改变创建对象授权的决策管理类AffirmativeBased的bean
        accessDecisionMgr = new RootBeanDefinition(AffirmativeBased.class);  
        //添加依赖的voter列表  
        accessDecisionMgr.getPropertyValues().addPropertyValue("decisionVoters", voters);  
        accessDecisionMgr.setSource(pc.extractSource(httpElt));  
      
        // Set up the access manager reference for http  
        //access-decision-manager-ref属性,可以使我们手动注入AccessDecisionManager(认证管理器),下面是详细配置
        String accessManagerId = httpElt.getAttribute(""access-decision-manager-ref"");  
        //如果未定义access-decision-manager-ref属性,就使用默认的  
         //AffirmativeBased  
        if (!StringUtils.hasText(accessManagerId)) {  
            accessManagerId = pc.getReaderContext().generateBeanName(accessDecisionMgr);  
            pc.registerBeanComponent(new BeanComponentDefinition(accessDecisionMgr, accessManagerId));  
        }  
        //创建FilterSecurityInterceptor过滤器,通过BeanDefinitionBuilder来手动注入bean,下面有提示
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterSecurityInterceptor.class);  
        //往创建FilterSecurityInterceptor过滤器添加决策管理器  
        //	// 设置属性accessDecisionManager,此属性引用已经定义的String accessManagerId
        builder.addPropertyReference("accessDecisionManager", accessManagerId);  
        //往创建FilterSecurityInterceptor过滤器添加认证管理类  
        //设置属性authenticationManager,此属性引用已经定义的bean authManager
        builder.addPropertyValue("authenticationManager", authManager);  
      
        if ("false".equals(httpElt.getAttribute(ATT_ONCE_PER_REQUEST))) {  
            builder.addPropertyValue("observeOncePerRequest", Boolean.FALSE);  
        }  
        //添加授权需要的安全元数据资源  
        builder.addPropertyValue("securityMetadataSource", securityMds);  
        //得到FilterSecurityInterceptor过滤器的BeanDefinition,通过BeanDefinition创建bean
        BeanDefinition fsiBean = builder.getBeanDefinition();  
        //向ioc容器注册FilterSecurityInterceptor的bean  
        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);  
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • BeanDefinition在Spring中是用来描述Bean对象的,其不是一个bean实例,仅仅是包含bean实例的所有信息,比如属性值、构造器参数以及其他信息。Bean对象创建是根据BeanDefinitionc中描述的信息来创建的,BeanDefinitionc存在的作用是为了可以方便的进行修改属性值和其他元信息,比如通过BeanFactoryPostProcessor进行修改一些信息,然后在创建Bean对象的时候就可以结合原始信息和修改后的信息创建对象了。
    • BeanDefinition spring一开始都是使用GenericBeanDefinition类保存Bean的相关信息,在需要时,在将其转换为其他的BeanDefinition类型
    • BeanDefinitionBuilder作用是手动向BeanDefinition注入信息然后通过BeanDefinition手动Spring容器中注入Bean

    2.2 SecurityMetadataSource的创建

    通过org.springframework.security.config.http.FilterInvocationSecurityMetadataSourceParser中的createSecurityMetadataSource用于SecurityMetadataSource的创建

    static BeanDefinition createSecurityMetadataSource(List<Element> interceptUrls, Element elt, ParserContext pc) {  
        //创建Url处理类,有两个实现:AntUrlPathMatcher、RegexUrlPathMatcher是spring的路径匹配工具类
        UrlMatcher matcher = HttpSecurityBeanDefinitionParser.createUrlMatcher(elt);  
        boolean useExpressions = isUseExpressions(elt);  
        //解析intercept-url标签,构造所有需要拦截url的map信息  
         //map中的key:RequestKey的bean定义,value:SecurityConfig的bean定义  
        ManagedMap<BeanDefinition, BeanDefinition> requestToAttributesMap = parseInterceptUrlsForFilterInvocationRequestMap(  
                interceptUrls, useExpressions, pc);  
        BeanDefinitionBuilder fidsBuilder;  
      
        if (useExpressions) {  
            //定义表达式处理类的bean  
            Element expressionHandlerElt = DomUtils.getChildElementByTagName(elt, Elements.EXPRESSION_HANDLER);  
            String expressionHandlerRef = expressionHandlerElt == null ? null : expressionHandlerElt.getAttribute("ref");  
      
            if (StringUtils.hasText(expressionHandlerRef)) {  
                logger.info("Using bean '" + expressionHandlerRef + "' as web SecurityExpressionHandler implementation");  
            } else {  
                BeanDefinition expressionHandler = BeanDefinitionBuilder.rootBeanDefinition(DefaultWebSecurityExpressionHandler.class).getBeanDefinition();  
                expressionHandlerRef = pc.getReaderContext().generateBeanName(expressionHandler);  
                pc.registerBeanComponent(new BeanComponentDefinition(expressionHandler, expressionHandlerRef));  
            }  
            //定义表达式类型的FilterInvocationSecurityMetadataSource  
            fidsBuilder = BeanDefinitionBuilder.rootBeanDefinition(ExpressionBasedFilterInvocationSecurityMetadataSource.class);  
            //通过构造函数注入依赖  
            fidsBuilder.addConstructorArgValue(matcher);  
            fidsBuilder.addConstructorArgValue(requestToAttributesMap);  
            fidsBuilder.addConstructorArgReference(expressionHandlerRef);  
        } else {  
            //定义非表达式类型的FilterInvocationSecurityMetadataSource  
            fidsBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultFilterInvocationSecurityMetadataSource.class);  
            //通过构造函数注入依赖  
            fidsBuilder.addConstructorArgValue(matcher);  
            fidsBuilder.addConstructorArgValue(requestToAttributesMap);  
        }  
      
        fidsBuilder.addPropertyValue("stripQueryStringFromUrls", matcher instanceof AntUrlPathMatcher);  
        fidsBuilder.getRawBeanDefinition().setSource(pc.extractSource(elt));  
      
        return fidsBuilder.getBeanDefinition();  
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    2.3 FilterSecurityInterceptor鉴权

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)  
            throws IOException, ServletException {  
        //封装request, response, chain,方便参数传递、增加代码阅读性  
        FilterInvocation fi = new FilterInvocation(request, response, chain);  
        invoke(fi);  
    }  
      
    public void invoke(FilterInvocation fi) throws IOException, ServletException {  
        if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)  
                && observeOncePerRequest) {  
            if (fi.getRequest() != null) {  
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);  
            }  
            //执行父类beforeInvocation,类似于aop中的before  
            InterceptorStatusToken token = super.beforeInvocation(fi);  
      
            try {  
                //filter传递  
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());  
            } finally {  
                //执行父类的afterInvocation,类似于aop中的after  
                super.afterInvocation(token, null);  
            }  
        }  
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在FilterSecurityInterceptor中,会调用父类的beforeInvocation(filterInvocation)方法进行处理,最终返回一个InterceptorStatusToken对象,它就是spring security处理鉴权的入口。

    	protected InterceptorStatusToken beforeInvocation(Object object) {
    		Assert.notNull(object, "Object was null");
    		// 1. 判断object是不是FilterInvocation
    		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());
    		}
    		// 2. 获取配置的访问控制规则 any request =》authenticated ,没有配置,return null
    		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
    		if (CollectionUtils.isEmpty(attributes)) {
    			Assert.isTrue(!this.rejectPublicInvocations,
    					() -> "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 (this.logger.isDebugEnabled()) {
    				this.logger.debug(LogMessage.format("Authorized public object %s", object));
    			}
    			publishEvent(new PublicInvocationEvent(object));
    			return null; // no further work post-invocation
    		}
    		// 3. 判断认证对象Authentication是否为null
    		if (SecurityContextHolder.getContext().getAuthentication() == null) {
    			credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
    					"An Authentication object was not found in the SecurityContext"), object, attributes);
    		}
    		// 4. 获取Authentication对象
    		Authentication authenticated = authenticateIfRequired();
    		if (this.logger.isTraceEnabled()) {
    			this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
    		}
    		// Attempt authorization
    		// 5. 进行授权判断
    		attemptAuthorization(object, attributes, authenticated);
    		if (this.logger.isDebugEnabled()) {
    			this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
    		}
    		// 6. 发布授权成功
    		if (this.publishAuthorizationSuccess) {
    			publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    		}
    
    		// Attempt to run as a different user
    		// 7. 对Authentication进行再处理,这里没有处理,直接返回null
    		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
    		if (runAs != null) {
    			SecurityContext origCtx = SecurityContextHolder.getContext();
    			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
    			SecurityContextHolder.getContext().setAuthentication(runAs);
    
    			if (this.logger.isDebugEnabled()) {
    				this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
    			}
    			// need to revert to token.Authenticated post-invocation
    			return new InterceptorStatusToken(origCtx, true, attributes, object);
    		}
    		this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
    		// no further work post-invocation
    		// 8. 返回InterceptorStatusToken
    		return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
    
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    在beforeInvocation方法中的核心方法为attemptAuthorization,它会调用授权管理器进行决策,当失败发生异常时,会爆出异常。

    	/**
    	 * 授权判断
    	 *
    	 * @param object        filter invocation [GET /test]
    	 * @param attributes 配置的URL放行、需要验证路径等配置
    	 * @param authenticated 认证对象
    	 */
    	private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
    			Authentication authenticated) {
    		try {
    			// 1. 调用授权管理器进行决策
    			this.accessDecisionManager.decide(authenticated, object, attributes);
    		} catch (AccessDeniedException ex) {
    			// 2. 访问被拒绝。抛出AccessDeniedException异常
    			if (this.logger.isTraceEnabled()) {
    				this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
    						attributes, this.accessDecisionManager));
    			} else if (this.logger.isDebugEnabled()) {
    				this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
    			}
    			// 3. 发布授权失败事件
    			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
    			throw ex;
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    决策者进行投票

    调用授权管理器进行决策,会进入默认的决策器AffirmativeBased,上面说过它的投票机制,这里获取到的选民只有一个。
    在这里插入图片描述
    在这里插入图片描述

    开始投票

    进入WebExpressionVoter的vote方法开始投票。

    	// 投票
    	@Override
    	public int vote(Authentication authentication, FilterInvocation filterInvocation,
    			Collection<ConfigAttribute> attributes) {
    		// 1. 校验参数
    		Assert.notNull(authentication, "authentication must not be null");
    		Assert.notNull(filterInvocation, "filterInvocation must not be null");
    		Assert.notNull(attributes, "attributes must not be null");
    		// 2. 获取http配置项
    		WebExpressionConfigAttribute webExpressionConfigAttribute = findConfigAttribute(attributes);
    		// 3. 没有配置规则,弃权
    		if (webExpressionConfigAttribute == null) {
    			this.logger
    					.trace("Abstained since did not find a config attribute of instance WebExpressionConfigAttribute");
    			return ACCESS_ABSTAIN;
    		}
    		// 4. 对EL表达式进行处理
    		EvaluationContext ctx = webExpressionConfigAttribute.postProcess(
    				this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);
    		boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);
    		if (granted) {
    			// 5. 符合条件,赞成票
    			return ACCESS_GRANTED;
    		}
    		this.logger.trace("Voted to deny authorization");
    		// 6. 最后都没有则反对票
    		return ACCESS_DENIED;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    授权成功处理

    没有抛出异常,则认为授权通过,FilterSecurityInterceptor会进入finallyInvocation方法。这个方法主要是判断需不需要重新设置 SecurityContext内容,这里没有配置,直接跳过。

        protected void finallyInvocation(InterceptorStatusToken token) {
            if (token != null && token.isContextHolderRefreshRequired()) {
                SecurityContextHolder.setContext(token.getSecurityContext());
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug(LogMessage.of(() -> {
                        return "Reverted to original authentication " + token.getSecurityContext().getAuthentication();
                    }));
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接下来进入后置处理afterInvocation方法,再次调用了finallyInvocation方法,然后查询是否还有决策后置处理器,如果有,再次进行决策。最后的最后,才代表授权成功,就交由Spring MVC ,访问到我们的controller方法了。

        protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
            if (token == null) {
                return returnedObject;
            } else {
                this.finallyInvocation(token);
                if (this.afterInvocationManager != null) {
                    try {
                        returnedObject = this.afterInvocationManager.decide(token.getSecurityContext().getAuthentication(), token.getSecureObject(), token.getAttributes(), returnedObject);
                    } catch (AccessDeniedException var4) {
                        this.publishEvent(new AuthorizationFailureEvent(token.getSecureObject(), token.getAttributes(), token.getSecurityContext().getAuthentication(), var4));
                        throw var4;
                    }
                }
                return returnedObject;
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  • 相关阅读:
    MySQL索引
    拿下!这些证书可以帮你职场晋升!(PMP/CSPM/NPDP)
    【限定词习题】复习
    尚硅谷-SpringMVC篇
    Frida使用与解题
    肠道菌群失调与炎症性肠病的关联
    飞利浦zigbee智能灯泡的软硬件设计
    gcc编译工具
    [蓝桥杯 2018 省 A] 航班时间
    debian 12.5 使用官方源、清华源等,无法安装wget、mysql,如何解决?(操作系统-linux)
  • 原文地址:https://blog.csdn.net/Instanceztt/article/details/128143603