SpringSecurity认证授权流程及源码分析

零、前言

上一篇文章讲解了SpringSecurity基础配置,这一篇文章试图从源码角度来分析SpringSecurity的工作流程。

上一篇文章使用的是spirng boot 2.7.0,这篇文章离上一篇文章比较久了,最新版也更新到spirng boot 3.1.5,版本跨度比较大,但是上一篇文章中的新版本配置3.1.5版本中任然是有效的,只是部分配置有点小变化。所以不用担心版本不一样而不能使用了。文章最后会给出一版基于spirng boot 3.1.5版本(对于spring security 6.1.5)的配置,可以相互比较一下。

一、spring security过滤器

最简单的使用spring security的方式就是引入相应的spring boot security依赖,这样访问任何接口都需要认证了。这是为什么呢?

我们都知道,spring mvc中,一个请求都是经历一系列Filter,然后到达DispatcherServlet进行请求处理,而DispatcherServlet是不涉及请求认证的,所以认证过程一定发生在前面的Filter中,也就是说一定存在某些操作往我们的系统中添加了过滤器,导致我们的请求需求认证。

我们先来认识下spring security中定义的过滤器,也可以说是官方提供的Filter

1.1 过滤器

spring security提供的所有过滤器都在FilterOrderRegistration类中声明,越先定义的Filter优先级越高,其中任意两个过滤器之间的优先级顺序相差100。里面使用全限定类名添加的Filter表明该类不在spring-security-config包及其依赖包中,需要单独引入。

java 复制代码
FilterOrderRegistration() {
    Step order = new Step(INITIAL_ORDER, ORDER_STEP);
    put(DisableEncodeUrlFilter.class, order.next());
    put(ForceEagerSessionCreationFilter.class, order.next());
    put(ChannelProcessingFilter.class, order.next());
    order.next(); // gh-8105
    put(WebAsyncManagerIntegrationFilter.class, order.next());
    put(SecurityContextHolderFilter.class, order.next());
    put(SecurityContextPersistenceFilter.class, order.next());
    put(HeaderWriterFilter.class, order.next());
    put(CorsFilter.class, order.next());
    put(CsrfFilter.class, order.next());
    put(LogoutFilter.class, order.next());
    this.filterToOrder.put(
          "org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
          order.next());
    this.filterToOrder.put(
          "org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter",
          order.next());
    put(X509AuthenticationFilter.class, order.next());
    put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
    this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
    this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
          order.next());
    this.filterToOrder.put(
          "org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
          order.next());
    put(UsernamePasswordAuthenticationFilter.class, order.next());
    order.next(); // gh-8105
    put(DefaultLoginPageGeneratingFilter.class, order.next());
    put(DefaultLogoutPageGeneratingFilter.class, order.next());
    put(ConcurrentSessionFilter.class, order.next());
    put(DigestAuthenticationFilter.class, order.next());
    this.filterToOrder.put(
          "org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter",
          order.next());
    put(BasicAuthenticationFilter.class, order.next());
    put(RequestCacheAwareFilter.class, order.next());
    put(SecurityContextHolderAwareRequestFilter.class, order.next());
    put(JaasApiIntegrationFilter.class, order.next());
    put(RememberMeAuthenticationFilter.class, order.next());
    put(AnonymousAuthenticationFilter.class, order.next());
    this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
          order.next());
    put(SessionManagementFilter.class, order.next());
    put(ExceptionTranslationFilter.class, order.next());
    put(FilterSecurityInterceptor.class, order.next());
    put(AuthorizationFilter.class, order.next());
    put(SwitchUserFilter.class, order.next());
}

