likes
comments
collection
share

源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前

作者站长头像
站长
· 阅读数 65

源码分析:Spring Security 表单登录(上)

0、前言

Spring Security 6.2.0 - 表单登录

Spring Security 提供了对通过 HTML 表单提供用户名和密码的支持。本节将详细介绍基于表单的认证在 Spring Security 中如何工作。

本节研究了基于表单的登录在 Spring Security 中是如何工作的。在上半节中,我们解析用户是如何被重定向到登录表单的。在下半节中,我们完成剩余部分的解析,即从用户登录认证开始的剩余认证流程。

源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前

  1. 客户端发出一个未经认证的请求,向服务器请求一个未被授权的资源 /private
  2. Spring Security 的 FilterSecurityInterceptor 抛出一个 AccessDeniedException 来表明未经认证的请求被拒绝了。
  3. 由于用户没有被认证,ExceptionTranslationFilter 捕获了 AccessDeniedException ,并通过 LoginUrlAuthenticationEntryPoint 告诉客户端重定向到 /login
  4. 客户端发送 GET /login 请求。
  5. 服务器返回 login.html

下边从 FilterChainProxy 开始,解释代码执行流程。

1、FilterChainProxy 中的执行流程

1)所有过滤器的核心方法都是 doFilter(...)FilterChainProxydoFilter(...) 方法的核心代码是 doFilterInternal(...) 方法。

/**
 * 过滤器的核心方法,负责处理每个请求并确保安全上下文的清理和异常处理。
 * 这个方法会被调用以处理 HTTP 请求,处理请求前会进行状态检查,确保过滤器的处理逻辑只执行一次。
 *
 * @param request  请求对象,代表客户端的 HTTP 请求。
 * @param response 响应对象,代表服务器向客户端返回的 HTTP 响应。
 * @param chain    过滤器链,允许调用下一个过滤器或最终的目标资源。
 * @throws IOException 如果在处理请求时发生 I/O 错误,则抛出此异常。
 * @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,则抛出此异常。
 */
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    
    // 判断当前请求是否已经通过此过滤器处理(检查 FILTER_APPLIED 属性)
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    
    // 如果请求已经被处理过,则直接调用内部处理方法,跳过后续逻辑
    if (!clearContext) {
        doFilterInternal(request, response, chain);
        return;  // 结束当前处理
    }
    
    // 否则,设置 FILTER_APPLIED 属性,标记该请求已处理
    try {
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        
        // 调用内部的过滤方法进行处理
        doFilterInternal(request, response, chain);
    }
    catch (Exception ex) {
        // 处理异常链,分析异常的根本原因
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
        
        // 获取异常链中的第一个 RequestRejectedException 异常
        Throwable requestRejectedException = this.throwableAnalyzer
                .getFirstThrowableOfType(RequestRejectedException.class, causeChain);
        
        // 如果捕获到的异常不是 RequestRejectedException,重新抛出原异常
        if (!(requestRejectedException instanceof RequestRejectedException)) {
            throw ex;
        }
        
        // 如果是 RequestRejectedException,则调用自定义的处理方法
        // 处理特定的请求拒绝异常
        this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response,
                (RequestRejectedException) requestRejectedException);
    }
    finally {
        // 确保无论如何都会清理 SecurityContextHolder,防止泄漏安全上下文信息
        SecurityContextHolder.clearContext();
        
        // 移除 FILTER_APPLIED 属性,允许下一个请求重新通过过滤器
        request.removeAttribute(FILTER_APPLIED);
    }
}

2)FilterChainProxydoFilterInternal 方法。核心逻辑是创建一个虚拟过滤器链 VirtualFilterChain,按顺序执行所有配置的过滤器。

下一步进入 VirtualFilterChaindoFilter 方法。

