零、前语

上一篇文章讲解了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包及其依靠包中,需求单独引进。

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,然后对反常进行处理。

@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反常如下所示:

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

  • AccessDeniedException:表明拜访被拒绝反常,也便是无权限。分为两种状况:一种是未认证,需求认证;另一种是认证了,可是不具备对资源的拜访权限。

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反常的处理

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. 认证端点处理
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进行处理
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反常。

@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

@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

完结很简单,界说了两个静态内部类。

@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项目再了解不过了。
@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()

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

@Override
public void init(H http) throws Exception {
    super.init(http);
    initDefaultLoginFilter(http);
}

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

AbstractAuthenticationFilterConfigurer.init()

该办法首要做了三件事:

  1. 更新默许的认证信息
  2. 更新拜访权限的默许值
  3. 注册默许的认证端点
@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结构器赋值:

protected AbstractAuthenticationFilterConfigurer() {
    setLoginPage("/login");
}
private void setLoginPage(String loginPage) {
    this.loginPage = loginPage;
    this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
}

initDefaultLoginFilter()

其实便是设置DefaultLoginPageGeneratingFilter的各个特点,特点默许值坐落DefaultLoginPageGeneratingFilter

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. 增加过滤器
@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,用于完结对应的功能。

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

而关于Filter,咱们则重视doFilter()办法即可。这儿以FormLoginConfigurer增加的UsernamePasswordAuthenticationFilter为例进行进一步阐明。

2.3.3 UsernamePasswordAuthenticationFilter

咱们翻开UsernamePasswordAuthenticationFilter源码,发现其并没有doFilter()办法,可是有一个父类AbstractAuthenticationProcessingFilter,不难想到,doFilter()办法坐落其父类中。

AbstractAuthenticationProcessingFilter.doFilter

@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

@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()

@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()

@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()办法中自界说处理逻辑嘛,便是这儿调用的。

@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()办法进行设置
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()

该办法用来履行认证失利的处理逻辑。

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

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