讲到过滤器,在这里就先说下HttpSecurityaddFilter()addFilterBefore()等方法的区别:

  • addFilter():往过滤器链中添加一个过滤器,添加的过滤器必须位于FilterOrderRegistration中定义的过滤器中,也就是官方定义的Filter
  • addFilterBefore()addFilterAfter()addFilterAtOffsetOf()等方法:用于添加自定义过滤器,并指定过滤器优先级(在官方定义的哪个Filter前执行,在哪个Filter后执行)。当然也可以是官方定义的过滤器,如果添加的是官方定义的Filter,这里指定的优先级不会生效,仍然是官方定义的那个优先级。至于原因,可以去看下源码。所有这里推荐这类方法只用来添加自定义过滤器,当然,你要弄明白是怎么回事了,怎么用都行。

1.2 过滤器拦截过程

上面定义了一系列的Filter,其中ExceptionTranslationFilter是用来处理异常的Filter,而ExceptionTranslationFilterAuthorizationFilter是来校验请求是否通过认证的。ExceptionTranslationFilterspring security 6.0中被标记为过时了,在spring security 7.0中会被删除,故后续都以AuthorizationFilter进行说明。

这里ExceptionTranslationFilter先于AuthorizationFilter执行,这样只需再Filter执行的最外层加一个try catch就能捕获AuthorizationFilter执行的异常,如果请求未认证,则抛出AccessDeniedException异常,由ExceptionTranslationFilter进行处理,进而重定向到登录页面。

下面我们具体来看下ExceptionTranslationFilter实现。

1.2.1 ExceptionTranslationFilter

可以看到,实现很简单,就是在chain.doFilter(request, response)外面包了一层try catch,然后对异常进行处理。

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

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
       throws IOException, ServletException {
    try {
       // 直接继续执行后续的Filter,这里就是执行AuthorizationFilter
       chain.doFilter(request, response);
    }
    // AuthorizationFilter 执行抛出IO异常,不处理,原样抛出
    catch (IOException ex) {
       throw ex;
    }
    // 不是IO异常,则进行异常处理
    catch (Exception ex) {
       // Try to extract a SpringSecurityException from the stacktrace
       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);
       }
       // 处理异常
       handleSpringSecurityException(request, response, chain, securityException);
    }
}

handleSpringSecurityException()

该方法进一步区分了异常类型,并根据不同的异常类型做不同的逻辑处理,这里分为两类:

  • AuthenticationException:表示认证异常,包括用户不存在异常(UsernameNotFoundException)、密码错误异常(UsernameNotFoundException)、账号过期异常(UsernameNotFoundException)异常。所有的AuthenticationException异常如下所示:

  • AccessDeniedException:表示访问被拒绝异常,也就是无权限。分为两种情况:一种是未认证,需要认证;另一种是认证了,但是不具备对资源的访问权限。

java 复制代码
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
       FilterChain chain, RuntimeException exception) throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
       handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
    }
    else if (exception instanceof AccessDeniedException) {
       handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
    }
}

handleAuthenticationException()

该方法是对AuthenticationException异常的处理

java 复制代码
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
       FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
    this.logger.trace("Sending to authentication entry point since authentication failed", exception);
    sendStartAuthentication(request, response, chain, exception);
}
sendStartAuthentication()

该方法用来进行重新认证。包括认证异常AuthenticationException异常以及AccessDeniedException异常里的未认证的情况,都会调用该方法,进行重新认证。主要做了一下三件事:

  1. 设置空的安全上下文
  2. 保存请求,这样登录后能重定向到登录前访问的地址(使用默认配置。spring security可以配置为登录成功后重定向到指定的地址)
  3. 认证端点处理
java 复制代码
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        AuthenticationException reason) throws ServletException, IOException {
    // 1、设置空的安全上下文
    SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
    this.securityContextHolderStrategy.setContext(context);
    // 2、保存请求,这样登录后能重定向到登录前访问的地址
    this.requestCache.saveRequest(request, response);
    // 3、认证端点处理
    this.authenticationEntryPoint.commence(request, response, reason);
}

handleAccessDeniedException()

该方法用来处理AccessDeniedException异常。分两种情况处理:

  • 一种是未认证,需要认证,则调用sendStartAuthentication()方法;
  • 另一种是认证了,但是不具备对资源的访问权限,则调用无权限处理器AccessDeniedHandler进行处理