/**
 * 过滤器的内部方法,负责对请求应用防火墙规则并执行安全过滤。
 * 如果没有配置安全过滤器,则直接将请求传递给后续处理链。
 * 如果配置了过滤器,则使用 `VirtualFilterChain` 按照顺序执行过滤器。
 *
 * @param request  请求对象,表示客户端的 HTTP 请求。会被封装为 `FirewalledRequest` 进行防火墙处理。
 * @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。会被封装为 `FirewalledResponse`。
 * @param chain    过滤器链,允许调用下一个过滤器或最终的目标资源。
 * @throws IOException 如果处理请求时发生 I/O 错误,抛出该异常。
 * @throws ServletException 如果处理请求时发生 Servlet 相关的异常,抛出该异常。
 */
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

    // 使用防火墙获取经过封装的请求和响应,确保所有传入的请求和响应符合防火墙规则
    FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);

    // 获取请求需要应用的安全过滤器列表
    List<Filter> filters = getFilters(firewallRequest);

    // 如果没有配置过滤器(filters 为空或 null),则直接继续执行请求链
    if (filters == null || filters.size() == 0) {
        // 如果启用了 TRACE 日志级别,记录未应用安全的请求信息
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
        }

        // 重置请求对象,以便后续过滤器或目标链继续处理
        firewallRequest.reset();

        // 调用过滤器链,传递处理后的请求和响应
        chain.doFilter(firewallRequest, firewallResponse);
        return;  // 跳出方法,表示不需要进一步的安全处理
    }

    // 如果启用了 DEBUG 日志级别,记录正在应用安全处理的请求信息
    if (logger.isDebugEnabled()) {
        logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
    }

    // 创建一个虚拟过滤器链,按顺序执行所有配置的过滤器
    VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);

    // 使用虚拟过滤器链执行请求处理,确保所有的过滤器都能按顺序执行
    virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}

3)VirtualFilterChaindoFilter 方法。

/**
 * 过滤器链的核心方法,按顺序执行当前过滤器链中的每个过滤器。
 * 该方法负责管理过滤器的执行顺序,并确保请求在处理完当前过滤器后传递到下一个过滤器。
 * 
 * @param request  请求对象,表示客户端的 HTTP 请求。
 * @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
 * @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
 * @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
 */
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {

    // 判断当前过滤器是否已处理完所有过滤器,如果已处理完所有过滤器,则执行后续操作
    if (this.currentPosition == this.size) {
        
        // 如果启用了 DEBUG 日志级别,记录已经安全处理完的请求信息
        if (logger.isDebugEnabled()) {
            logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
        }

        // 退出安全过滤器链后,禁用路径剥离操作(防止路径信息丢失)
        this.firewalledRequest.reset();

        // 调用原始的过滤器链,继续处理请求和响应
        this.originalChain.doFilter(request, response);
        return;  // 结束当前过滤器处理,转交给下一个处理环节
    }

    // 更新当前过滤器的位置,表示尚未处理所有过滤器
    this.currentPosition++;

    // 获取下一个需要执行的过滤器
    Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);

    // 如果启用了 TRACE 日志级别,记录当前正在调用的过滤器信息
    if (logger.isTraceEnabled()) {
        logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
                this.currentPosition, this.size));
    }

    // 调用下一个过滤器,递归执行过滤器链
    nextFilter.doFilter(request, response, this);
}

4)在 doFilter 方法中,按照过滤器的顺序依次执行。当前背景下,过滤器的执行流程如图:

源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前 这里我们重点关注以下几个过滤器:

  • UsernamePasswordAuthenticationFilter
    • 处理用户名和密码的身份验证请求。通常用于表单登录认证。
  • DefaultLoginPageGeneratingFilter
    • 生成默认的登录页面,当需要登录时显示给用户。
  • ExceptionTranslationFilter
    • 捕捉和转换Spring Security中的异常,通常会将这些异常转化为用户友好的响应或页面。
  • FilterSecurityInterceptor
    • 用于访问控制决策,检查用户是否有权限访问特定的资源或操作。

2、UsernamePasswordAuthenticationFilter 中的执行流程

Filter 的核心代码都在 doFilter 方法中。UsernamePasswordAuthenticationFilter 也是个 Filter,它的 doFilter 方法在父类 AbstractAuthenticationProcessingFilter 中。

1)AbstractAuthenticationProcessingFilterdoFilter 方法调用了重载方法 doFilter

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}

