ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot 3 + Spring Security 6 + JWT 完整整合实践与安全指南

Spring Boot 3 + Spring Security 6 + JWT 完整整合实践与安全指南 在 Spring 项目里做登录认证Spring Security 和 JWT 基本是绕不开的组合。一个是 Java 生态里事实标准的权限框架一个是无状态 Token 的典型代表把它们拼在一起就成了目前后端接口鉴权最常见的方案。我最近正好在重构一个老项目的登录模块把原来的 Session 拦截器彻底换成了 Spring Boot 3 Spring Security 6 JWT中间踩了不少坑也把配置迁移、Token 续签、验证码登录这些流程重新捋了一遍。这篇文章不打算翻教科书式地讲理论就按实际整合过程中那些绕不过去的设计决策、代码片段和排错记录把整套认证机制拆开说清楚。适合正在做前后端分离项目、想把登录鉴权做干净点的人也适合被 Spring Security 配置绕晕的初学者照着我的思路走能少走不少弯路。1. 为什么选 Spring Security JWT认证机制的前世今生1.1 Session 的痛点和 JWT 的切入点很多人刚接触 Spring Security 时第一反应是“这东西配置太复杂了”。但如果你经历过老项目那套 Session 登录就会理解 Spring Security 真正解决的是什么问题。传统 Session 方案里登录成功后服务端把用户数据存进 HttpSession再通过 Cookie 把一个 JSESSIONID 丢给浏览器。单机部署没问题一旦要做集群、要做微服务Session 同步就成了麻烦事要么引入 Spring Session Redis要么做粘滞会话要么搞会话复制。而且前后端分离之后前端可能是一个 SPA 应用也可能同时对接 App 和 H5Cookie 在跨域场景下管理起来非常别扭。JWT 的思路是彻底反转服务端不保存会话状态把用户身份、权限、过期时间等关键信息编码进一个自包含的 Token 里客户端每次请求带上这个 Token服务端验签通过后直接信任里面的内容。这样一来服务端天然无状态水平扩容不需要关心 Session 落在哪台机器上跨域、多端也顺利得多。但无状态不等于安全JWT 一旦签发在有效期内是“泼出去的水”想主动失效只能靠黑名单或者等过期所以实际项目里通常不会只用纯 JWT还要配合刷新令牌、白名单、短过期时间等手段。1.2 Spring Security 在认证链路里到底做了什么Spring Security 的核心不是某一个具体功能而是一条过滤器链。请求进入应用后会依次经过一系列 Filter其中UsernamePasswordAuthenticationFilter负责处理表单登录BasicAuthenticationFilter处理 Basic AuthFilterSecurityInterceptor做最后的授权判断。我们自己接入 JWT就是在这条链上找一个合适的位置插入一个自定义过滤器把 Token 解析成Authentication塞进SecurityContextHolder后面的授权环节就能直接使用了。很多新手误以为 Spring Security 只能做 Session 登录其实它只是提供了默认的登录方式。只要理解了过滤器链机制JWT 认证就可以看作“换了一个取凭证的方式”原来是从 Session 里拿用户现在是解析请求头里的 Token。Spring Security 里的AuthenticationManager、UserDetailsService、PasswordEncoder这些组件完全可以复用不需要另起炉灶。这样一来你已经拥有的用户表、密码校验逻辑、权限模型都可以原封不动地接进来。1.3 JWT 三个部分的含义和签名关键JWT 长什么样它是由Header.Payload.Signature三段 Base64URL 字符串加.连接组成的。Header 里面通常声明了签名算法和 Token 类型比如{alg:HS256,typ:JWT}Payload 里放着sub用户标识、exp过期时间、iat签发时间、iss签发方、roles自定义角色字段等声明Signature 则是对前两段内容用指定算法和密钥算出来的签名用来防止内容被篡改。签名算法这里需要特别留意。HS256 是对称算法签名和验签用的是同一个密钥服务端要保存好密钥适合内部服务之间使用。RS256 是非对称算法私钥签名、公钥验签适合有多个服务需要独立验签的场景也方便把验签能力交给网关或第三方。生产环境如果条件允许我更推荐 RS256因为即使某个服务的公钥泄露攻击者也无法伪造 Token但 HS256 的密钥一旦泄露整个认证体系就会崩盘。这部分在后面安全总结里还会再展开。2. Spring Boot 3 下 Spring Security 配置迁移与 JWT 整合2.1 Spring Security 5 到 6 的迁移重点如果你是在 Spring Boot 2.x 时代接触过 Spring Security到了 Spring Boot 3 之后会发现很多配置类写法变了。最明显的是不再继承WebSecurityConfigurerAdapter而是直接声明一个SecurityFilterChain的 Bean。WebSecurityConfigurerAdapter在 5.7 版本就标记了废弃Spring Security 6 里干脆把它移除了如果你在网上找到的旧教程还在用extends WebSecurityConfigurerAdapter那个项目大概率跑不起来。其次是授权规则的写法从antMatchers()换成了requestMatchers()这不只是改个名字。Spring Security 6 强调使用更现代的路径匹配模式mvcMatchers也被整合进了requestMatchers。还有配置的格式化方式Spring Security 6 推荐使用 Lambda DSL比如http.csrf(csrf - csrf.disable())而不是早期的http.csrf().disable()。刚开始看这种风格会不习惯但它让每个配置项的作用域更清晰也避免了链式调用顺序导致的隐性 Bug。迁移的时候最容易忽略的是密码加密。很多老项目直接用{noop}前缀绕过密码加密这在 Spring Security 6 默认配置下会直接报错因为默认要求使用DelegatingPasswordEncoder。建议统一用 BCrypt在配置类里声明一个PasswordEncoderBean密码校验的事情交给它处理。如果老数据里有明文密码可以继续用{noop}临时兼容但一定要尽快迁移。2.2 依赖引入和项目结构整合 JWT 需要的依赖主要两部分Spring Security 本身和 JWT 工具库。Spring Security 在 Spring Boot 3 的 starter 里已经包含了JWT 库我比较常用的是 JJWT它的 API 设计相对清晰。这里给出一个完整的 Maven 依赖示例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependencyjjwt-api是编译期依赖jjwt-impl和jjwt-jackson是运行时实现。因为 JJWT 底层需要 JSON 解析器它会自动适配 Jackson所以把jjwt-jackson加进来就能直接使用Jwts.parserBuilder()这类 API。如果你用的是较新版本API 可能会变成Jwts.parser()这种新写法但核心逻辑是一样的注意看版本文档。项目结构上我习惯把认证相关的类单独放到一个security包下里面再分config、filter、utils、service几个小包。不要把这些类到处乱扔否则排查问题时你会发现自己都不记得 Token 生成逻辑在哪个类里。2.3 无状态安全配置类示例Spring Security 整合 JWT 的配置核心是关闭 Session 管理、关闭 CSRF、放行登录接口、所有其他请求需要认证。CSRF 要关的原因很简单JWT 通常放在请求头里不再依赖 CookieCSRF 攻击的最大前提就消失了而且无状态 API 使用 CSRF Token 非常不方便。下面是一个在 Spring Boot 3 中可用的配置类骨架Configuration EnableWebSecurity public class SecurityConfig { Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/auth/captcha).permitAll() .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() ) .exceptionHandling(ex - ex.authenticationEntryPoint(unauthorizedHandler())) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }这里有几个关键点。SessionCreationPolicy.STATELESS是必须的它告诉 Spring Security 不再创建 HttpSession彻底转成无状态模式。OPTIONS请求放行也很重要前端跨域调试时经常发预检请求如果不放行浏览器会报 CORS 错误或者 401。addFilterBefore把自定义 JWT 过滤器放到用户名密码过滤器之前因为我们的认证方式不是表单登录而是从 Token 里解析身份必须在过滤链进入授权环节之前完成身份注入。再说回authorizeHttpRequests。这个配置的核心逻辑是“先放行公开接口其余全部需要认证”。要注意requestMatchers的顺序Spring Security 会按声明顺序匹配一旦匹配上就终止判断所以把放行的接口写在前面anyRequest().authenticated()写在最后。3. JWT 认证全流程实操登录签发、请求校验、Token 续签3.1 用户实体与 UserDetailsService认证第一步当然是准备好用户来源。Spring Security 做认证时会通过UserDetailsService来加载用户信息。你不需要让自己的实体类实现UserDetails更常见的做法是自己维护一套用户表再写一个实现类把用户信息转换成 Spring Security 认识的UserDetails。推荐的做法是写一个UserDetailsServiceImpl注入自己的用户 Mapper通过用户名查询用户然后返回一个org.springframework.security.core.userdetails.User对象里面带上用户名、密码、角色和启用状态。注意密码的编码要和登录时传来的明文密码通过PasswordEncoder.matches()比较不要自己写明文比对逻辑。这里有一个容易被忽略的坑如果你的用户表里有状态字段比如冻结、删除一定要在UserDetails里把isEnabled()这些状态体现出来。否则用户被封号后Token 只要没过期他依然可以正常访问所有接口这个在前面的安全风险里我会专门强调。3.2 登录接口签发 JWT登录接口的核心流程是接收用户名、密码可能还有验证码→ 调用AuthenticationManager做认证 → 认证成功后根据用户信息生成 JWT 返回给前端。因为我们已经把 Spring Security 的认证机制留下来了所以最优雅的方式是注入AuthenticationManager调用它的authenticate()方法。为了能让它工作需要先创建AuthenticationProvider或者DaoAuthenticationProvider并设置UserDetailsService和PasswordEncoder。Spring Boot 3 里可以直接声明一个AuthenticationManagerBeanBean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { DaoAuthenticationProvider provider new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder); return new ProviderManager(provider); }登录成功后生成 Token 的代码我用一个 JwtService 类封装起来。它至少需要提供三个方法生成 Token、解析 Token、校验 Token 是否有效。简化版的生成逻辑如下public String generateToken(String username, Collection? extends GrantedAuthority authorities) { MapString, Object claims new HashMap(); claims.put(roles, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() accessTokenExpiration)) .signWith(SignatureAlgorithm.HS256, secretKeyBytes) .compact(); }secretKeyBytes来自配置长度不能太短HS256 要求至少 256 位32 个字节否则 JJWT 会直接抛异常。我一般把密钥放在application.yml里用环境变量覆盖绝对不要写死在代码里也不能用secret、123456这种默认值。至于过期时间根据业务场景设定通常访问令牌 15 分钟到 2 小时不等具体看你对安全性和体验的取舍。3.3 自定义 JWT 过滤器注入安全上下文客户端拿到 Token 后后续每次请求都要在请求头上带上Authorization: Bearer token。服务端需要一个过滤器来解析这个 Token并把用户身份放入SecurityContextHolder。这个过滤器是整套机制里最核心的部分。我自己实现的JwtAuthenticationFilter长这样Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authorization request.getHeader(Authorization); if (authorization ! null authorization.startsWith(Bearer )) { String token authorization.substring(7); try { String username jwtService.extractUsername(token); if (username ! null SecurityContextHolder.getContext().getAuthentication() null) { UserDetails userDetails userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(token, userDetails)) { UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } catch (Exception e) { logger.debug(JWT 解析失败: {}, e.getMessage()); } } chain.doFilter(request, response); } }有几个细节需要注意。第一OncePerRequestFilter保证一个请求只执行一次过滤逻辑避免转发时重复解析。第二解析失败时不要直接抛异常因为过滤器层抛出的异常很难被RestControllerAdvice捕获最好只记日志然后继续走过滤链让后面的AuthenticationEntryPoint统一返回 401。第三从 Token 中解出用户名之后一定要再去UserDetailsService查一次用户的最新状态这样才能在用户被禁用后立即阻止访问而不是单纯相信 Token 里的声明。3.4 Token 续签策略纯 JWT 最尴尬的问题就是过期。令牌有效期设短了用户一会儿就要重新登录体验差设长了Token 泄露后的风险窗口太大。业界标准解法是引入 Refresh Token签发一个短期 Access Token 和一个长期 Refresh TokenAccess Token 用来正常请求Refresh Token 专门用来换取新的 Access Token。具体的落地方案可以这样设计Access Token 有效期 15 分钟到 2 小时。Refresh Token 有效期 7 天到 30 天一般存数据库或者 Redis并关联用户 ID、设备标识。前端在 Access Token 过期后带着 Refresh Token 调用/api/auth/refresh接口。服务端校验 Refresh Token 存在且未使用然后签发新的 Access Token并顺带把 Refresh Token 轮换掉。如果 Refresh Token 不存在、过期或者被使用过两次直接判定异常要求用户重新登录。Refresh Token 存 Redis 的好处是能主动失效还能比较优雅地实现“踢人下线”。Redis 里的键可以设计成refresh_token:{userId}值是 Refresh Token 本身并设置与 Token 过期时间一致的 TTL。每次刷新时重新生成 Token 并更新 Redis。如果用户在别处登录可以把同一用户的旧 Refresh Token 全部删除实现单端登录。滑动过期是另一种思路只要用户在过期前的某个时间窗口内有操作就自动给他续一个新的 Access Token。这种方式适合内部系统但对攻击者来说Token 窃取后也能一直续期所以安全性不如 Refresh Token Redis 轮换。我的建议是面向公网的项目用 Refresh Token内部管理后台用滑动过期具体看风险承受能力。4. 细节打磨验证码、SPA 跨域与发包格式4.1 登录接口集成图形验证码登录接口最容易被打爆所以实际项目里往往要加图形验证码。验证码不复杂但跟 JWT 登录放在一起时要注意流程先后。我常用的做法是前端请求/api/auth/captcha后端生成一张图片验证码把正确答案存到 Redis键为captcha:{uuid}图片返回给前端的同时附带一个uuid。用户提交登录时把uuid和captcha一起放到请求体里。后端先校验验证码从 Redis 里取正确答案比对成功就删除这个键然后才走用户名密码认证。验证码比对失败直接返回业务错误码不要再继续认证。验证码本身可以不通过 Spring Security 做放在登录接口里手动校验就行。没有装 Redis 的小项目也可以把验证码内存缓存到ConcurrentHashMap但要注意加上过期时间否则键只会无限增长。另外验证码图片生成我建议用Hutool的验证码工具它内置了几种干扰样式简单省事。这里有个实操小技巧验证码不管比对成功还是失败用完立刻删除目的是防重放。否则同一个验证码可能被尝试多次等于验证码形同虚设。校验不通过时前端需要重新获取验证码所以登录接口的错误码要区分“验证码错误”和“密码错误”前端才能友好提示。4.2 前端 SPA 携带 Token 的正确姿势后端接口定好了Authorization: Bearer token前端就必须统一在每个请求上都带着这个 Header。直接在axios里每个请求手动加太容易漏我一般用拦截器统一处理axios.interceptors.request.use(config { const token localStorage.getItem(access_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });这里有一个我反复踩过的坑不要把所有 Token 都放在localStorage里。XSS 攻击时攻击者可以用脚本直接读取localStorageToken 就泄露了。如果是纯前端 SPA我比较推荐用内存变量保存 Access Token页面刷新后重新通过 Refresh Token 获取如果不想这么麻烦至少不要把 Refresh Token 长期放在localStorage可以把它放在 HttpOnly Cookie 里降低脚本读取风险。跨域方面后端需要配置 CORS允许前端的域名访问。Spring Security 的 Config 里可以通过http.cors(Customizer.withDefaults())开启再单独定义一个CorsConfigurationSourceBean。配置时注意把允许的方法、请求头写完整尤其要允许Authorization这个 HeaderallowedHeaders(*)最省事但不够严谨项目里有安全要求的话还是按需配置。4.3 401 响应与自定义异常处理默认的 Spring Security 在没有认证信息时会返回一个空响应或者重定向到登录页这对前后端分离项目来说是完全不友好的。前端拿到一个 HTML 重定向页面只会一脸懵。我们需要自定义AuthenticationEntryPoint让它返回 JSON 格式的 401 响应Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未认证或Token已过期\}); } }同样的道理权限不足403时也需要一个AccessDeniedHandler返回统一格式的错误信息。这个环节的值不只是用户体验对前端排查问题也有意义看到 401 就知道要去刷新 Token看到 403 就知道用户没有权限而不是被一些莫名其妙的 302 搞晕。在exceptionHandling()里把这两个 Handler 配置好再结合过滤器里对解析失败的静默处理整条认证链的错误处理就闭环了。以后遇到未登录、过期、无权限三种情况前端都可以根据状态码走对应的登录跳转或提示逻辑而不必解析 HTML 内容。5. JWT 安全风险总结与避坑清单5.1 常见漏洞盘点JWT 的漏洞网上总结过很多我自己也踩过其中几个这里挑影响最大的说。弱密钥是最常见的。很多教程为了演示方便直接写secret、123456、my_secret_key这种字符串。攻击者拿到一个 Token用字典就能爆破出签名密钥然后随便伪造任意用户的 Token。这就是所谓“默认密钥认证绕过”的原理不只是某个组件有这个问题任何使用 JWT 的系统都可能中招。我之前看到过一个安全通报里提到某些配置中心因为内置默认 JWT 密钥攻击者可以直接伪造管理员身份绕过高权限校验。这个教训完全可以迁移到自己的项目里密钥必须随机生成、足够长、并保存在环境变量或密钥管理服务中。算法混淆攻击是 JWT 历史上比较经典的一个漏洞。攻击者把 Header 里的alg改成none同时删掉签名部分有些简陋的验签逻辑看到none就直接放行等于没有认证。另一些攻击者会把HS256换成RS256如果服务端误以为自己是验签方其实拿的是签名公钥就会用公钥当对称密钥去验证签名导致攻击者也能算出有效签名。解决方法是解析 Token 时固定算法不要完全相信 Header 里的alg声明。JJWT 在这个问题上处理得比较好它默认就禁止alg:none而且要求签名密钥的强度匹配算法能挡住大部分简单攻击。kid头注入也值得警惕。kid是用来指定密钥 ID 的有些实现会把它拼进文件路径去读取密钥文件攻击者利用kid../../etc/passwd之类的路径穿越就能读取服务器文件。后来很多库限制了这个行为但如果你经常自己拼路径一定记得对kid做白名单校验而不是直接拼接。过期时间缺失或过长是业务层问题。有的人写 JWT 签发时忘了setExpiration结果 Token 永远不过期有的人图方便把过期时间设成一年甚至十年。这些都属于重大安全隐患建议在工具类里强制校验Token 里没有exp就视为无效。5.2 从默认密钥事件看密钥管理前面提到过 Nacos 默认密钥身份认证绕过漏洞攻击者利用默认 JWT 密钥构造 Token就能绕过认证拿到系统权限。这件事给所有开发者的提醒是任何框架、中间件、自研服务只要涉及签名、加密、令牌生成密钥都不能用默认值也不能用容易被猜到的值更不能在代码里硬编码。对于 Spring Security JWT 项目密钥管理的几条红线我建议直接写进团队的代码规范密钥长度至少 256 位用SecureRandom生成随机字节不要自己敲键盘乱打。密钥放在application.yml中但用${JWT_SECRET}占位符由部署环境注入真实值。不同环境的密钥必须不同测试环境和生产环境用一套密钥是事故的温床。定期轮换密钥轮换时考虑 Token 兼容期一般先让新旧密钥同时存在一段时间。做到这几点你就能避免大部分和密钥相关的低级漏洞。毕竟安全体系的强度往往取决于最容易攻破的那个环节。5.3 生产加固建议除了密钥以外还有几个加固点值得做。第一个是签名算法优先用 RS256。非对称算法下即使公钥被前端或其他服务拿到也无法伪造 Token。如果你的架构里有很多内部服务需要独立验证 TokenRS256 更为合适如果只有单一后端HS256 也能用但必须把密钥保护好。第二个是 Access Token 有效期尽量短。短 Token 能把数据泄露的时间窗口压缩到最小频繁刷新带来的体验问题由 Refresh Token 解决。我这里说的短不是“几分钟”而是结合业务场景比如 15 分钟到 30 分钟都属于合理范围。第三个是主动失效机制。刚才说 JWT 无状态但如果用户修改密码、被踢下线、管理员禁用了账号你肯定希望 Token 立即失效。方案有两个一是把 Token 的jti唯一标识存到 Redis校验时看黑名单二是每个请求都去查一次用户状态并校验 Token 版本号。前者性能好后者更彻底。我的做法是过滤器里加载用户状态同时把登录时分配的自定义版本号放进 Token用户密码修改后版本号递增旧 Token 自然失效。第四个是对 JWT 内容做瘦身。不要往 Payload 里塞大量业务数据Token 越大每次请求的开销越大也越容易被各种日志系统完整打印出来泄漏到第三方。只保留必须的用户标识、角色、过期时间用户其他资料需要时再查库。6. 常见问题与排查技巧实录6.1 “我放行了 /api/auth/login 但还是 401”这个场景我在工作中遇到过很多次原因五花八门。最常见的是请求实际上被 CORS 预检堵住了前端发跨域 POST 请求浏览器会先发一个 OPTIONS 预检如果你的 Security 配置没有放行 OPTIONS预检请求就会因为未认证被拦截表现出来就是“登录接口都调不通”。另外要注意requestMatchers的路径匹配规则。Spring Security 6 里/api/auth/login不会自动匹配到/api/auth/login/带斜杠的形式如果前端实际请求路径不同放行规则就不生效。排查时可以先在过滤器里打印请求 URI确认实际路径。第三种情况是自定义过滤器抛了异常但异常被吞掉了最后走到了认证入口。这种问题很难一次性定位我通常会在 JWT 过滤器里加调试日志分别打印“Header 存在”“Token 解析成功”“用户合法”几个关键节点的结果很快就能看到卡在哪一步。6.2 过滤器里解析 Token 失败导致请求中断有人会在过滤器解析失败时直接往响应里写 401然后return不继续执行chain.doFilter()。从效果看似乎是正确响应了但问题在于你在过滤器里写了响应体Spring Security 后续的异常处理机制就不会生效了导致返回的 JSON 格式和你全局定义的 401 结构不一致前端解析逻辑就要写两套。更合理的做法是过滤器里只做“能解析就继续不能解析就保持匿名状态”不写响应把最终决定权交给AuthenticationEntryPoint。除非你有非常特殊的业务需求否则不要在过滤器层面直接结束请求。如果确实需要在某些条件下直接拦截例如 Token 黑名单命中那也建议抛一个自定义异常再在EntryPoint里统一处理。6.3 Token 过期返回信息不规范并发刷新时间偏移前端经常遇到一个问题Token 刚好过期的那一瞬间多个请求同时返回 401于是前端同时发起多个刷新请求后端 Redis 里的 Refresh Token 被并发轮换最后只有一个请求能成功其他请求失败用户依然要重新登录。解决方法是前端做一个刷新锁第一次 401 时启动刷新后续请求在刷新过程中等待同一个 Promise刷新完成后再重放原请求而不是各自为政。另一个隐蔽问题是服务器和客户端时间不一致。JWT 的exp是按服务器时间校验的如果部署环境有容器时钟漂移可能出现明明没过期却判定过期的情况。JJWT 提供了setAllowedClockSkewSeconds()允许你设置几秒的时钟偏移容忍值一般设 30 到 60 秒足够。最后提醒一个很多人忽略的小点Token 过期不等于用户必须重新登录。配合 Refresh Token应该做到用户无感刷新。如果你发现用户每 15 分钟就收到登录失效提示那大概率是前端刷新逻辑没做好而不是后端过期时间设太短。最后分享一个我自己的习惯JWT 验证不能只依赖过滤器里的一次解析我通常还会在关键操作里再校验一次用户状态比如是否被封禁因为 JWT 在签发后是没有状态的你要让它失效只能靠黑名单或者缩短过期时间。这个设计一开始没想清楚上线之后吃过暗亏。如果你正在做类似的项目建议把“签发-校验-续签-失效”这条链路在纸上先画一遍再动手写代码能省掉很多返工。
返回列表