java 复制代码
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
       FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
    // 判断是否为匿名用户
    Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
    boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
    // 如果是匿名用户或者为rememberMe用户,则要求进行认证
    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);
       }
       this.accessDeniedHandler.handle(request, response, exception);
    }
}

1.2.2 AuthorizationFilter

核心逻辑就是通过授权管理器进行核验,当前用户是否对当前资源有权限,如果没有权限,则抛出AccessDeniedException异常。

java 复制代码
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
       throws ServletException, IOException {

    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;

    // 1、对请求只验证一次,并且该属性名称已经进行过验证,则不进行验证。observeOncePerRequest默认为false,表示每次请求都要验证
    if (this.observeOncePerRequest && isApplied(request)) {
       chain.doFilter(request, response);
       return;
    }

    // 2、部分请求无需验证
    if (skipDispatch(request)) {
       chain.doFilter(request, response);
       return;
    }

    // 3、缓存进行验证过的属性名称,如果observeOncePerRequest为true,则只进行一次验证,后续再次请求时不会验证
    String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
    request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
    try {
        // 4、验证,
       AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
       this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
       // 无权限,则抛出AccessDeniedException
       if (decision != null && !decision.isGranted()) {
          throw new AccessDeniedException("Access Denied");
       }
       chain.doFilter(request, response);
    }
    finally {
       // 清空缓存的属性名称
       request.removeAttribute(alreadyFilteredAttributeName);
    }
}

二、spring security的自动配置

我们先看下spring security的自动配置类,看他做了哪些事情:

2.1 SecurityAutoConfiguration

java 复制代码
@AutoConfiguration(before = UserDetailsServiceAutoConfiguration.class)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(AuthenticationEventPublisher.class)
    public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
       return new DefaultAuthenticationEventPublisher(publisher);
    }

}

核心是注入了一个DefaultAuthenticationEventPublisher类型的bean,用来发布事件;然后导入了两个类SpringBootWebSecurityConfigurationSecurityDataConfiguration,核心类是SpringBootWebSecurityConfiguration,我们详细看下。

SpringBootWebSecurityConfiguration

实现很简单,定义了两个静态内部类。

java 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {

       @Bean
       @Order(SecurityProperties.BASIC_AUTH_ORDER)
       SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
          http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
          http.formLogin(withDefaults());
          http.httpBasic(withDefaults());
          return http.build();
       }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
    @ConditionalOnClass(EnableWebSecurity.class)
    @EnableWebSecurity
    static class WebSecurityEnablerConfiguration {

    }

}

WebSecurityEnablerConfiguration是用来判断项目中是否使用了@EnableWebSecurity注解,如果没有使用,则添加。也就是说我们自定义spring security配置类时,可以不用添加@EnableWebSecurity注解。spring boot强大吧!虽然但是,还是建议自定义配置类时手动添加@EnableWebSecurity注解。

再来看下SecurityFilterChainConfiguration。注入了一个SecurityFilterChain类型的bean,该类是spring security的核心类,我们所有的配置以及过滤器都是位于SecurityFilterChain

该方法做了三件事:

  1. authorizeHttpRequests:配置认证过滤器AuthorizationFilter,所有请求都会被此过滤器拦截,如果未登录,则重定向登录页面;否则同行
  2. formLogin:配置用户使用用户名密码的登录认证过滤器UsernamePasswordAuthenticationFilter,这样我们才能通过用户名密码登录
  3. httpBasic:配置basic认证过滤器BasicAuthenticationFilter,这样我们就能通过添加请求头来进行认证

虽然说spring boot自动配置只做了这三件事,但是别忘了,这里还有一个入参HttpSecurity http,初始HttpSecurity 又是怎样的呢?

我们都知道,配置类中@bean方法的入参来源都是容器的中一个bean对象,所有一定有一个地方注入了一个HttpSecurity类型的bean。其实就是在HttpSecurityConfiguration类中,而该类是由EnableWebSecurity注解引入的,所有呢spring security项目中需要使用EnableWebSecurity注解,也就有了前面的WebSecurityEnablerConfiguration,万一用户忘记了添加EnableWebSecurity注解,也能保证程序不出错。看看,spring boot为用户考虑的周全吧。

