当前位置: 首页 > news >正文

JWT与Spring Security整合实战:从原理到安全陷阱全解析

1. 项目概述:为什么我们需要深入理解JWT与Spring Security

在构建现代Web应用,尤其是微服务架构时,身份认证与授权是绕不开的核心议题。我见过太多项目,初期为了快速上线,简单地在Session里存个用户ID就完事了,结果随着业务拆分、移动端接入、第三方集成需求的到来,整个认证体系变得臃肿不堪,甚至漏洞百出。这时,JWT(JSON Web Token)和Spring Security这对组合就频繁地出现在技术选型的讨论桌上。JWT提供了一种无状态的、自包含的令牌机制,而Spring Security则是一个功能强大且高度可定制的安全框架。听起来很美好,对吧?但坑也恰恰埋在这里。很多人以为引入这两个依赖,配置几个注解,系统就安全了。实际上,这仅仅是开始。错误地使用JWT会导致令牌被伪造、泄露,而Spring Security配置不当则会引入CSRF、权限绕过等经典漏洞。这篇文章,我将结合我过去在多个分布式项目中趟过的坑,为你拆解JWT的核心原理、Spring Security的整合之道,以及那些开发文档里不会明说,但线上故障一定会教你的安全问题。无论你是正在为单体应用寻找更优雅的认证方案,还是在为微服务设计统一的安全网关,这里的内容都能给你提供可直接落地的代码和必须警惕的教训。

2. JWT深度解析:从结构到隐患

2.1 JWT究竟是什么?不止是三个点隔开的字符串

很多人对JWT的第一印象就是一个由点号分隔的三段式字符串,类似xxxxx.yyyyy.zzzzz。这没错,但理解其内部结构才是安全使用的基石。JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这里的“自包含”是关键:令牌本身包含了所有必要的用户声明信息,服务器无需再去查询数据库或会话存储来验证用户身份,这为实现无状态认证提供了可能。

一个标准的JWT由三部分组成,分别对应字符串中被点号分隔的三段:

  1. Header(头部):通常由两部分组成,令牌类型(即“JWT”)和所使用的签名算法,如HMAC SHA256或RSA。它会被Base64Url编码形成第一段。
  2. Payload(负载):包含所谓的“声明”。声明是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:注册声明(预定义,如iss签发者,exp过期时间)、公共声明和私有声明。它会被Base64Url编码形成第二段。
  3. Signature(签名):用于验证消息在传输过程中没有被篡改。签名是这样生成的:取编码后的Header、编码后的Payload,一个秘密密钥(使用HMAC算法时)或一个私钥(使用RSA算法时),然后用Header中指定的算法进行签名。

将这三部分用点号连接起来,就形成了一个完整的JWT。这里有一个极其重要的误区:Base64Url编码不等于加密!任何人都可以轻松地将JWT的前两段解码,读取其中的内容。因此,绝对不要在Payload中存放任何敏感信息,如用户密码、信用卡号等。签名的存在确保了负载的完整性,但无法保证其机密性。

2.2 核心工作流程与常见误区

一个典型的JWT认证流程如下:

  1. 用户使用凭证(如用户名密码)登录。
  2. 服务端验证凭证有效后,生成一个JWT(包含用户标识和必要声明)并返回给客户端。
  3. 客户端在后续请求中,通常在HTTP头的Authorization字段中以Bearer <token>的形式携带此JWT。
  4. 服务端收到请求,验证JWT的签名有效性、是否过期以及其他业务声明。
  5. 验证通过后,服务端处理请求。

这个过程看似简单,但暗藏玄机。最常见的误区之一是将JWT当作会话(Session)来用。JWT一旦签发,在过期之前,服务端理论上无法使其失效(除非维护一个很小的黑名单,但这违背了无状态的初衷)。这意味着如果你需要实现“立即下线”功能,仅靠标准的JWT会非常棘手。另一个误区是令牌过长。由于所有信息都包含在令牌里,如果存放了过多用户角色、权限等数据,会导致令牌体积膨胀,增加每次请求的传输开销。