/**
 * 过滤器的核心方法,负责处理请求的身份验证。如果需要身份验证,尝试进行身份验证并处理身份验证结果。
 * 如果身份验证成功,继续执行过滤器链;如果身份验证失败,则处理失败情况。
 *
 * @param request  请求对象,表示客户端的 HTTP 请求。
 * @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
 * @param chain    过滤器链,允许调用下一个过滤器或最终的目标资源。
 * @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
 * @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
 */
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {

    // 判断当前请求是否需要身份验证。如果不需要,直接调用过滤器链继续处理请求。
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;  // 结束当前方法,表示请求无需身份验证,已交给后续过滤器处理
    }

    try {
        // 尝试进行身份验证
        Authentication authenticationResult = attemptAuthentication(request, response);
        
        // 如果身份验证结果为空,表示身份验证未完成,直接返回,等待子类处理
        if (authenticationResult == null) {
            return;
        }

        // 认证成功后,执行身份验证成功时的后续操作(如记录认证信息、设置会话等)
        this.sessionStrategy.onAuthentication(authenticationResult, request, response);

        // 如果身份验证成功且在身份验证成功之前继续过滤器链的执行,则执行链
        if (this.continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        // 执行身份验证成功后的处理逻辑
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    catch (InternalAuthenticationServiceException failed) {
        // 如果发生内部认证服务异常,记录错误日志并处理认证失败的情况
        this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
        unsuccessfulAuthentication(request, response, failed);
    }
    catch (AuthenticationException ex) {
        // 如果身份验证失败,处理认证失败的逻辑
        unsuccessfulAuthentication(request, response, ex);
    }
}

当执行 doFilter 方法时,会先调用 requiresAuthentication 判断当前请求是否需要身份验证。

2)AbstractAuthenticationProcessingFilterrequiresAuthentication 方法。

/**
 * 检查当前请求是否需要进行身份验证。
 * 通过请求匹配器(`requiresAuthenticationRequestMatcher`)判断请求是否需要身份验证。
 * 如果请求匹配该匹配器的条件,返回 `true`,表示请求需要认证;否则返回 `false`。
 *
 * @param request  请求对象,表示客户端的 HTTP 请求。
 * @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
 * @return 如果请求匹配认证条件,返回 `true`,否则返回 `false`。
 */
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
    // 判断请求是否匹配认证条件(通过请求匹配器进行判断)
    if (this.requiresAuthenticationRequestMatcher.matches(request)) {
        return true;  // 如果匹配,表示请求需要身份验证
    }

    // 如果启用了 TRACE 日志级别,记录未匹配认证条件的请求
    if (this.logger.isTraceEnabled()) {
        this.logger
                .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
    }

    // 如果请求不匹配认证条件,返回 false,表示不需要身份验证
    return false;
}

this.requiresAuthenticationRequestMatcher 的值如图:

源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前

也就是说AbstractAuthenticationProcessingFilterrequiresAuthentication 方法会判断请求是否是 POST /login ,如果请求不匹配的话,代表不需要执行认证流程,requiresAuthentication 方法返回 false

AbstractAuthenticationProcessingFilter 的重载方法 doFilter 继续执行 chain.doFilter(request, response); 进入下一个过滤器 DefaultLoginPageGeneratingFilter

3、DefaultLoginPageGeneratingFilter 中的执行流程

DefaultLoginPageGeneratingFilter 中实际执行的是重载方法 doFilter(…) 。如果请求是登录 URL 或者登录错误页面注销成功页面,则生成登录页面的 HTML 内容并返回给客户端。

1)DefaultLoginPageGeneratingFilter 的是重载方法 doFilter(…)

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}

/**
 * 过滤器方法,用于处理登录错误、注销成功或登录 URL 请求。如果请求是登录相关的请求,
 * 则直接返回登录页面的 HTML 内容;否则,将请求传递给下一个过滤器或目标资源。
 *
 * @param request  请求对象,表示客户端的 HTTP 请求。
 * @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
 * @param chain    过滤器链,允许调用下一个过滤器或最终的目标资源。
 * @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
 * @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
 */
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    
    // 判断当前请求是否为登录错误页面
    boolean loginError = isErrorPage(request);
    
    // 判断当前请求是否为注销成功的请求
    boolean logoutSuccess = isLogoutSuccess(request);
    
    // 如果请求是登录 URL 或者登录错误页面或注销成功页面,则生成登录页面的 HTML 内容并返回给客户端
    if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
        // 生成登录页面的 HTML 内容,根据请求类型(登录错误或注销成功)进行相应处理
        String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
        
        // 设置响应类型为 HTML,并指定字符集为 UTF-8
        response.setContentType("text/html;charset=UTF-8");
        
        // 设置响应内容的长度,避免出现响应数据不完整的情况
        response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
        
        // 将生成的登录页面 HTML 写入响应输出流
        response.getWriter().write(loginPageHtml);
        
        // 终止当前方法,避免继续传递请求给后续过滤器链
        return;
    }
    
    // 如果请求不属于上述类型,继续执行过滤器链,传递请求给下一个过滤器或目标资源
    chain.doFilter(request, response);
}