2.2 HttpSecurityConfiguration

回到HttpSecurityConfiguration.httpSecurity()来,可以看到他是一个多例bean,而不是单例,这样我们每次自定义配置时使用的HttpSecurity的配置信息都是一样的,不会因为一个地方配置了从而影响另一个地方,bean名称为org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.httpSecurity

该方法主要做了以下几个配置:

  1. 设置认证管理器。通过注入的AuthenticationConfiguration对象来获取AuthenticationManager,我们也可以通过此方式来获取AuthenticationManager
  2. csrf():开启csrfwithDefaults()Customizer接口中的静态方法,表示不手动设置,使用默认配置,我们也可以使用lambda进行自定义配置,后续所有withDefaults()方法都一样,就不重复说明了。
  3. exceptionHandling():启用异常处理器,添加ExceptionTranslationFilter过滤器,我们访问需要认证的地址,会抛出一个异常,然后被该过滤器拦截,重定向到登陆页面。ExceptionTranslationFilter由两个核心属性AuthenticationEntryPointAccessDeniedHandlerAuthenticationEntryPoint用于设置未登录用户的处理逻辑;AccessDeniedHandler用于处理用户登录成功了,但是无权限访问的逻辑,实际使用时我们可以设置这两个属性覆盖掉默认的操作。
  4. apply(new DefaultLoginPageConfigurer<>()):添加默认的登录页的配置,该配置添加了两个过滤器:DefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilter。前者用于定义默认的登录页面,后者用于定义默认的注销页面。
  5. logout(withDefaults()):添加注销配置类(LogoutConfigurer),该配置添加了一个LogoutFilter类型的Filter,用户设置注销请求地址以及注销处理器。
  6. applyDefaultConfigurers():加载spring.factories文件中定义的AbstractHttpConfigurer类型的对象,对于spring boot项目再熟悉不过了。
java 复制代码
@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
    LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
    // 构造认证管理器的构建器
    AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
          this.objectPostProcessor, passwordEncoder);
    // 添加认证管理器
    authenticationBuilder.parentAuthenticationManager(authenticationManager());
    authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
    // 构造HttpSecurity对象
    HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
    WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
    webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
    // @formatter:off
    // 一系列的默认配置
    http
       .csrf(withDefaults())
       .addFilter(webAsyncManagerIntegrationFilter)
       .exceptionHandling(withDefaults())
       .headers(withDefaults())
       .sessionManagement(withDefaults())
       .securityContext(withDefaults())
       .requestCache(withDefaults())
       .anonymous(withDefaults())
       .servletApi(withDefaults())
       .apply(new DefaultLoginPageConfigurer<>());
    http.logout(withDefaults());
    // @formatter:on
    applyDefaultConfigurers(http);
    return http;
}

2.3 源码分析

说了这么多,我们来看下HttpSecurity中的方法,这里以formLogin()来进行说明。

2.3.1 HttpSecurity.formLogin()

实现很简单,new了一个FormLoginConfigurer对象,通过getOrApply()方法添加到AbstractConfiguredSecurityBuilder.configurers集合中。入参的formLoginCustomizer是一个消费者的函数式接口,用于我们自定义FormLoginConfigurer各个属性,如果无需自定义,则可以使用Customizer.withDefaults()

java 复制代码
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
    formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
    return HttpSecurity.this;
}

2.3.2 FormLoginConfigurer

FormLoginConfigurer继承了AbstractAuthenticationFilterConfigurer,添加了一个UsernamePasswordAuthenticationFilter过滤器。

我们直接看init方法,至于其他属性赋值方法可以查看另一篇文章SpringSecurity基础配置 - 掘金 (juejin.cn)

init()

java 复制代码
@Override
public void init(H http) throws Exception {
    super.init(http);
    initDefaultLoginFilter(http);
}

