【Spring Security】 授权 流程分析

Metadata

title: 【Spring Security】 授权 流程分析
date: 2023-02-02 14:36
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/SpringSecurity
categories:
  - SpringSecurity
keywords:
  - SpringSecurity
description: 【Spring Security】 授权 流程分析

【Spring Security】 授权 流程分析

1. 进入 ExceptionTranslationFilter

首先会进入 ExceptionTranslationFilter 的 doFilter 方法,之前说过这个过滤器的主要作用是拦截异常并处理。

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            // 1. 请求直接放行
            chain.doFilter(request, response);
        } catch (IOException ex) {
            throw ex;
        } catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            // 2. 捕获后续出现的异常
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
            RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException) this.throwableAnalyzer
                        .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }
            if (securityException == null) {
                rethrow(ex);
            }
            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception "
                        + "because the response is already committed.", ex);
            }
            // 3. 处理发生的异常
            handleSpringSecurityException(request, response, chain, securityException);
        }
    }

2. 进入 FilterSecurityInterceptor

接下来进入 FilterSecurityInterceptor 过滤器,他的 doFilter 方法,调用的是自身的 invoke(FilterInvocation filterInvocation) 方法。该方法完成了整个访问控制逻辑。

    /**
     *  doFilter实际执行的方法
     * @param filterInvocation 封装了request response 过滤器链的对象
     */
    public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        // 1. 如果已经执行过该过滤器,直接放行
        if (isApplied(filterInvocation) && this.observeOncePerRequest) {
            // filter already applied to this request and user wants us to observe
            // once-per-request handling, so don't re-do security checking
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
            return;
        }
        // first time this request being called, so perform security checking
        // 2. 第一次调用这个请求,所以执行安全检查
        if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
            // 3. 在request中添加__spring_security_filterSecurityInterceptor_filterApplied = true,表示执行了该过滤器
            filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }
        // 4. 前置访问控制处理
        InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
        try {
            // 5. 放行		filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } finally {
            super.finallyInvocation(token);
        }
        // 6. 后置处理
        super.afterInvocation(token, null);
    }

3. 进入 AbstractSecurityInterceptor

在 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);

    }

在 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;
        }
    }

4. 决策者进行投票

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

循环投票者,并开始计票。

5. 开始投票

进入 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;
    }

6. 对表达式进行处理

投票中的核心代码为:

EvaluationContext ctx = webExpressionConfigAttribute.postProcess(this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);
            boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);

首先会创建 EL 表达式的上下文。

this.expressionHandler.createEvaluationContext(authentication, filterInvocation)

然后调用 ExpressionUtils 工具类对 EL 表达式进行处理,最终调用的是 SpelExpression 中的 getValue 方法。

第一次是对配置类中的规则进行校验,这里是 anyRequest()).authenticated(),因为登录了,所以这个投票是通过的。

第二次是对我们配置了权限注解的方法进行校验。

会首先获取到我们请求方法上的 EL 表达式,然后进行配置校验,涉及到 EL 的相关知识,这里后续介绍。

表达式检验之后,这个当前用户有这个角色,所以投票通过,加下来就要进行授权成功处理了。

7. 授权成功处理

没有抛出异常,则认为授权通过,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();
                }));
            }
        }
    }

接下来进入后置处理 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;
        }
    }