2)isLoginUrlRequest 方法匹配请求是否是 /login 。我们访问的是 /private,与 /login 不匹配。所以最终 DefaultLoginPageGeneratingFilter 会继续执行过滤器链中的下一个过滤器或目标资源。

private boolean isLoginUrlRequest(HttpServletRequest request) {
		return matches(request, this.loginPageUrl);
	}

源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前

执行完 DefaultLoginPageGeneratingFilter 后会继续执行后边的几个过滤器,最终会执行到 ExceptionTranslationFilter

4、ExceptionTranslationFilter 中的执行流程

1)执行重载方法 doFilter(…)

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}

/**
 * 过滤器方法,用于捕获过滤器链中的异常并处理 Spring Security 相关的异常。
 * 如果异常是 Spring Security 中的认证或授权异常(例如 AuthenticationException 或 AccessDeniedException),
 * 会对其进行处理;否则,将异常重新抛出。
 *
 * @param request  请求对象,表示客户端的 HTTP 请求。
 * @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
 * @param chain    过滤器链,允许调用下一个过滤器或最终的目标资源。
 * @throws IOException      如果在处理请求时发生 I/O 错误,抛出该异常。
 * @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
 */
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    try {
        // 尝试通过过滤器链进行过滤操作,如果一切正常,继续处理请求
        chain.doFilter(request, response);
    } catch (IOException ex) {
        // 如果发生 I/O 异常,直接抛出
        throw ex;
    } catch (Exception ex) {
        // 捕获所有非 I/O 异常
        // 尝试从异常链中提取 Spring Security 相关的异常
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);

        // 尝试从异常链中提取 AuthenticationException(身份验证异常)
        RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
                .getFirstThrowableOfType(AuthenticationException.class, causeChain);

        // 如果没有找到 AuthenticationException,尝试提取 AccessDeniedException(权限不足异常)
        if (securityException == null) {
            securityException = (AccessDeniedException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        }

        // 如果既没有 AuthenticationException 也没有 AccessDeniedException,重新抛出原始异常
        if (securityException == null) {
            rethrow(ex);
        }

        // 如果响应已经提交,无法处理异常,则抛出新的 ServletException
        if (response.isCommitted()) {
            throw new ServletException("Unable to handle the Spring Security Exception "
                    + "because the response is already committed.", ex);
        }

        // 处理 Spring Security 的异常(如认证失败或访问权限不足)
        handleSpringSecurityException(request, response, chain, securityException);
    }
}

2)ExceptionTranslationFilter 的核心逻辑就是继续放行到下一个过滤器,然后捕获下游过滤器可能产生的异常,针对不同的异常类型执行不同的逻辑。具体对应的逻辑我们后边遇到再说。

ExceptionTranslationFilter 执行完后,执行最后一个过滤器 FilterSecurityInterceptor

5、FilterSecurityInterceptor 中的执行流程(核心)

1)FilterSecurityInterceptordoFilter 方法中,执行了 invoke(…) 方法

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	invoke(new FilterInvocation(request, response, chain));
}

2)FilterSecurityInterceptorinvoke方法中,关键步骤是调用父类的 beforeInvocation 方法。

/**
 * 处理安全过滤的核心方法,用于在过滤器链中对请求进行安全检查。
 * 该方法执行前置和后置的安全检查,并确保每个请求只被安全过滤器处理一次。
 *
 * @param filterInvocation 包含当前 HTTP 请求、响应和过滤器链的封装对象。
 * @throws IOException      如果在处理请求时发生 I/O 错误,抛出该异常。
 * @throws ServletException 如果在处理请求时发生 Servlet 相关的错误,抛出该异常。
 */
public void invoke(FilterInvocation filterInvocation) throws IOException, 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 方法
    InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
    
    try {
        // 将请求交给过滤器链中的下一个过滤器进行处理
        filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
    } finally {
        // 无论请求处理中是否发生异常,都要确保调用 finallyInvocation 进行清理操作
        super.finallyInvocation(token);
    }
    
    // 进行后置的安全检查,处理安全上下文的清理或状态恢复
    super.afterInvocation(token, null);
}

3)FilterSecurityInterceptor 父类是 AbstractSecurityInterceptorAbstractSecurityInterceptorbeforeInvocation 方法中,关键步骤是调用 attemptAuthorization 方法。