调用了父类的init()方法,然后再初始化默认的登录过滤器

AbstractAuthenticationFilterConfigurer.init()

该方法主要做了三件事:

  1. 更新默认的认证信息
  2. 更新访问权限的默认值
  3. 注册默认的认证端点
java 复制代码
@Override
public void init(B http) throws Exception {
    // 更新默认的认证信息
    updateAuthenticationDefaults();
    // 更新访问权限的默认值。
    updateAccessDefaults(http);
    // 注册默认的认证端点
    registerDefaultAuthenticationEntryPoint(http);
}

protected final void updateAuthenticationDefaults() {
    // 设置登录请求,默认的登录页面请求为Get请求的/login,故登录请求也为/login,只是请求方法为Post请求
    if (this.loginProcessingUrl == null) {
        loginProcessingUrl(this.loginPage);
    }
    // 设置登录失败后的请求地址,如果存在登录失败的Handler,则不会设置,也就是说failureHandler会覆盖掉failureUrl
    if (this.failureHandler == null) {
        failureUrl(this.loginPage + "?error");
    }
    // 设置退出登录后的重定向地址,这里默认为欸/login?logout,表示是退出登录后重定向来的,其实就是默认的登录页
    LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(LogoutConfigurer.class);
    if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
        logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
    }
}

protected final void registerDefaultAuthenticationEntryPoint(B http) {
    // 设置ExceptionHandlingConfigurer中的默认的认证端点,其实就是设置的ExceptionTranslationFilter中的认证断点,请求为认证时的处理逻辑
    registerAuthenticationEntryPoint(http, this.authenticationEntryPoint);
}

protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) {
    // 从共享配置中拿到 ExceptionHandlingConfigurer
    ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
    if (exceptionHandling == null) {
        return;
    }
    exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),
            getAuthenticationEntryPointMatcher(http));
}

上面提到的ExceptionHandlingConfigurer认证端点由AbstractAuthenticationFilterConfigurer构造器赋值:

java 复制代码
protected AbstractAuthenticationFilterConfigurer() {
    setLoginPage("/login");
}

private void setLoginPage(String loginPage) {
    this.loginPage = loginPage;
    this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
}

initDefaultLoginFilter()

其实就是设置DefaultLoginPageGeneratingFilter的各个属性,属性默认值位于DefaultLoginPageGeneratingFilter

java 复制代码
private void initDefaultLoginFilter(H http) {
    // 从共享对象中获取 DefaultLoginPageGeneratingFilter
    DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
       .getSharedObject(DefaultLoginPageGeneratingFilter.class);
    if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
       // 请用表单登录
       loginPageGeneratingFilter.setFormLoginEnabled(true);
       // 设置用户名参数,默认username
       loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
       // 设置密码参数,默认password
       loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
       // 设置登录页面请求,默认/login
       loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
       // 设置登录失败的请求,默认为/login?error
       loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
       // 设置登录请求,默认/login
       loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
    }
}

FormLoginConfigurer并没有重写configure()方法,那我们看下父类的configure()方法。

configure()

主要做了一下几件事:

  1. 设置端口映射器
  2. 设置请求缓存器
  3. 设置认证管理器
  4. 设置认证成功处理器和认证失败处理器
  5. 设置session认证策略
  6. 设置rememberMe服务
  7. 设置安全上下文仓库
  8. 设置安全上下文持有者策略
  9. 添加过滤器
