文章

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

image-20240817111945355

前言

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

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

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

Sping Security Overall 架构

image-20240905153452942

要点说明:

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

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

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

  • Spring 提供了一个名为 DelegatingFilterProxyFilter 实现,允许在 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 DeniedAccessDeniedHandler 被调用来处理拒绝访问(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_ADMINISTRATORROLE_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 的子类。例如,UsernamePasswordAuthenticationFilterHttpServletRequest 中提交的 usernamepassword 创建一个 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的实现,它使用 UserDetailsServicePasswordEncoder来验证一个用户名和密码。

image-20240905165840342

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

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

3️⃣ DaoAuthenticationProviderUserDetailsService 中查找 UserDetails

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

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

认证持久化

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

  • HttpSessionSecurityContextRepository

  • RequestAttributeSecurityContextRepository

在Spring Security 6 中,默认会使用DelegatingSecurityContextRepositorySecurityContext 保存到多个 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提供的最常见的 AuthorizationManagerAuthorityAuthorizationManager。它被配置为在当前 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,可以加入一个 CsrfCookieFilterSessionManagementFilter 之后

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

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

License:  CC BY 4.0