/**
 * 在安全对象执行前进行前置处理的方法。该方法负责获取当前的安全上下文、认证信息,
 * 检查授权,并支持切换到 "RunAs" 身份。返回一个包含当前状态的 InterceptorStatusToken。
 *
 * @param object 传入的安全对象,通常是请求或方法调用,包含需要进行安全检查的内容。
 * @return InterceptorStatusToken 包含了当前的安全上下文和授权信息。如果不需要进一步处理,可能返回 null。
 * @throws IllegalArgumentException 如果传入对象类型与配置的不匹配时抛出异常。
 * @throws AuthenticationCredentialsNotFoundException 如果在安全上下文中找不到认证对象时抛出异常。
 */
protected InterceptorStatusToken beforeInvocation(Object object) {
    // 检查传入的 object 是否为空
    Assert.notNull(object, "Object was null");
    
    // 检查传入对象是否为安全对象的有效类型
    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);
    
    // 如果安全属性为空,检查是否允许公共调用
    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; // 不需要进一步处理,返回 null
    }

    // 如果当前没有认证信息,抛出认证未找到的异常
    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        credentialsNotFound(this.messages.getMessage(
            "AbstractSecurityInterceptor.authenticationNotFound", 
            "An Authentication object was not found in the SecurityContext"), object, attributes);
    }

    // 如果需要,进行用户身份认证
    Authentication authenticated = authenticateIfRequired();
    
    // 调试日志:记录正在进行的授权操作
    if (this.logger.isTraceEnabled()) {
        this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
    }

    // 尝试对传入对象进行授权
    attemptAuthorization(object, attributes, authenticated);

    // 授权成功后,记录调试日志
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
    }

    // 如果配置了发布授权成功事件,发布授权成功事件
    if (this.publishAuthorizationSuccess) {
        publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    }

    // 尝试以不同的身份 ("RunAs") 运行当前用户
    Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
    if (runAs != null) {
        // 获取当前安全上下文
        SecurityContext origCtx = SecurityContextHolder.getContext();
        // 创建一个空的安全上下文,并将身份切换到新的 RunAs 认证
        SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
        newCtx.setAuthentication(runAs);
        SecurityContextHolder.setContext(newCtx);

        // 记录切换后的身份信息
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
        }

        // 返回包含原始安全上下文的 InterceptorStatusToken,以便在方法调用后恢复身份
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }

    // 如果 RunAsManager 没有返回身份切换,则记录调试日志
    this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");

    // 返回当前上下文的 InterceptorStatusToken,标记不需要身份切换
    return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}

4)AbstractSecurityInterceptorattemptAuthorization 方法,关键步骤是调用 this.accessDecisionManager.decide(...)。调用 AccessDecisionManager 来决定用户是否有权限访问该资源。

/**
 * 尝试对当前用户进行授权检查。
 * 该方法会将授权决策委托给 AccessDecisionManager,后者根据用户的认证信息和资源所需的安全属性来判断
 * 用户是否有权限访问指定的资源。
 *
 * @param object 需要访问的资源对象,通常是 HTTP 请求等。
 * @param attributes 与该资源相关的安全配置属性(ConfigAttribute),描述了访问该资源所需的权限。
 * @param authenticated 当前用户的认证对象,包含了用户的凭证和权限信息。
 */
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
        Authentication authenticated) {
    try {
        // 调用 AccessDecisionManager 来决定用户是否有权限访问该资源
        this.accessDecisionManager.decide(authenticated, object, attributes);
    } catch (AccessDeniedException ex) {
        // 如果日志级别为 trace,记录详细的授权失败信息,包括所使用的 AccessDecisionManager
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("授权失败,资源: %s,所需权限: %s,使用的决策管理器: %s", object,
                    attributes, this.accessDecisionManager));
        }
        // 如果日志级别为 debug,仅记录授权失败信息和资源
        else if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("授权失败,资源: %s,所需权限: %s", object, attributes));
        }
        // 触发授权失败事件,传递资源、所需权限、用户认证信息及异常
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
        // 抛出异常,表示授权失败
        throw ex;
    }
}

5)这里的AccessDecisionManager 是其实现类 AffirmativeBaseddecide 方法。