java 复制代码
@Override
public void configure(B http) throws Exception {
    // 1、设置端口映射器
    PortMapper portMapper = http.getSharedObject(PortMapper.class);
    if (portMapper != null) {
       this.authenticationEntryPoint.setPortMapper(portMapper);
    }
    // 2、设置请求缓存器
    RequestCache requestCache = http.getSharedObject(RequestCache.class);
    if (requestCache != null) {
       this.defaultSuccessHandler.setRequestCache(requestCache);
    }
    // 3、设置认证管理器
    this.authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    // 4、设置认证成功处理器和认证失败处理器
    this.authFilter.setAuthenticationSuccessHandler(this.successHandler);
    this.authFilter.setAuthenticationFailureHandler(this.failureHandler);
    if (this.authenticationDetailsSource != null) {
       this.authFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
    }
    // 5、设置session认证策略
    SessionAuthenticationStrategy sessionAuthenticationStrategy = http
       .getSharedObject(SessionAuthenticationStrategy.class);
    if (sessionAuthenticationStrategy != null) {
       this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
    }
    // 6、设置rememberMe服务
    RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
    if (rememberMeServices != null) {
       this.authFilter.setRememberMeServices(rememberMeServices);
    }
    // 7、设置安全上下文仓库
    SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
    if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
       SecurityContextRepository securityContextRepository = securityContextConfigurer
          .getSecurityContextRepository();
       this.authFilter.setSecurityContextRepository(securityContextRepository);
    }
    // 8、设置安全上下文持有者策略,认证通过后,会将认证信息放入这里设置的策略中,这样我们就能通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到认证对象了
    this.authFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
    // 过滤器的后置处理
    F filter = postProcess(this.authFilter);
    // 9、添加过滤器
    http.addFilter(filter);
}

FormLoginConfigurer类的核心内容我们就分析完了,这里总结下

总结

FormLoginConfigurer是一个SecurityConfigurerAdapter对象,包括其他方法如authorizeHttpRequests()添加一个AuthorizeHttpRequestsConfigurer对象;exceptionHandling()方法添加的ExceptionHandlingConfigurer对象,都是SecurityConfigurerAdapter子类。

对于所有的SecurityConfigurerAdapter类,核心方法就两个:init()configure(),前者为初始化方法,后者为配置方法,因此我们分析SecurityConfigurerAdapter子类时从这两个方法入手即可,其他的都是一下属性配置方法。

这里给出了所有SecurityConfigurerAdapter子类,这些类都会添加一个或多个Filter,用于完成对应的功能。

而对于Filter,我们则关注doFilter()方法即可。这里以FormLoginConfigurer添加的UsernamePasswordAuthenticationFilter为例进行进一步说明。

2.3.3 UsernamePasswordAuthenticationFilter

我们打开UsernamePasswordAuthenticationFilter源码,发现其并没有doFilter()方法,但是有一个父类AbstractAuthenticationProcessingFilter,不难想到,doFilter()方法位于其父类中。

AbstractAuthenticationProcessingFilter.doFilter

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

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 immediately as subclass has indicated that it hasn't completed
          return;
       }
       // 认证成功
       // 处理session策略 
       this.sessionStrategy.onAuthentication(authenticationResult, request, response);
       // Authentication success
       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) {
       // Authentication failed
       unsuccessfulAuthentication(request, response, ex);
    }
}

attemptAuthentication()

该方法是一个抽象方法,由子类实现,这里是UsernamePasswordAuthenticationFilter

java 复制代码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
       throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
       throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    // 获取用户名参数
    String username = obtainUsername(request);
    username = (username != null) ? username.trim() : "";
    // 获取密码参数
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    // 初始化认证对象
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
          password);
    // 设置请求详细信息
    setDetails(request, authRequest);
    // 通过认证管理器认证
    return this.getAuthenticationManager().authenticate(authRequest);
}

认证管理器有很多,一般都是由ProviderManager进行处理。

ProviderManager.authenticate()

java 复制代码
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    int currentPosition = 0;
    int size = this.providers.size();
    // 身份验证提供程序逐个进行认证
    for (AuthenticationProvider provider : getProviders()) {
        // 当前认证器不支持,下一个
       if (!provider.supports(toTest)) {
          continue;
       }
       try {
          // 认证
          result = provider.authenticate(authentication);
          if (result != null) {
             copyDetails(authentication, result);
             break;
          }
       }
       catch (AccountStatusException | InternalAuthenticationServiceException ex) {
          ...... //异常处理
       }
       catch (AuthenticationException ex) {
          lastException = ex;
       }
    }
    // 使用父认证器
    if (result == null && this.parent != null) {
       try {
          parentResult = this.parent.authenticate(authentication);
          result = parentResult;
       }
       catch (ProviderNotFoundException ex) {
          // 忽略异常
       }
       catch (AuthenticationException ex) {
          parentException = ex;
          lastException = ex;
       }
    }
    
    if (result != null) {
       ......
	   // 返回认证结果
       return result;
    }
    ......
    // 抛出异常
    throw lastException;
}

