后端做了几年JWT 身份认证算是每次都会被拉出来遛一遍的核心模块。尤其是给前端 SPA 项目做登录时Token 怎么签发、怎么验签、过期怎么续、漏洞怎么防每一环都直接决定了接口的安全性和用户体验的顺滑度。这篇文章是我基于 JJWT 库从零搭建的一整套 JWT 认证流程总结包含依赖配置、版本选择、工具类封装、登录接口落地、拦截器接入、Token 续签方案以及后来踩过的各种安全漏洞和异常坑。适合正在做登录鉴权模块的后端同学也适合刚接手认证系统、想一次性搞懂 JWT 全链路的新手。1. 认证方案选型JWT 到底解决了什么问题1.1 Session 认证的典型痛点先说为什么放着好好的 Session-Cookie 不用非要去折腾 JWT。我在前两年维护过一个老项目登录态全放在服务端 Session 里所有接口靠 Cookie 里的 JSESSIONID 找对应 Session。单体部署的时候没什么大问题后来服务一拆问题全冒出来了Session 要么存 Redis要么搞粘滞会话否则用户第一次请求打到 A 实例、第二次请求打到 B 实例就直接掉登录前后端分离之后跨域请求光配置 Cookie 的 SameSite、Domain 就够折腾一圈还有 CSRF 防护又要给请求额外塞 Token。这几个问题单独看都不致命但合在一起就逼着团队去换一种更契合分布式场景的认证方式。JWT 的优势就是无状态。服务器不保存会话登录成功后签一个 Token 发给客户端客户端每次请求把 Token 放在 Header 里带过来服务端只需要验签和检查过期时间就能确认“这个人是谁”。因为不需要查存储接口天然可以横向扩容任意一台节点都能独立完成校验。对于现在这种微服务、网关、多端复用接口的项目结构来说JWT 几乎是标准答案。1.2 JWT 的数据结构与签名原理JWT 全称是 JSON Web Token长得像一个用点号隔开的三段字符串格式长这样eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIiwiZXhwIjoxNzAwMDAwMDAwfQ.signature第一段是 Header声明签名算法比如{alg:HS256,typ:JWT}第二段是 Payload存放业务声明比如用户 ID、角色、过期时间第三段是签名由签名算法(header . payload, 密钥)算出来。三段分别做 Base64URL 编码然后拼在一起。这里我要特别强调一点Payload 里的内容是明文任何人拿到 Token 都能解出来看。它只保证“内容没被篡改”不保证“内容不可见”。所以密码、身份证号这类敏感信息绝对不能塞进 Token我在后面安全章节还会再提。签名的作用其实很像快递封口上的防拆贴投递过程中只要有人动过包裹收件人一眼就能看出封口坏了Token 里的内容只要被改一个字符服务端重新计算签名时就会发现对不上直接判定无效。1.3 为什么选 JJWT 而不是手写或自研有人可能会说JWT 不就是 Base64URL 加 HMAC 加时间判断吗自己写也没几行代码。我最初也是这么想的但真写起来才发现坑不少Base64URL 的字符集要和标准一致HMAC 签名要处理字节数组和字符串之间的编码校验异常要区分过期、签名不匹配、格式损坏还有未来的算法扩展和密钥管理。把时间花在这些造轮子上不如直接用现成、成熟的库。Java 生态里主流的 JWT 库有两个JJWTio.jsonwebtoken和 Auth0 的 java-jwt。我选型时对比过两者基础功能差不多但 JJWT 的 API 语义更接近 JWT 规范本身对 JWE、密钥构建、解析异常的分类也更细致。后来在项目里还用到它提供的Keys和Decoders工具类确实省了不少事。所以本文所有代码都基于 JJWT 0.12.x 这套新 API 来写。2. 依赖配置与初始化版本坑和密钥设计2.1 Maven 坐标与 Gradle 坐标别再用老掉牙的 0.9.1先上一段我项目里实际使用的 Maven 依赖。JJWT 从 0.10 开始分成jjwt-api、jjwt-impl、jjwt-jackson三个模块其中impl和jackson是运行时实现和 JSON 序列化实现只在runtime作用域里引入即可dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.12.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.12.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.12.5/version scoperuntime/scope /dependency如果用 Gradle对应是implementation io.jsonwebtoken:jjwt-api:0.12.5 runtimeOnly io.jsonwebtoken:jjwt-impl:0.12.5 runtimeOnly io.jsonwebtoken:jjwt-jackson:0.12.5版本选择这里值得单独说几句。网上大量教程用的是 0.9.1写法是setSubject、setExpiration、signWith(SignatureAlgorithm.HS256, key)。这套 API 太老了新项目我不推荐再碰因为 0.10 之后内部结构重构过很多老方法已经被标记废弃甚至移除。我实际迁移过一次老项目把 0.9.1 升到 0.12.x 时所有调用点几乎都要重写一遍。如果新项目现在入场直接拥抱 0.11 或 0.12 系列更好。版本差异最典型的地方在解析端我把三种版本的写法整理成了表格方便你排查代码时对照版本典型解析方式说明0.9.1Jwts.parser().setSigningKey(key).parseClaimsJws(token)老到不能再老建议尽快升级0.11.5Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)稳定网上资料也多可用0.12.xJwts.parser().verifyWith(key).build().parseSignedClaims(token)新 API本文采用另一个常见问题是依赖冲突。有些老项目里 Shiro、Spring Security 或第三方 SDK 会传递依赖进一个很老的 JJWT 版本运行时出现NoSuchMethodError大概率就是两个版本混用了。解决思路是用 Maven 的dependencyManagement统一固定版本或者跑一遍mvn dependency:tree把冲突链条揪出来。2.2 密钥配置HS256 密钥长度不是随便写个字符串就行密钥是 JWT 安全的基石配置上不能偷懒。我的做法是把密钥和相关参数放在application.yml里通过外部环境变量覆盖而不是写死在代码里jwt: secret: R2Vsb2NhdGVEZXZlbG9wTWVudG9yU3VwZXJLZXlGb3JIVFMyNTZUb2tlbjIwMjQ expire-minutes: 30 refresh-expire-days: 7 issuer: my-spa-app配置里的secret是一段 Base64 编码后的字节串HS256 算法要求密钥字节长度至少 32 字节256 位。很多新手直接配一个123456或secret当密钥一旦使用新版 JJWT启动或者签发 Token 时会直接抛WeakKeyException提示 key 长度不够。这里补充一下原因HS256 是 HMAC-SHA256它内部会把密钥分成块来做哈希迭代如果密钥太短安全性会显著下降。JJWT 在设计上直接用异常把弱密钥挡在门外这个设计很合理不要用suppress或换算法的方式绕过它。生成一个合格密钥的最简单办法是取一段 32 字节以上的随机字节流再用 Base64 编码千万别用短字符串硬凑。2.3 配置读取与密钥对象构建配置文件里接到的是一串文本代码里要用 JJWT 的Decoders和Keys把它转成SecretKey对象。下面这段代码是从配置中解析密钥的常用写法Component public class JwtProperties { Value(${jwt.secret}) private String secret; Value(${jwt.expire-minutes}) private long expireMinutes; Value(${jwt.refresh-expire-days}) private long refreshExpireDays; // getters ... }密钥对象本身由Keys.hmacShaKeyFor构建这个方法会顺手校验字节长度长度不够或格式不对直接抛异常等于给配置加了一层启动期保护。我习惯把密钥构建放在一个只初始化一次的组件里避免每个请求都重复解码一遍 Base64白白浪费性能Component public class SecretKeyProvider { private final SecretKey key; public SecretKeyProvider(JwtProperties props) { byte[] keyBytes Decoders.BASE64.decode(props.getSecret()); this.key Keys.hmacShaKeyFor(keyBytes); } public SecretKey getKey() { return key; } }3. 工具封装JwtService 设计与签名校验的细节3.1 签发 Token主体、自定义声明、过期时间怎么填工具封装这一节我建议不要写一个全静态方法的JwtUtil而是封装成一个 Spring 管理的JwtService组件。理由是密钥、过期时间这些配置需要注入静态类要么把配置写死要么每次自己读最后很容易变成到处static乱飞的状态。签发 Token 的核心方法如下基于 JJWT 0.12.x 的新 APIService public class JwtService { private final SecretKeyProvider keyProvider; private final JwtProperties props; public String createAccessToken(Long userId, String username, ListString roles) { long now System.currentTimeMillis(); Date issuedAt new Date(now); Date expiresAt new Date(now props.getExpireMinutes() * 60 * 1000); return Jwts.builder() .issuer(props.getIssuer()) .subject(username) .claim(userId, userId) .claim(roles, roles) .issuedAt(issuedAt) .expiration(expiresAt) .signWith(keyProvider.getKey(), Jwts.SIG.HS256) .compact(); } }说明几个我自己觉得值得注意的点subject我用来存用户名或用户唯一标识标准注册声明里的sub字段就是干这个的不要为了省事把所有信息都堆在自定义claim里。claim(userId, userId)是自定义声明存用户 ID 方便后续查库或拼权限这里只放“非敏感但业务需要”的字段。issuedAt和expiration必须带上。如果签发时忘了设置过期时间处理方在解析时再想控制有效期就非常被动。.signWith(key, Jwts.SIG.HS256)里的算法要和密钥匹配HS256 就是对称密钥如果项目要求非对称签名可以用 RS256密钥对则用KeyPairGenerator生成这属于另一个话题这里不展开。3.2 解析与校验verifyWith 和异常捕获有签发就有解析。0.12.x 的解析器 API 和 0.11 有区别核心是verifyWith替代了setSigningKeyparseSignedClaims替代了parseClaimsJws。解析方法我一般这样写public Claims parseToken(String token) { return Jwts.parser() .verifyWith(keyProvider.getKey()) .requireIssuer(props.getIssuer()) .build() .parseSignedClaims(token) .getPayload(); }这里多做了一个requireIssuer校验用处是防止别人拿另一个服务签发的 Token 来访问当前服务。虽然密钥不同大概率验签不过但多个服务共用同一套密钥体系时这个校验还是能多一层保险。解析过程中最常见的四类异常我直接列出来方便你对照异常类型触发场景业务上怎么处理ExpiredJwtExceptionToken 已过期返回 401提示登录态失效触发续签或重新登录SignatureException签名不匹配Token 被篡改或密钥不对返回 401并且建议记录日志疑似恶意请求MalformedJwtExceptionToken 格式不是三段式返回 401多半是前端拼错了UnsupportedJwtException加密类型或算法不受支持返回 401检查签名算法配置我在真实项目里见过一种很蠢的写法在 Controller 里try-catch每个方法都解析一遍 Token重复代码一堆。正确做法是在全局异常处理器集中处理这几种异常比如 Spring 里继承ResponseEntityExceptionHandler把ExpiredJwtException映射到 401把SignatureException也映射到 401然后统一返回一段 JSON 给前端。3.3 别把 Claims 裸传给业务层再封装一层登录用户上下文Claims 本质是个 Map业务层如果到处接收Claims会带来很强耦合。我的做法是定义了一个LoginUser对象把 Token 里解析出来的关键信息统一转成这一份“当前登录用户上下文”public class LoginUser { private Long userId; private String username; private ListString roles; private Date expireAt; // getters/setters/构造器省略 }然后在接口层做一次转换比如jwtService.parseToken(token)拿到 Claims 之后调用一个convertToLoginUser(claims)方法。这样做的好处是如果后续要从 Redis 或数据库补充用户信息只需改这一个转换方法业务层永远不关心 Token 长什么样只认LoginUser对象。4. 登录应用实战从验证码校验到请求拦截4.1 登录接口完整实现验证码、密码校验、Token 返回很多 SPA 项目的登录页都会带图形验证码尤其是连续输错密码之后验证码几乎是必备步骤。这里我先说验证码的设计生成一个随机码存在 Redis 里设置 5 分钟过期同时返回给前端一张 Base64 图片登录请求带上codeId和用户输入的code。服务端先校验验证码再校验用户名密码最后才签发 Token。核心逻辑可以拆成这样1. 根据 codeId 从 Redis 取验证码 2. 判断用户输入是否一致不一致直接返回“验证码错误” 3. 根据 username 查询用户 4. 用 BCrypt 校验密码 5. 校验通过 - 创建 accessToken refreshToken - 返回给前端登录接口的 Controller 代码并不复杂PostMapping(/api/auth/login) public ResponseEntityLoginResponse login(RequestBody Valid LoginRequest request) { // 1. 图形验证码校验 captchaService.verify(request.getCodeId(), request.getCode()); // 2. 查询并校验用户 User user userService.findByUsername(request.getUsername()); if (user null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new AuthException(用户名或密码错误); } // 3. 生成 Token String accessToken jwtService.createAccessToken(user.getId(), user.getUsername(), user.getRoles()); String refreshToken jwtService.createRefreshToken(user.getId(), user.getUsername()); return ResponseEntity.ok(new LoginResponse(accessToken, refreshToken, jwtService.getAccessTokenExpireSeconds(), convertToUserVO(user))); }这里要提醒一个问题验证码校验一定要在密码校验之前做或者至少两者并行、只要有一个错误就拒绝。顺序反过来的话攻击者可以拿脚本不断尝试用户名和密码直到最后一步才被验证码挡住验证码就形同虚设了。验证码更严谨的做法是校验通过后立即删除 Redis 中的记录保证一次一用防止重放。BCrypt 校验这里多说一句不要用 MD5、SHA 这类快哈希直接存密码数据库一旦泄露用户密码很快会被撞库工具还原。BCryptPasswordEncoder自带盐和慢哈希设计是目前后端密码存储比较稳妥的默认选项。4.2 请求鉴权拦截器统一拿 Token、统一抛 401登录接口之外的所有接口都要校验身份。项目里没有重度使用 Spring Security 的情况下我用HandlerInterceptor实现了一套轻量鉴权只用三块核心代码拦截器、注册配置、全局异常映射。先看拦截器Component public class AuthInterceptor implements HandlerInterceptor { private final JwtService jwtService; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、验证码等白名单路径 if (request.getRequestURI().startsWith(/api/auth/)) { return true; } String header request.getHeader(Authorization); if (header null || !header.startsWith(Bearer )) { throw new AuthException(未携带 Token); } try { Claims claims jwtService.parseToken(header.substring(7)); LoginUser loginUser jwtService.convertToLoginUser(claims); request.setAttribute(loginUser, loginUser); return true; } catch (ExpiredJwtException e) { throw new AuthException(登录已过期); } catch (SignatureException e) { throw new AuthException(Token 不合法); } } }拦截器里有一个容易漏的细节白名单路径必须显式放行。登录接口、验证码获取接口、静态资源、CORS 预检请求OPTIONS这几类如果不放行就会出现“自己在这个系统里还没登录结果连登录接口都被拦截”的哭笑不得的场景。CORS 的 OPTIONS 预检请求尤其坑我见过不下三次线上接口跨域报错最后查出来就是拦截器把 OPTIONS 拦掉了。注册拦截器和路径配置也很直接Configuration public class WebConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/captcha/**); } }控制器里取当前登录用户时就直接从 Request 属性拿loginUser不用再二次解析 Token。比如GetMapping(/api/user/profile) public UserVO getUserProfile(HttpServletRequest request) { LoginUser loginUser (LoginUser) request.getAttribute(loginUser); return userService.getProfile(loginUser.getUserId()); }这套方案的好处是足够透明不走 Spring Security 那套严格的过滤器链也可以避免复杂配置带来的学习成本。如果你的项目本身已经接了 Spring Security我更推荐把校验逻辑放在OncePerRequestFilter或SecurityFilter里原理完全一致只是容器不同。4.3 前端 SPA 的 Token 存取与请求携带后端接口完成后前端接入同样有讲究。登录成功后前端拿到accessToken和refreshToken在 axios 请求拦截器里把 Token 塞进请求头axios.interceptors.request.use(config { const token localStorage.getItem(access_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });这里要提一个安全权衡Token 存localStorage还是内存业界讨论很多。localStorage的优点是刷新页面 Token 不丢实现方便但它对 XSS 攻击就像是钱包直接放在客厅茶几上只要脚本能执行Token 就可能被偷走。更稳妥的做法是短期 Token 放内存刷新页面时用 refreshToken 重新换或者干脆用httpOnlyCookie 存 Token让 JS 完全没有读取能力。我在 SPA 项目里更倾向于前者因为后端接口本身已经设计成无状态用“内存保存 刷新拉取”模式能有效缩小 XSS 攻击面。响应层面前端拦截器还要处理 401 的情况。如果后端返回 401说明 Access Token 过期需要自动调刷新接口换新 Token然后重放刚才失败的请求。这个逻辑放在统一响应拦截器里用户基本无感知这就是所谓的“无感刷新”。5. Token 续签方案滑动续签与双 Token 的取舍5.1 Access Token 过期时间不能无限拉长工作中我经常看到有人把 Token 过期时间设置成一小时、一天甚至一周理由是“省得用户老要重新登录”。这个思路可以理解但不安全。Token 一旦签发在过期之前服务端无法主动让它失效除非额外接黑名单或 Redis 存储。过期时间越长Token 泄露后的有效攻击窗口就越大。所以业界普遍的做法是Access Token 短命15 分钟到 2 小时Refresh Token 长命几天到一两周用双 Token 机制兼顾安全和体验。5.2 双 Token 机制的落地双 Token 的逻辑很简单Access Token 用来访问资源短期Refresh Token 专门用来换新的 Access Token长期。前端发现 Access Token 即将过期或已经过期时拿 Refresh Token 请求一个刷新接口PostMapping(/api/auth/refresh) public ResponseEntityTokenResponse refresh(RequestBody RefreshRequest request) { String refreshToken request.getRefreshToken(); // 校验 Refresh Token 本身是合法且未过期的 Claims claims jwtService.parseRefreshToken(refreshToken); Long userId claims.get(userId, Long.class); // 可选比对 Redis 中保存的 refreshToken jti防止多端互相顶替 if (!refreshTokenStore.isValid(claims.get(jti, String.class), userId)) { throw new AuthException(Refresh Token 已失效); } // 旧 Refresh Token 立即作废返回新 Token 对实现轮换 refreshTokenStore.invalidate(claims.get(jti, String.class)); User user userService.findById(userId); String newAccessToken jwtService.createAccessToken(user.getId(), user.getUsername(), user.getRoles()); String newRefreshToken jwtService.createRefreshToken(user.getId(), user.getUsername()); return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken)); }这里有个重要细节Refresh Token 用一次就换一个新的。如果攻击者盗用了一个 Refresh Token而用户正常刷新后旧的 Refresh Token 没有失效攻击者就可以一直使用泄露的 Token一旦实现了轮换用户每次刷新都会作废上一次的 Refresh Token攻击者的令牌会很快失效。我把 Refresh Token 的jtiJWT ID也存了一份在 Redis 里支持主动吊销和判断是否已被轮换过这样即便用户发现问题也可以在后端一键踢下线。5.3 滑动续签的简单变体如果项目规模不大不想上双 Token那至少可以做“滑动续签”。思路是每次请求解析 Token 时发现剩余有效期已经低于某个阈值比如 5 分钟就顺手签发一个新 Token通过响应头X-New-Access-Token返回给前端前端判断到有该响应头就替换本地 Token。实现上就是在拦截器里加几行逻辑long remainingMillis claims.getExpiration().getTime() - System.currentTimeMillis(); if (remainingMillis 5 * 60 * 1000L) { String newToken jwtService.createAccessToken(loginUser.getUserId(), loginUser.getUsername(), loginUser.getRoles()); response.setHeader(X-New-Access-Token, newToken); }这个方案的优点是代码量小跟现有单 Token 体系兼容缺点是没有 Refresh Token 的“长周期凭证”Access Token 过期时间设置非常短时还是需要用户重新登录。所以最终取舍看你的业务复杂度纯粹的内部管理系统用滑动续签足够面向消费者的 C 端应用我还是建议上双 Token。6. 常见问题与安全漏洞排查6.1 JJWT 高频异常排查速查表把我在项目里遇到过的问题整理成一张表直接对着排查就行异常/表现根本原因解决办法WeakKeyExceptionHS256 密钥不足 32 字节重新生成随机密钥配置改为 Base64 串ExpiredJwtExceptionAccess Token 过期走刷新接口或引导重新登录SignatureExceptionToken 被篡改或签发/校验密钥不一致检查各环境配置确认密钥对象相同MalformedJwtExceptionToken 不是标准三段式检查前端是否截断字符串检查请求头拼写NoSuchMethodErrorJJWT 版本混乱用mvn dependency:tree排查传递依赖统一版本偶发 401重试又正常多环境配置不同签发和校验用了不同密钥对比配置文件统一环境变量6.2 安全漏洞清单与防护建议JWT 领域流传的那些知名漏洞我在安全评估中也见过不少整理成下面这份防护清单每一条都是真实遇到过的案例或者行业里公开过的教训。算法混淆攻击Alg Confusion攻击者把 Token 头部的alg改成none或者把RS256改成HS256试图骗服务器“这是非对称算法你用公钥就能验签”。JJWT 这类库默认会拒绝none算法但如果你是自己手写校验或者库版本过于老旧就可能掉坑。防护上做到两点签发和解析都显式指定允许的算法集合绝不相信 Header 里声明的算法一律以服务端配置为准。密钥硬编码与弱密钥很多项目把 JWT 密钥直接写进代码或者配置成jwt.secret123456。一旦代码仓库泄露所有 Token 都能被伪造比数据库泄露还严重。密钥必须放环境变量或密钥管理服务长度按算法要求生成定期轮换。轮换时要给新旧密钥一个重叠期旧 Token 在过期前仍然能验签通过避免一把密钥切换就大批量踢用户下线。Payload 放敏感信息我没少在别人系统里看到 JWT Payload 里直接放明文手机号甚至地址。记住JWT 的 Payload 是 Base64 明文任何人都能解码它只有防篡改能力没有防泄露能力。凡是涉及隐私的数据要么不写进 Token要么写成用户 ID 这类标识符等后端真正需要时再查库。过期时间缺失或设置过长签发 Token 时不设置exp等于给了攻击者一个永久通行证。JJWT 解析时如果 Token 没有exp并不会主动报错这点很坑所以工具封装时必须强制校验过期字段。设计上也不要图省事把 Access Token 设成 7 天前面章节已经讨论过原因。Refresh Token 无法主动吊销无状态 Token 的最大短板就是服务端无法主动使其失效。要弥补这个短板最简单的方法是引入一个 Redis 黑名单用户退出登录时把 Access Token 的jti写入黑名单过期时间设置为和 Token 本身一致Refresh Token 则通过 Redis 存储或记录jti来实现主动吊销。否则“退出登录”就只能删除客户端本地 Token服务器这边毫无招架之力。日志打印完整 Token我排查问题时会习惯性打日志有时候顺手把请求头里的 Authorization 整个打出来这其实是很严重的信息泄露。建议日志里只打印 Token 的前 8 位、用户 ID、过期时间这类脱敏信息防止日志系统被拖走后把用户登录态也顺带丢了。6.3 一次多环境密钥不一致的排查实录最后分享一个让我记忆深刻的线上问题。某天晚上测试环境突然大面积报 401生产环境却完全正常刷新页面重新登录也没用。我当时第一反应是 Token 过期时间被改短了查了配置发现没动过又怀疑是服务器时钟漂移NTP 同步查下来也正常。后来把测试环境打印的SignatureException堆栈捞出来又对比了签发服务的日志才发现测试环境的JWT_SECRET环境变量在最近一次发版时被运维覆盖成了别的值。前一台节点还在用老密钥签发新节点用新密钥验签两边自然对不上。这个问题解决起来不复杂但排查过程给了我一个教训遇到 JWT 校验失败第一步永远先确认签发节点和校验节点用的是不是同一把密钥尤其是在多实例部署、多环境切换的时候。现在我的习惯是把这套认证逻辑的单元测试补得特别全生成、解析、过期、篡改、弱密钥这五个用例只要有人改了密钥配置或者升级了 JJWT 版本CI 阶段第一个挂掉的往往就是测试而不是线上接口。如果你正准备给项目加 JWT 认证先把密钥长度、过期策略、续签方式、拦截器顺序、异常映射这五件事想清楚后面会少踩非常多的坑。
阅读完成 · 觉得有帮助?