/**
 * 根据多个 AccessDecisionVoter 的投票结果,决定是否授予访问权限。
 * 此方法遍历所有投票者,并根据每个投票者的结果来做出最终的访问决策。
 *
 * @param authentication 当前用户的认证信息,包括凭证和权限。
 * @param object 需要保护的资源或对象(通常是请求对象)。
 * @param configAttributes 资源所需的权限集合(ConfigAttribute)。
 * @throws AccessDeniedException 如果用户被拒绝访问资源,则抛出此异常。
 */
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
        throws AccessDeniedException {
    int deny = 0; // 记录被拒绝投票的次数

    // 遍历所有的 AccessDecisionVoter 进行投票
    for (AccessDecisionVoter voter : getDecisionVoters()) {
        // 投票结果:ACCESS_GRANTED、ACCESS_DENIED 或 ACCESS_ABSTAIN
        int result = voter.vote(authentication, object, configAttributes);
        
        switch (result) {
            case AccessDecisionVoter.ACCESS_GRANTED:
                // 如果有投票者授予访问权限,立即返回,表示访问允许
                return;
            case AccessDecisionVoter.ACCESS_DENIED:
                // 记录被拒绝的投票
                deny++;
                break;
            default:
                // 其他情况,继续循环(ACCESS_ABSTAIN)
                break;
        }
    }

    // 如果有任何投票者拒绝访问,则抛出 AccessDeniedException
    if (deny > 0) {
        throw new AccessDeniedException(
            this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    }

    // 如果所有投票者都选择了弃权,检查是否允许访问
    checkAllowIfAllAbstainDecisions();
}

最终返回的是 ACCESS_DENIED

源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前 所以会在AccessDecisionManager 的实现类 AffirmativeBaseddecide 方法中抛出 AccessDeniedException

6)在AccessDecisionManager 的实现类 AffirmativeBaseddecide 方法中抛出 AccessDeniedException 后,层层抛出,直到在 ExceptionTranslationFilter 中被捕获,执行对应的处理逻辑。

6、ExceptionTranslationFilter 中的异常处理逻辑

1)我们重新回到 ExceptionTranslationFilterdoFilter 方法。当 AccessDeniedException 被捕获后,最终会执行到 handleSpringSecurityException 方法。

/**
 * 过滤器方法,用于捕获过滤器链中的异常并处理 Spring Security 相关的异常。
 * 如果异常是 Spring Security 中的认证或授权异常(例如 AuthenticationException 或 AccessDeniedException),
 * 会对其进行处理;否则,将异常重新抛出。
 *
 * @param request  请求对象,表示客户端的 HTTP 请求。
 * @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
 * @param chain    过滤器链,允许调用下一个过滤器或最终的目标资源。
 * @throws IOException      如果在处理请求时发生 I/O 错误,抛出该异常。
 * @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
 */
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    try {
        // 尝试通过过滤器链进行过滤操作,如果一切正常,继续处理请求
        chain.doFilter(request, response);
    } catch (IOException ex) {
        // 如果发生 I/O 异常,直接抛出
        throw ex;
    } catch (Exception ex) {
        // 捕获所有非 I/O 异常
        // 尝试从异常链中提取 Spring Security 相关的异常
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);

        // 尝试从异常链中提取 AuthenticationException(身份验证异常)
        RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
                .getFirstThrowableOfType(AuthenticationException.class, causeChain);

        // 如果没有找到 AuthenticationException,尝试提取 AccessDeniedException(权限不足异常)
        if (securityException == null) {
            securityException = (AccessDeniedException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        }

        // 如果既没有 AuthenticationException 也没有 AccessDeniedException,重新抛出原始异常
        if (securityException == null) {
            rethrow(ex);
        }

        // 如果响应已经提交,无法处理异常,则抛出新的 ServletException
        if (response.isCommitted()) {
            throw new ServletException("Unable to handle the Spring Security Exception "
                    + "because the response is already committed.", ex);
        }

        // 处理 Spring Security 的异常(如认证失败或访问权限不足)
        handleSpringSecurityException(request, response, chain, securityException);
    }
}

2)ExceptionTranslationFilterhandleSpringSecurityException 方法。异常类型是 AccessDeniedException ,执行 handleAccessDeniedException(...) 方法。