对于基于用户名密码的认证方式,使用的是AbstractUserDetailsAuthenticationProvider认证器,其实就是DaoAuthenticationProvider认证器

AbstractUserDetailsAuthenticationProvider.authenticate()

java 复制代码
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    String username = determineUsername(authentication);
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
       cacheWasUsed = false;
       try {
          // 获取 UserDetails 对象,由子类DaoAuthenticationProvider实现
          user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
       }
       catch (UsernameNotFoundException ex) {
          // 如果隐藏UsernameNotFoundException,则抛出BadCredentialsException,为了安全,不管是用户名还是密码错误,都提示密码错误,则增加破解难度
          if (!this.hideUserNotFoundExceptions) {
             throw ex;
          }
          throw new BadCredentialsException(this.messages
             .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
       }
       Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }
    try {
       // 校验账号是否启用,是否锁定等
       this.preAuthenticationChecks.check(user);
       // 校验用户名和密码是否正确
       additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException ex) {
       // 不是缓存数据,则原样抛出异常
       if (!cacheWasUsed) {
          throw ex;
       }
       // 使用了缓存,则重新获取最新的,然后再校验
       cacheWasUsed = false;
       user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
       this.preAuthenticationChecks.check(user);
       additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    this.postAuthenticationChecks.check(user);
    // 将用户对象放入缓存
    if (!cacheWasUsed) {
       this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
       principalToReturn = user.getUsername();
    }
    // 创建认证成功的凭证。之前创建的认证凭证,并不代表认证成功,你可以将其理解为是一个VO对象,用来流转属性。
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

DaoAuthenticationProvider.retrieveUser()

该方法就是调用UserDetailsService接口的loadUserByUsername(),获取UserDetails对象。我们实际使用中不都是会定义一个类,实现UserDetailsService接口,然后在loadUserByUsername()方法中自定义处理逻辑嘛,就是这里调用的。

java 复制代码
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
       throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
       // 调用UserDetailsService接口的loadUserByUsername(),获取UserDetails对象
       UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
       if (loadedUser == null) {
          throw new InternalAuthenticationServiceException(
                "UserDetailsService returned null, which is an interface contract violation");
       }
       return loadedUser;
    }
    catch (UsernameNotFoundException ex) {
       mitigateAgainstTimingAttack(authentication);
       throw ex;
    }
    catch (InternalAuthenticationServiceException ex) {
       throw ex;
    }
    catch (Exception ex) {
       throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

successfulAuthentication()

该方法是认证成功后的处理方法,主要做了一下几件事:

  1. 设置安全上下文,并将其放到安全上下文持有者策略中。这样我们才能通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到上下文信息
  2. rememberMe 的登录成功处理逻辑
  3. 登录成功处理器。如果我们自定义了,就是执行自定义的处理逻辑;如果没有自定义,则使用默认的SimpleUrlAuthenticationSuccessHandler,将请求重定向到指定的url,该url可以通过FormLoginConfigurerdefaultSuccessUrl()方法进行设置
java 复制代码
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
       Authentication authResult) throws IOException, ServletException {
    // 设置安全上下文
    SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
    context.setAuthentication(authResult);
    // 将上下文放到安全上下文持有者策略中,这样我们才能通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到上下文信息
    this.securityContextHolderStrategy.setContext(context);
    this.securityContextRepository.saveContext(context, request, response);
    if (this.logger.isDebugEnabled()) {
       this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }
    // rememberMe 的登录成功处理逻辑
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
       this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    // 登录成功处理器
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

unsuccessfulAuthentication()

该方法用来执行认证失败的处理逻辑。

java 复制代码
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
       AuthenticationException failed) throws IOException, ServletException {
    // 清空上下文信息
    this.securityContextHolderStrategy.clearContext();
    this.logger.trace("Failed to process authentication request", failed);
    this.logger.trace("Cleared SecurityContextHolder");
    this.logger.trace("Handling authentication failure");
    // rememberMe服务的登录失败处理逻辑
    this.rememberMeServices.loginFail(request, response);
    // 执行定义的失败处理器
    this.failureHandler.onAuthenticationFailure(request, response, failed);
}