注意:JWT的“无法废止”特性是其设计使然。在选择JWT前,务必评估你的业务是否真的需要无状态,以及“强制下线”是否是高频或关键需求。对于后台管理系统,传统的Session-Cookie方案可能更合适。

2.3 手撕一个JWT工具类:生成与解析

理论说再多,不如一行代码。下面是一个基于Javajjwt库的实用工具类。我强烈建议你在项目中封装这样一个工具类,统一令牌的生成、解析和验证逻辑。

import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component public class JwtTokenProvider { // 从配置文件中注入,绝对不要硬编码在代码里! @Value("${jwt.secret}") private String jwtSecret; @Value("${jwt.expiration}") private long jwtExpirationInMs; // 生成密钥。对于HMAC-SHA算法,密钥需要足够的强度。 private SecretKey getSigningKey() { // 注意:这里示例使用Keys类,实际生产环境密钥应从安全配置中获取 return Keys.hmacShaKeyFor(jwtSecret.getBytes()); } /** * 生成指定用户名的JWT令牌 * @param username 用户名 * @return JWT令牌字符串 */ public String generateToken(String username) { Map<String, Object> claims = new HashMap<>(); // 可以在此处添加自定义声明,如角色、权限等 // claims.put("role", "ADMIN"); Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); return Jwts.builder() .setClaims(claims) // 声明 .setSubject(username) // 主题,通常放用户名/用户ID .setIssuedAt(now) // 签发时间 .setExpiration(expiryDate) // 过期时间 .signWith(getSigningKey(), SignatureAlgorithm.HS256) // 签名算法和密钥 .compact(); } /** * 从令牌中解析用户名(主题) * @param token JWT令牌 * @return 用户名 */ public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } /** * 验证JWT令牌是否有效 * @param token JWT令牌 * @return 是否有效 */ public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token); return true; } catch (Exception ex) { // 这里可以细分异常类型,如过期、签名错误等,便于日志记录和问题排查 // log.error("JWT validation error: {}", ex.getMessage()); return false; } } }

实操心得

  • 密钥管理是命门jwt.secret必须足够复杂(建议使用安全的随机生成器生成,长度至少32字节),并且像保护数据库密码一样保护它。绝对不要提交到版本库。生产环境应考虑使用密钥管理服务。
  • 异常处理要细致validateToken方法中捕获了泛化的Exception,在生产中最好捕获JwtException的具体子类,如ExpiredJwtExceptionSignatureException等,以便在日志中明确区分是令牌过期还是被篡改,这对于安全监控和问题诊断至关重要。
  • 声明(Claims)的取舍:负载中不要塞入过多数据。通常sub(用户标识)和exp(过期时间)是必需的。如果需要角色信息,可以考虑放入,但要意识到这会使得更新用户角色后,旧令牌在过期前依然有效的问题。

3. Spring Security整合实战:从配置到定制

3.1 Spring Security核心概念扫盲

Spring Security是一个为基于Spring的应用程序提供声明式安全访问控制解决方案的框架。它的核心是一系列过滤器链(Filter Chain),请求会经过这个链条,依次进行安全检查。理解几个核心对象是配置的关键:

  • SecurityContextHolder:存储当前安全上下文(Security Context)的地方,其中包含当前用户的认证信息(Authentication对象)。
  • Authentication:代表一个认证请求或一个已认证的主体。它包含principal(主体,如用户名)、credentials(凭证,如密码)和authorities(权限集合)。
  • UserDetailsUserDetailsServiceUserDetails是Spring Security对用户核心信息的抽象接口。UserDetailsService只有一个方法loadUserByUsername,用于根据用户名加载用户信息到UserDetails对象。这是我们连接自己用户数据库的桥梁。
  • GrantedAuthority:代表授予主体的权限,如“ROLE_ADMIN”、“READ_PRIVILEGE”。它通常是字符串。

整合JWT后,我们的目标就是自定义一个过滤器,放在Spring Security的过滤器链中合适的位置,用来拦截请求,解析JWT,并构造一个Authentication对象放入SecurityContextHolder

3.2 构建JWT认证过滤器

这是整合的关键。我们需要创建一个过滤器,它继承自OncePerRequestFilter(确保每次请求只执行一次),并从HTTP头中提取JWT,进行验证,最后设置安全上下文。

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; private final UserDetailsService userDetailsService; public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) { this.tokenProvider = tokenProvider; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // 1. 从请求头中获取JWT String jwt = getJwtFromRequest(request); // 2. 验证令牌格式和有效性 if (jwt != null && tokenProvider.validateToken(jwt)) { // 3. 从JWT中解析用户名 String username = tokenProvider.getUsernameFromToken(jwt); // 4. 加载用户详情(注意:这里可能会查数据库) UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 5. 构建Authentication对象 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, // credentials通常为null,因为密码已验证过 userDetails.getAuthorities() // 用户的权限集合 ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 6. 将Authentication设置到SecurityContext中 SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { logger.error("Could not set user authentication in security context", ex); // 这里不要直接抛出异常导致请求失败,可以记录日志,让后续的过滤器处理(如返回401) } // 7. 继续执行过滤器链 filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); // 去掉"Bearer "前缀 } return null; } }

关键点解析

  • 第4步的权衡:这里调用了userDetailsService.loadUserByUsername,意味着每次携带JWT的请求都可能查询一次数据库。这似乎与JWT“无状态”的初衷相悖。是否查询数据库取决于你的设计:
    • 方案A(推荐用于高并发):将必要的权限信息也编码到JWT的Payload中。在过滤器中,直接从JWT解析出权限,构造Authentication对象,完全避免此次数据库查询。代价是权限更新后,旧令牌在过期前依然有效。
    • 方案B(权限实时性要求高):就像上面代码一样,每次都查询数据库。这保证了权限的实时性,但增加了数据库压力。可以通过缓存用户权限信息来缓解。
  • 异常处理:过滤器中捕获异常后,只是记录了日志,并没有中断请求。这是因为Spring Security链后续还有ExceptionTranslationFilter等过滤器来处理认证授权异常,最终会返回标准的401或403响应。直接抛出异常可能导致不友好的错误页面。

3.3 安全配置类详解

现在,我们需要通过一个配置类,将自定义的过滤器、密码编码器、访问规则等组装起来,注入到Spring Security的机制中。

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 禁用CSRF,因为使用JWT后,通常由前端在Header中携带令牌,不易受到CSRF攻击。 // 但如果你同时使用Cookie,则需要重新评估。 .csrf().disable() // 设置会话管理为无状态,因为我们使用JWT .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 配置请求授权规则 .authorizeHttpRequests(authz -> authz // 公开访问的端点,如登录、注册、Swagger文档等 .requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() // 拥有ADMIN角色的用户才能访问 /api/admin/** 下的资源 .requestMatchers("/api/admin/**").hasRole("ADMIN") // 其他所有请求都需要认证 .anyRequest().authenticated() ) // 在UsernamePasswordAuthenticationFilter之前添加我们的JWT过滤器 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { // 使用BCrypt强哈希加密密码 return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }

配置精讲

  • csrf().disable():这是一个重要且容易引起争议的配置。CSRF攻击依赖于浏览器自动携带的会话Cookie。在使用JWT且令牌通过HTTP头(而非Cookie)传递时,CSRF攻击的载体不存在,因此可以安全地禁用它。如果你的JWT也通过Cookie存储和发送,则绝对不能禁用CSRF防护。
  • sessionCreationPolicy(SessionCreationPolicy.STATELESS):明确告诉Spring Security不要创建和使用HttpSession。这是实现真正无状态REST API的关键。
  • addFilterBefore:将我们的JwtAuthenticationFilter添加到UsernamePasswordAuthenticationFilter之前。这样,请求会先经过我们的JWT解析过滤器,如果解析成功设置了认证信息,原生的用户名密码过滤器就不会再执行。
  • PasswordEncoder永远不要以明文存储密码!BCrypt是当前公认的安全的密码哈希算法,它会自动处理“盐值”,且计算速度可调,能有效抵御彩虹表攻击。

4. 典型安全问题与防御实战

整合完成并不意味着高枕无忧。错误的使用模式会引入严重漏洞。下面我们来剖析几个最常见的安全陷阱。

4.1 JWT自身的安全陷阱

1. 签名算法篡改攻击(“none”算法攻击)JWT规范支持一种名为“none”的算法,表示令牌未签名。如果服务器配置不当,在验证签名时,信任了Header里声明的算法,攻击者就可以将算法改为“none”,并去掉签名部分,从而伪造一个有效的JWT。

  • 防御:在验证JWT时,必须显式指定期望的签名算法,而不是从JWT头部读取。使用jjwt库时,应使用parserBuilder().setSigningKey(...).build(),库内部会进行校验。确保你的依赖库版本是最新的,旧版本可能存在此类漏洞。

2. 密钥强度不足与泄露如果使用HMAC(对称加密),密钥强度不够(如太短、太简单)会导致被暴力破解。密钥泄露更是灾难性的,意味着攻击者可以签发任意令牌。

  • 防御
    • 对称密钥长度至少256位。
    • 非对称加密(RSA)通常更安全,私钥妥善保管在服务器,公钥用于验证。
    • 密钥必须通过环境变量、配置中心或密钥管理服务获取,严禁硬编码。
    • 定期轮换密钥。轮换后,旧密钥在一定宽限期内仍可用于验证,但新签发的令牌必须使用新密钥。

3. 令牌泄露与撤销难题JWT一旦泄露,在过期前都无法直接作废。这是其最大缺点。

  • 防御策略
    • 设置较短的过期时间(如15-30分钟),配合刷新令牌(Refresh Token)机制。刷新令牌是一个长期有效(但可撤销)的令牌,专门用于获取新的访问令牌。访问令牌泄露的影响窗口较小。
    • 维护一个小的令牌黑名单。对于关键操作(如修改密码、退出登录),将对应的JWT唯一标识(如jti声明)加入一个短期缓存的黑名单。在验证JWT时,额外检查是否在黑名单中。这在一定程度上牺牲了无状态性,但增强了控制力。
    • 将令牌存储在前端的HttpOnly Cookie中,而非LocalStorage,可以防止XSS攻击直接窃取令牌。但需妥善处理CSRF问题。

4.2 Spring Security配置不当引发的漏洞

1. 权限绕过与路径匹配错误.requestMatchers("/api/admin/**").hasRole("ADMIN")这类配置中,如果路径匹配规则写得不严谨,可能导致权限绕过。例如,配置了/api/user/*需要认证,但/api/user/../public可能被绕过。

  • 防御:Spring Security默认会对路径进行规范化处理,但理解其匹配规则(Ant风格)很重要。对于REST API,使用antMatchersrequestMatchers时,要明确测试边界情况。更推荐使用基于方法的注解(如@PreAuthorize)进行细粒度控制。

2. 密码明文传输与弱编码即使后端用BCrypt存储密码,如果登录接口不使用HTTPS,密码在传输过程中就是明文。

  • 防御全站强制使用HTTPS。在Spring Boot中,可以轻松配置HTTP重定向到HTTPS。

3. 用户枚举漏洞在登录接口,无论是用户名不存在还是密码错误,如果返回的错误信息不同(如“用户名不存在” vs “密码错误”),攻击者就可以利用此差异枚举出系统中存在的有效用户名。

  • 防御:登录失败时,返回统一的、模糊的错误信息,例如“用户名或密码错误”。在日志中记录详细原因以便排查。

4.3 刷新令牌机制的安全实现

为了平衡安全与用户体验,刷新令牌机制是必须的。访问令牌(Access Token)短期有效,刷新令牌(Refresh Token)长期有效但可服务器端撤销。

// 登录成功时,返回两个令牌 public class JwtAuthenticationResponse { private String accessToken; private String refreshToken; private String tokenType = "Bearer"; // ... getters and setters } // 刷新令牌的端点 @PostMapping("/api/auth/refresh-token") public ResponseEntity<?> refreshToken(@Valid @RequestBody RefreshTokenRequest request) { String requestRefreshToken = request.getRefreshToken(); // 1. 验证刷新令牌是否有效且未过期(这里需要自己的验证逻辑,可能查数据库或缓存) // 例如,可以将刷新令牌的哈希值存储在数据库或缓存中,并关联用户和状态 if (!refreshTokenService.validateRefreshToken(requestRefreshToken)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid refresh token"); } // 2. 从刷新令牌中解析用户信息(刷新令牌本身也可以是一个JWT,但通常需要可撤销) String username = refreshTokenService.getUsernameFromRefreshToken(requestRefreshToken); // 3. 生成新的访问令牌 String newAccessToken = jwtTokenProvider.generateToken(username); // 4. (可选)生成新的刷新令牌并作废旧的,实现令牌轮换 String newRefreshToken = refreshTokenService.rotateRefreshToken(requestRefreshToken, username); return ResponseEntity.ok(new JwtAuthenticationResponse(newAccessToken, newRefreshToken)); }

刷新令牌安全要点

  • 刷新令牌必须存储在服务端可控制的地方(如数据库、Redis),并标记为已使用或直接删除,确保单次使用。
  • 刷新令牌的过期时间可以较长(如7天、30天),但必须提供管理员可手动撤销的机制。
  • 当使用刷新令牌获取新访问令牌时,应同时颁发一个新的刷新令牌,并使旧的失效(“轮换”机制)。这样即使某个刷新令牌泄露,攻击者也只能使用一次。

5. 生产环境进阶考量与排查技巧

5.1 分布式环境下的令牌验证

在微服务架构中,一个请求可能经过网关再路由到多个服务。你不可能在每个服务中都配置一遍JWT密钥。

  • 解决方案
    1. API网关统一认证:在网关层(如Spring Cloud Gateway)进行JWT的验证和解析,然后将解析出的用户信息(如用户名、权限)以HTTP头(如X-User-Id)的形式传递给下游服务。下游服务信任这个头信息。这种方式简单,但需要确保网关到下游服务的内网通信安全。
    2. 使用非对称加密(RSA):认证服务用私钥签发JWT,其他所有服务只用公钥来验证签名。公钥可以安全地分发给所有服务。这是更优雅和安全的方案。
    3. OAuth 2.0 / OIDC:对于更复杂的多系统单点登录(SSO)和第三方授权,直接采用OAuth 2.0协议和OpenID Connect标准。Spring Security提供了spring-security-oauth2-resource-server模块来支持,它可以自动从认证服务器获取JWK Set(包含公钥)来验证JWT。

5.2 监控、日志与审计

安全是可观测性的重要部分。

  • 监控令牌使用情况:记录令牌的签发、验证失败(尤其是签名错误、过期)、刷新操作。异常数量的失败验证可能预示着攻击。
  • 详细的认证日志:在过滤器和认证入口点记录关键事件,但注意不要记录令牌本身或敏感信息。可以记录用户标识、IP地址、时间戳和操作类型。
  • 审计日志:对于关键业务操作(如登录、修改密码、权限变更、重要数据访问),记录“谁在什么时候做了什么”。这不仅是安全要求,也是故障排查和取证的依据。Spring Security提供了良好的审计事件发布机制。

5.3 常见问题排查实录

问题1:登录成功,但后续接口返回403 Forbidden。

  • 排查步骤
    1. 检查JWT过滤器是否成功执行并设置了SecurityContext。在过滤器中加调试日志,打印解析出的用户名和权限。
    2. 检查UserDetailsService.loadUserByUsername返回的UserDetails对象中的权限(GrantedAuthority)是否正确。权限字符串需要以ROLE_前缀开头,如果你在配置中使用了.hasRole("ADMIN"),那么权限集合中应包含ROLE_ADMIN。如果使用.hasAuthority("ADMIN"),则权限集合中应包含ADMIN
    3. 检查安全配置中的路径匹配规则,确认请求的URL是否被正确匹配到了所需的权限上。

问题2:令牌明明未过期,却突然验证失败。

  • 可能原因
    1. 密钥被轮换:服务重启或密钥轮换后,用于签名的密钥和用于验证的密钥不一致。
    2. 时钟偏差:签发令牌的服务和验证令牌的服务系统时间不同步,导致验证时认为令牌已过期或未生效。确保服务器使用NTP服务同步时间。
    3. 令牌格式错误:检查传输过程中令牌是否被意外修改(如空格、换行符)。确保客户端正确地在Header中设置Authorization: Bearer <token>

问题3:性能瓶颈,数据库查询压力大。

  • 现象:每个API请求都触发一次UserDetailsService数据库查询。
  • 优化
    • 将用户权限信息缓存在Redis等内存数据库中,Key可以是user:perms:<userId>,设置合理的过期时间(如30分钟)。在过滤器中,先查缓存,缓存未命中再查库。
    • 如前所述,将非频繁变更的权限信息编码到JWT负载中,彻底避免此次查询。这需要设计好权限更新后令牌的失效策略。

问题4:如何安全地处理“记住我”功能?

  • 方案:不要简单地延长访问令牌的过期时间。应该使用刷新令牌机制。前端在用户选择“记住我”时,将刷新令牌持久化存储(如安全的HttpOnly Cookie)。访问令牌过期后,静默地用刷新令牌获取新访问令牌。同时,提供“退出”功能,该功能不仅清除前端令牌,更要通知后端使对应的刷新令牌失效。

安全是一个持续的过程,而非一劳永逸的配置。JWT和Spring Security提供了强大的工具,但最终的安全性取决于开发者对细节的理解和把控。每一次配置、每一行代码都需要带着安全的视角去审视。希望这篇长文能帮你建立起一道坚固而不失灵活的安全防线。在实际开发中,多测试、多Review、保持依赖库的更新,是守护系统安全的不二法门。

http://www.jsqmd.com/news/1074152/

相关文章:

  • MATLAB编程实战:通过Cody平台游戏化学习提升问题解决能力
  • 合成数据合规性验证:从隐私公平到效用的技术实现与挑战
  • VC6.0安装与汉化实战:解决路径、兼容性与IDE崩溃问题
  • SC140 DSP内核工作模式与异常处理机制深度解析
  • Windows下ComfyUI+GPT Image 2批量生图手术级部署指南
  • MCP协议:AI Agent业务落地的关键拼图与实战指南
  • CTF逆向工程实战:从easyre看三层加密的逆向分析与解密
  • Apple Pencil 终极指南:从选型到高阶技巧,释放 iPad 生产力
  • MATLAB增量测试实战:用Build Tool实现智能测试筛选,提升开发效率
  • Web渗透测试入门:从零基础到实战,掌握安全攻防核心技能
  • OpenAI官方开源Codex插件:本地大模型零依赖接入VS Code
  • MATLAB EXPO用户讲演实战指南:从环境配置到算法部署的避坑经验
  • Python实战:IP-guard加密Word文档的解密与数据恢复
  • Seedanc 2.0与Nano-Banana-2私有化视频生成部署实战
  • Ollama本地部署实战:大模型落地企业工作流的完整指南
  • OpenClaw对接飞书配置原理与生产级排错指南
  • MPC8540 TSEC嵌入式网络控制器:架构、接口与驱动开发实战详解
  • GLM-5与Claude Code协同重构开源项目实战
  • Kali Linux下DoS攻击原理与防御实战:从工具拆解到合规测试
  • Simulink项目结构化:从文件管理到工程化协作的完整指南
  • LangChain 生产级输出校验:用 Zod 构建数据契约防火墙
  • OpenViking:面向AI Agent的上下文文件系统范式
  • 深入解析PowerQUICC III缓存一致性与MMU:嵌入式系统开发的核心机制与实践
  • CVE-2015-1635漏洞深度解析:从HTTP.sys整数溢出到内核RCE
  • AVGen-Bench:音视频生成评估的新标准与技术解析
  • QREAM框架:解决RAG系统文档风格与问题场景错配的实践方案
  • Claude Code架构逆向解析:从SDK与UI行为推演AI编程Agent设计
  • insmod底层内存机制深度解析:从页表刷新到物理页分配
  • FreeRTOS链表源码list.c/list.h深度解析:实时调度的底层骨架
  • 数据可视化中“一图看全”功能:原理、实现与最佳实践