/**
 * 处理访问拒绝异常(AccessDeniedException),根据用户的认证状态决定是重定向到认证入口点,还是调用访问拒绝处理器。
 * 该方法检查当前用户是否为匿名用户或记住我用户,如果是,则重定向到认证入口点;否则,调用访问拒绝处理器处理该请求。
 *
 * @param request 当前 HTTP 请求对象。
 * @param response 当前 HTTP 响应对象。
 * @param chain 当前请求的 FilterChain。
 * @param exception 发生的 AccessDeniedException 异常。
 * @throws ServletException 如果处理过程中发生 Servlet 异常。
 * @throws IOException 如果处理过程中发生输入输出异常。
 */
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
        FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
    
    // 获取当前认证对象
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    
    // 判断当前用户是否为匿名用户或者记住我用户
    boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
    
    // 如果是匿名用户或记住我用户,重定向到认证入口点
    if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
        // 记录日志
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
                    authentication), exception);
        }
        // 发送开始认证请求,提示需要完全认证才能访问资源
        sendStartAuthentication(request, response, chain,
                new InsufficientAuthenticationException(
                        this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
                                "Full authentication is required to access this resource")));
    }
    // 如果不是匿名用户且已经认证,调用访问拒绝处理器
    else {
        // 记录日志
        if (logger.isTraceEnabled()) {
            logger.trace(
                    LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
                    exception);
        }
        // 交给自定义的 accessDeniedHandler 处理访问拒绝
        this.accessDeniedHandler.handle(request, response, exception);
    }
}

3)当前用户为匿名用户,进入 ExceptionTranslationFiltersendStartAuthentication 方法。核心代码是调用了 AuthenticationEntryPointcommence 方法。具体执行的是 AuthenticationEntryPoint 接口的实现类 DelegatingAuthenticationEntryPointcommence 方法。

/**
 * 处理开始认证的逻辑。当用户访问需要完全认证的资源时,清除当前的认证信息并重定向到认证入口点。
 * 该方法会清空当前的 `SecurityContext`,保存当前请求,然后通过认证入口点开始认证过程。
 *
 * @param request 当前 HTTP 请求对象。
 * @param response 当前 HTTP 响应对象。
 * @param chain 当前请求的 FilterChain。
 * @param reason 认证失败的原因,通常是一个 `AuthenticationException`。
 * @throws ServletException 如果处理过程中发生 Servlet 异常。
 * @throws IOException 如果处理过程中发生输入输出异常。
 */
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        AuthenticationException reason) throws ServletException, IOException {
    
    // SEC-112: 清空当前的 SecurityContext 中的认证信息,因为现有的认证信息已经不再有效
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    SecurityContextHolder.setContext(context); // 设置一个新的空的 SecurityContext
    
    // 保存当前的请求,以便认证完成后可以重定向回原始请求
    this.requestCache.saveRequest(request, response);
    
    // 调用认证入口点,开始认证流程,通常会重定向到登录页面
    this.authenticationEntryPoint.commence(request, response, reason);
}

4)DelegatingAuthenticationEntryPointcommence 方法。

/**
 * 实现 `AuthenticationEntryPoint` 接口的 `commence` 方法,处理认证失败时的逻辑。 
 * 它遍历一组预定义的 `RequestMatcher` 来匹配当前请求,找到匹配的入口点后执行相应的认证处理。
 * 如果没有找到匹配的入口点,默认使用配置的默认认证入口点来处理请求。
 *
 * @param request 当前 HTTP 请求对象。
 * @param response 当前 HTTP 响应对象。
 * @param authException 认证异常,表示认证失败的原因。
 * @throws IOException 如果处理过程中发生输入输出异常。
 * @throws ServletException 如果处理过程中发生 Servlet 异常。
 */
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException authException) throws IOException, ServletException {

    // 遍历所有已配置的 RequestMatcher,尝试找到匹配当前请求的入口点
    for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {
        logger.debug(LogMessage.format("Trying to match using %s", requestMatcher));
        
        // 如果找到匹配的 RequestMatcher,则执行相应的 AuthenticationEntryPoint
        if (requestMatcher.matches(request)) {
            AuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher);
            logger.debug(LogMessage.format("Match found! Executing %s", entryPoint));
            entryPoint.commence(request, response, authException);
            return; // 一旦找到匹配的 EntryPoint,就执行完相应逻辑并返回
        }
    }

    // 如果没有找到匹配的 EntryPoint,则使用默认的 EntryPoint 来处理认证失败
    logger.debug(LogMessage.format("No match found. Using default entry point %s", this.defaultEntryPoint));
    this.defaultEntryPoint.commence(request, response, authException);
}

5)通过 LoginUrlAuthenticationEntryPoint 告诉客户端重定向到 /login

源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前

转载自:https://juejin.cn/post/7425803259443707943
评论
请登录