三、spring security 6.X配置

这里给出新版本的一个配置,供大家参考。

注意不要忘了@EnableWebSecurity注解,前面已经说过,如果忘了spring boot会自动添加,但还是建议手动添加@EnableWebSecurity

spring security 6.0版本和5.0的区别不大,主要就是一些方法被标记为过时了,如authorizeRequests(),同时删除了antMatchers()mvcMatchers()方法,统一使用requestMatchers()

java 复制代码
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    /**
     * Spring Security 白名单url
     */
    @Value("${security.whitelist.urls:/login,/register,/captcha}")
    private String[] whileUrls;

    @Resource
    private RuoYiConfig ruoYiConfig;

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Resource
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Resource
    private MyLogoutSuccessHandler logoutSuccessHandler;

    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    /**
     * 跨域过滤器
     */
    @Resource
    private CorsFilter corsFilter;

    /**
     * 定义密码编码方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 通过 AuthenticationConfiguration 获取 AuthenticationManager
     * @param configuration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    /**
     * 定义spring security配置
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 禁用csrf:基于token认证,故不需要csrf保护
                .csrf(AbstractHttpConfigurer::disable)
                // 禁用session:基于token认证,不需要session
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 请求认证,除了白名单外,所有请求都要认证
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(ruoYiConfig.getWhiteUrls()).permitAll()
                        .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                        .anyRequest().authenticated()
                )
                // 认证失败处理操作
                .exceptionHandling(exception -> exception
                        .authenticationEntryPoint(myAuthenticationEntryPoint)
                        .accessDeniedHandler(myAccessDeniedHandler)
                )
                // 登出操作
                .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
                // 添加jwt认证过滤器,由于JwtAuthenticationFilter是自定义过滤器,不在spring security的过滤器链中,
                // 故需调用addFilterBefore或addFilterAfter方法将其加入到过滤器链中
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                // 添加CorsFilter过滤器,由于CorsFilter存在于spring security的过滤器链中,故直接添加即可,
                // 当然也可以调用addFilterBefore(),本质上和addFilter()方法的逻辑一致。
                .addFilter(corsFilter)
        ;
        return http.build();
    }

}
相关推荐
程序员小凯2 小时前
Spring Boot性能优化详解
spring boot·后端·性能优化
tuine2 小时前
SpringBoot使用LocalDate接收参数解析问题
java·spring boot·后端
番茄Salad3 小时前
Spring Boot项目中Maven引入依赖常见报错问题解决
spring boot·后端·maven
摇滚侠4 小时前
Spring Boot 3零基础教程,yml配置文件,笔记13
spring boot·redis·笔记
!if5 小时前
springboot mybatisplus 配置SQL日志,但是没有日志输出
spring boot·sql·mybatis
阿挥的编程日记5 小时前
基于SpringBoot的影评管理系统
java·spring boot·后端
java坤坤5 小时前
Spring Boot 集成 SpringDoc OpenAPI(Swagger)实战:从配置到接口文档落地
java·spring boot·后端
摇滚侠6 小时前
Spring Boot 3零基础教程,整合Redis,笔记12
spring boot·redis·笔记
荣淘淘6 小时前
互联网大厂Java求职面试全景实战解析(涵盖Spring Boot、微服务及云原生技术)
java·spring boot·redis·jwt·cloud native·microservices·interview
吃饭最爱6 小时前
spring高级知识概览
spring boot