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();
    }

}
相关推荐
攻心的子乐23 分钟前
shell脚本启动springboot项目
spring boot
程序媛-徐师姐1 小时前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
.生产的驴1 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
bjzhang752 小时前
SpringBoot开发——Maven多模块工程最佳实践及详细示例
spring boot·maven·maven多模块工程
chusheng18402 小时前
Java项目-基于SpringBoot+vue的租房网站设计与实现
java·vue.js·spring boot·租房·租房网站
计算机毕设孵化场3 小时前
计算机毕设-基于springboot的高校网上缴费综合务系统视频的设计与实现(附源码+lw+ppt+开题报告)
java·spring boot·计算机外设·音视频·课程设计·高校网上缴费综合务系统视频·计算机毕设ppt
码蜂窝编程官方4 小时前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
许苑向上5 小时前
Dubbo集成SpringBoot实现远程服务调用
spring boot·后端·dubbo
郑祎亦5 小时前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
计算机毕设指导67 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea