文章

Spring Security+JWT 轻松实现前后端分离的认证架构

见字如面,与大家分享实践中的经验与思考。

大部分软件应用在开发业务之前,都需要完成认证和授权的功能。所以选择一个功能强大,简单易用的框架非常重要,以便于满足各种安全相关的诉求。

Spring Security 是一个强大且易于使用的框架,对 servlet 和 reactive 应用程序都提供支持,提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。

本文将基于SpringBooot 3.3.2 、Spring WebMvc 6.1.11 、 Spring Security 6.3.1 、jsonwebtoken 0.11.5。会先大量篇幅讲解原理,后讲述如何在实际项目中设计前后端分离的落地实践。如果嫌弃原理章节过长,可以直接跳到 前后端分离的认证架构设计模块。

Sping Security Overall 架构

image-20240905153452942

要点说明:

  • 客户端向应用程序发送一个请求,容器创建一个 FilterChain,其中包含 Filter 实例和 Servlet,应该根据请求URI的路径来处理 HttpServletRequest。在Spring MVC应用程序中,Servlet是 DispatcherServlet 的一个实例。

  • 一个 Servlet 最多可以处理一个 HttpServletRequest 和 HttpServletResponse。然而,可以使用多个 Filter 来完成不同的工作。

  • 由于一个 Filter 只影响下游的 Filter 实例和 Servlet,所以每个 Filter 的调用顺序是非常重要的。

  • Spring 提供了一个名为 DelegatingFilterProxy 的 Filter 实现,允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。

  • DelegatingFilterProxy 的另一个好处是,它允许延迟查找 Filter Bean实例。这一点很重要,因为在容器启动之前,容器需要注册 Filter 实例。然而, Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,这在需要注册 Filter 实例之后才会完成。

  • FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter,允许通过 SecurityFilterChain 委托给许多 Filter 实例。由于 FilterChainProxy 是一个Bean,它通常被包裹在 DelegatingFilterProxy 中。

  • SecurityFilterChain被 FilterChainProxy 用来确定当前请求应该调用哪些 Spring Security Filter 实例。确定何时应该调用 SecurityFilterChain 方面提供了更大的灵活性。在Servlet容器中,Filter 实例仅基于URL被调用。 然而,FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用。

  • 有多个 SecurityFilterChain 的话, FilterChainProxy 决定应该使用哪个 SecurityFilterChain。只有第一个匹配的 SecurityFilterChain 被调用。如果请求的URL是 /api/messages/,它首先与 /api/** 的 SecurityFilterChain_0 模式匹配,所以只有 SecurityFilterChain_0 被调用,尽管它也与 SecurityFilterChain_n 匹配。如果请求的URL是 /messages/,它与 /api/** 的 SecurityFilterChain_0 模式不匹配,所以 FilterChainProxy 继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例相匹配,则调用SecurityFilterChain_n。

  • Security Filter 是通过 SecurityFilterChain API 插入 FilterChainProxy 中的。Filter 的执行是有顺序的,可以查看FilterOrderRegistration 代码。也可以在控制台看到项目中所使用的 Security Filter 列表:

2024-09-05 15:56:47.152 DEBUG 41171 [] [yueji] [restartedMain] o.s.s.web.DefaultSecurityFilterChain     Will secure any request with filters: 
DisableEncodeUrlFilter, 
WebAsyncManagerIntegrationFilter, 
SecurityContextHolderFilter, 
HeaderWriterFilter, 
CorsFilter, 
CsrfFilter, 
LogoutFilter, 
JwtAuthenticationFilter, 
RequestCacheAwareFilter, 
SecurityContextHolderAwareRequestFilter, 
AnonymousAuthenticationFilter, 
SessionManagementFilter, 
CsrfCookieFilter, 
ExceptionTranslationFilter, 
AuthorizationFilter

Spring Security 异常处理

image-20240905160738642

  • 1️⃣ 首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response) 来调用应用程序的其他部分。

  • 2️⃣ 如果用户没有被认证,或者是一个 AuthenticationException,那么就 开始认证。

    • SecurityContextHolder 被清理掉。

    • HttpServletRequest 被保存起来,这样一旦认证成功,它就可以用来重放原始请求。

    • AuthenticationEntryPoint 用于请求客户的凭证。例如,它可以重定向到一个登录页面或发送一个 WWW-Authenticate 头。

  • 3️⃣ 否则,如果是 AccessDeniedException,那么就是 Access Denied。 AccessDeniedHandler 被调用来处理拒绝访问(access denied)。

ExceptionTranslationFilter 的伪代码看起来是这样的。

image-20240905161121401

Spring Security 认证架构

  • SecurityContextHolder - SecurityContextHolder 是 Spring Security 存储认证用户细节的地方。

  • SecurityContext - 是从 SecurityContextHolder 获得的,包含了当前认证用户的 Authentication (认证)。

  • Authentication - 可以是 AuthenticationManager 的输入,以提供用户提供的认证凭证或来自 SecurityContext 的当前用户。

  • GrantedAuthority- 在 Authentication (认证)中授予委托人的一种权限(即role、scope等)。

  • AuthenticationManager- 定义 Spring Security 的 Filter 如何执行 认证 的API。

  • ProviderManager - 最常见的 AuthenticationManager 的实现。

  • AuthenticationProvider- 由 ProviderManager 用于执行特定类型的认证。

  • 用 AuthenticationEntryPoint 请求凭证- 用于从客户端请求凭证(即重定向到登录页面,发送 WWW-Authenticate 响应,等等)。

  • AbstractAuthenticationProcessingFilter - 一个用于认证的基本 Filter。这也让我们很好地了解了认证的高层流程以及各部分是如何协作的。

SecurityContextHolder

securitycontextholder

默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储这些细节,这意味着 SecurityContext 对同一线程中的方法总是可用的,即使 SecurityContext 没有被明确地作为参数传递给这些方法。如果你注意在处理完当前委托人的请求后清除该线程,以这种方式使用 ThreadLocal 是相当安全的。Spring Security 的 FilterChainProxy确保 SecurityContext 总是被清空。

SecurityContext

SecurityContext 是从 SecurityContextHolder 中获得的。SecurityContext 包含一个 Authentication 对象。

Authentication

Authentication 接口在Spring Security中主要有两个作用。

  • 对 AuthenticationManager的一个输入,用于提供用户为验证而提供的凭证。当在这种情况下使用时,isAuthenticated() 返回 false。

  • 代表当前认证的用户。你可以从 SecurityContext 中获得当前的 Authentication。

认证(Authentication)包含了:

  • principal: 识别用户。当用用户名/密码进行认证时,这通常是 UserDetails 的一个实例。

  • credentials: 通常是一个密码。在许多情况下,这在用户被认证后被清除,以确保它不会被泄露。

  • authorities: GrantedAuthority 实例是用户被授予的高级权限。两个例子是角色(role)和作用域(scope)

GrantedAuthority

你可以从 Authentication.getAuthorities()法中获得 GrantedAuthority 实例。这个方法提供了一个 GrantedAuthority 对象的集合。例如 ROLE_ADMINISTRATOR 或 ROLE_HR_SUPERVISOR。

AuthenticationManager

AuthenticationManager 是定义 Spring Security 的 Filter 如何执行认证的API。返回的 认证(Authentication) 是由调用 AuthenticationManager 的控制器(即 Spring Security的 Filter 实例)在 SecurityContextHolder上设置的。

AuthenticationManager 最常见的实现是ProviderManager。

ProviderManager

ProviderManager是最常用的AuthenticationManager的实现。ProviderManager 委托给一个 List``AuthenticationProvider 实例。每个 AuthenticationProvider 都有机会表明认证应该是成功的、失败的,或者表明它不能做出决定并允许下游的 AuthenticationProvider 来决定。如果配置的 AuthenticationProvider 实例中没有一个能进行认证,那么认证就会以 ProviderNotFoundException 而失败,这是一个特殊的 AuthenticationException,表明 ProviderManager 没有被配置为支持被传入它的 Authentication 类型。

image-20240905163712128

在实践中,每个 AuthenticationProvider 都知道如何执行特定类型的认证(Authentication)。例如,一个 AuthenticationProvider 可能能够验证一个用户名/密码,而另一个可能能够验证一个 SAML 断言。这让每个 AuthenticationProvider 在支持多种类型的认证的同时,可以做一种非常具体的认证类型,并且只暴露一个 AuthenticationManager Bean。

可以一个 ProviderManager 的实例,也可以多个 ProviderManager 实例可能共享同一个父级 AuthenticationManager。。

image-20240905164326675

AuthenticationProvider

你可以在 ProviderManager 中注入多个 AuthenticationProvider实例。每个 AuthenticationProvider 都执行一种特定类型的认证。例如, DaoAuthenticationProvider支持基于用户名/密码的认证,而 JwtAuthenticationProvider 支持认证 JWT令牌。

AuthenticationEntryPoint 请求凭证

AuthenticationEntryPoint用于发送一个要求客户端提供凭证的HTTP响应。

有时,客户端会主动包含凭证(如用户名和密码)来请求资源。在这些情况下,Spring Security 不需要提供要求客户端提供凭证的HTTP响应,因为这些凭证已经被包括在内。

在其他情况下,客户端向他们未被授权访问的资源发出未经认证的请求。在这种情况下, AuthenticationEntryPoint 的实现被用来请求客户端的凭证。 AuthenticationEntryPoint 的实现可能会执行重定向到一个登录页面,用 WWW-Authenticate 头来响应,或采取其他行动。

AbstractAuthenticationProcessingFilter

image-20240905164839244

AbstractAuthenticationProcessingFilter 被用作验证用户凭证的基础 Filter。在认证凭证之前,Spring Security 通常通过使用AuthenticationEntryPoint来请求凭证。

1️⃣ 当用户提交他们的凭证时,AbstractAuthenticationProcessingFilter 会从 HttpServletRequest 中创建一个要认证的Authentication。创建的认证的类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter从 HttpServletRequest 中提交的 username 和 password 创建一个 UsernamePasswordAuthenticationToken。

2️⃣ 接下来,Authentication被传入 AuthenticationManager,以进行认证。

3️⃣ 如果认证失败,则为 Failure。

  • SecurityContextHolder 被清空。

  • RememberMeServices.loginFail 被调用。如果没有配置记住我(remember me),这就是一个无用功。

  • AuthenticationFailureHandler 被调用。参见 AuthenticationFailureHandler接口。

4️⃣ 如果认证成功,则为 Success。

  • SessionAuthenticationStrategy 被通知有新的登录。参见 SessionAuthenticationStrategy接口。

  • Authentication 是在 SecurityContextHolder上设置的。如果你需要保存 SecurityContext 以便在未来的请求中自动设置,必须显式调用 SecurityContextRepository#saveContext。参见 SecurityContextHolderFilter类。

  • RememberMeServices.loginSuccess 被调用。如果没有配置 remember me,这就是一个无用功。

  • ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent 事件。

  • AuthenticationSuccessHandler 被调用。参见 AuthenticationSuccessHandler接口。

DaoAuthenticationProvider

DaoAuthenticationProvider是一个 AuthenticationProvider的实现,它使用 UserDetailsService和 PasswordEncoder来验证一个用户名和密码。

image-20240905165840342

1️⃣ 读取用户名和密码的认证 Filter 将 UsernamePasswordAuthenticationToken 传递给 AuthenticationManager,它由 ProviderManager实现。

2️⃣ ProviderManager 被配置为使用一个 DaoAuthenticationProvider 类型的 AuthenticationProvider。

3️⃣ DaoAuthenticationProvider 从 UserDetailsService 中查找 UserDetails。

4️⃣ DaoAuthenticationProvider 使用 PasswordEncoder 来验证上一步返回的 UserDetails 上的密码。

5️⃣ 当认证成功时,返回的 Authentication是 UsernamePasswordAuthenticationToken 类型,并且有一个委托人(principal)是由配置的 UserDetailsService 返回的 UserDetails。最终,返回的 UsernamePasswordAuthenticationToken 被认证 Filter 设置在 SecurityContextHolder上。

认证持久化

通过 SecurityContextRepository实现的。SecurityContextRepository 的默认实现是 DelegatingSecurityContextRepository,它委托给:

  • HttpSessionSecurityContextRepository

  • RequestAttributeSecurityContextRepository

在Spring Security 6 中,默认会使用DelegatingSecurityContextRepository将 SecurityContext 保存到多个 SecurityContextRepository 委托(delegate),并允许按指定顺序从任何委托(delegate)中检索。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  http
    // ...
    .securityContext((securityContext) -> securityContext
      .securityContextRepository(new DelegatingSecurityContextRepository(
        new RequestAttributeSecurityContextRepository(),
        new HttpSessionSecurityContextRepository()
      ))
    );
  return http.build();
}

注意,如果使用的是前后端分离的项目,需要配置如下:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .sessionManagement((session) -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );
    return http.build();
}

上述配置是将 SecurityContextRepository 为使用 NullSecurityContextRepository,同时也是为了防止请求被保存在会话中。这时后端没有存储认证信息,需要无状态 Token,类似于 JWT 的实现。

Spring Security 授权架构

image-20240905173835259

Spring Security提供的最常见的 AuthorizationManager 是 AuthorityAuthorizationManager。它被配置为在当前 Authentication 中寻找一组给定的授权。如果 Authentication 包含任何配置的授权,它将返回 positive 的 AuthorizationDecision。否则,它将返回一个 negative 的 AuthorizationDecision。

另一个管理器是 AuthenticatedAuthorizationManager。它可以用来区分匿名、完全认证和记住我认证的用户。许多网站在Remember-me认证下允许某些有限的访问,但要求用户通过登录来确认他们的身份以获得完整的访问。

很明显,你也可以实现一个自定义的 AuthorizationManager,你可以把你想要的任何访问控制逻辑放在里面。它可能是针对你的应用程序的(与业务逻辑有关),也可能实现一些安全管理逻辑。

Spring Security 主要提供 授权 HTTP 请求 和 方法注解 两种方式进行授权处理,也可以自定义授权方式。具体参考官网:

JWT 简单理解

JSON Web Token(JWT): 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT 被设计为紧凑且安全的,特别适用于前后端无状态认证的场景

JWT 总共有三个部分:JWT头(header) 、有效载荷(payload) 、签名(signature)

JWT头(header):

{"alg": "HS256","typ": "JWT"}

alg:表示签名使用的算法,默认为HMAC SHA256(写为HS256)

typ:表示令牌的类型,JWT令牌统一写为JWT

有效载荷(payload):

有效载荷部分规定有如下七个默认字段供选择:

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

注意: 除以上默认字段外,还可以自定义私有字段

签名(signature):

签名实际上是一个加密的过程,是对上面两部分数据通过指定的算法生成哈希,以确保数据不会被篡改。在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。

前后端分离的认证架构设计

在了解 Spring Security 的原理之后,我们需要搭建一个前后端分离的认证授权体系。并通过 JWT Token 完成无状态的认证。

前端发起登录:

image-20240905183334371

前端发起接口请求: 使用 JWT token 访问后端资源,并且加入 CSRF 跨站伪造抵御

image-20240905185153160

前端设计一个 api 请求工具类:

image-20240905185919578

image-20240905190436949

后端 HttpSecurity 核心配置:

image-20240905185326778

最后,因为 JWT Token 是无状态的,无法做到踢出等功能,可以考虑将 token 存储在 数据库 或者 Redis 中。

附录

整体代码目录设计

image-20240905185540809

建议使用 Lambda DSL

@Configuration
@EnableWebSecurity
public class SecurityConfig {
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/blog/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(formLogin -> formLogin
                .loginPage("/login")
                .permitAll()
            )
            .rememberMe(Customizer.withDefaults());
​
        return http.build();
    }
}

不使用 Lambda 方式:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests()
                .requestMatchers("/blog/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .rememberMe();
​
        return http.build();
    }
}

使用 Lambda DSL 的好处:

  • 以前的方式不清楚什么对象被配置了,嵌套越深,越令人困惑

  • 一致性

密码存储最佳实践

历史遗留问题:

  • 许多应用程序使用旧的密码编码(password encode),不能轻易迁移。

  • 密码存储的最佳实践将再次改变。

  • 作为一个框架,Spring Security 不能频繁地进行破坏性的改变。

Spring Security 新版引入了 DelegatingPasswordEncoder,它通过以下方式解决了所有的问题。

  • 确保通过使用当前的密码存储建议对密码进行编码。

  • 允许验证现代和传统格式的密码。

  • 允许在未来升级编码

所以它的密码格式为:

{id}encodedPassword

源码中查看几乎兼容了现在主流的密码存储格式:

public static PasswordEncoder createDelegatingPasswordEncoder() {
  String encodingId = "bcrypt";
  Map<String, PasswordEncoder> encoders = new HashMap();
  encoders.put(encodingId, new BCryptPasswordEncoder());
  encoders.put("ldap", new LdapShaPasswordEncoder());
  encoders.put("MD4", new Md4PasswordEncoder());
  encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
  encoders.put("noop", NoOpPasswordEncoder.getInstance());
  encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
  encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
  encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
  encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
  encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
  encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
  encoders.put("sha256", new StandardPasswordEncoder());
  encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
  encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
  return new DelegatingPasswordEncoder(encodingId, encoders);
}

样例:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

注意:如果你之前项目使用的是 BCryptPasswordEncoder ,现在使用了 DelegatingPasswordEncoder,需要将旧密码加上前缀{bcrypt},不然无法解析密码。

CSRF 跨站请求伪造攻击

Spring Security 跨站请求伪造攻击的全面支持。简单理解为在已经登录的情况下,提交一个表单请求,如果这时外部通过 js 伪造相同请求,导致数据传输给了外部非法系统。

Spring Security 大致的原理是加入一个 CsrfToken 令牌,前端每次请求后端是通过 Http Header 或者 Attribute 属性传入该值,后端进行匹配,匹配失败,接口访问失败。如下图:

image-20240905111850551

大部分项目都是前后端分离的,而且每次接口认证请求完成后,CsrfAuthenticationStrategy 会将令牌清空,所以大致有两种方式实现:

  1. 方案一:后端提供一个接口,让前端获取到 CSRF Token,类似于用户登录的 Access Token,但是缺点比较明显,每次请求前都需要获取一次,性能较差,而且影响面广

  2. 后端使用 Cookie 存储 CSRF Token,前端每次请求接口从 cookie 中取出,但是前端发起连续的 api 请求时,会导致拿不到 CSRF Token,可以加入一个 CsrfCookieFilter 在 SessionManagementFilter 之后

以下是第二种方式的实现逻辑:

1)自定义Handler 和 Filter 类

package top.flyeric.yueji.app.infrastructure.config.security.csrf;
​
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.util.StringUtils;
​
import java.util.function.Supplier;
​
public class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
    private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
​
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
        this.delegate.handle(request, response, csrfToken);
    }
​
    @Override
    public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
        if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
            return super.resolveCsrfTokenValue(request, csrfToken);
        }
        return this.delegate.resolveCsrfTokenValue(request, csrfToken);
    }
}
​

@Slf4j
public class CsrfCookieFilter extends OncePerRequestFilter {
​
    private final CsrfTokenRepository csrfTokenRepository;
​
    public CsrfCookieFilter(CsrfTokenRepository csrfTokenRepository) {
        this.csrfTokenRepository = csrfTokenRepository;
    }
​
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
​
        // 从请求中获取 CSRF token
        CsrfToken csrfToken = csrfTokenRepository.loadToken(request);
​
        // 如果没有 CSRF token,则生成一个新的
        if (csrfToken == null) {
            csrfToken = csrfTokenRepository.generateToken(request);
            csrfTokenRepository.saveToken(csrfToken, request, response);
        }
​
        log.info("[CsrfCookieFilter] add csrf token to cookie");
​
        filterChain.doFilter(request, response);
    }
​
}

2)后端 Http Security 配置

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    http
            .csrf(csrf -> csrf
                    .ignoringRequestMatchers("/auth/**")
                    .csrfTokenRepository(csrfTokenRepository)
                    .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
            )
            //.csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterAfter(new CsrfCookieFilter(csrfTokenRepository), SessionManagementFilter.class)
            // 省略其他配置。。。
    ;
    return http.build();
}

image-20240905150830439

3)前端从 cookie 中获取 CSRF token,并在每次请求时放入 Header 中:

image-20240905151017129

安全相关的HTTP相应头

Spring Security提供了一套默认的安全相关的HTTP响应头,以提供安全的默认值。例如:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 0

许可协议:  CC BY 4.0