JWT + Refresh Token 双 Token 方案

📅 2026/6/26 9:41:08
JWT + Refresh Token 双 Token 方案
JWT Refresh Token 双 Token 方案文章目录JWT Refresh Token 双 Token 方案1. 背景2. 架构设计2.1 总体流程2.2 Token 类型对比2.3 JWT Payload 结构3. 核心实现3.1 配置3.2 Token 生成JwtTokenProvider3.3 登录返回双 TokenAuthController3.4 Token 刷新接口3.5 前端 401 自动刷新拦截器4. 安全性分析4.1 Access Token 泄露4.2 Refresh Token 泄露4.3 type claim 验证5. 与单 Token 方案对比6. 适用场景7. 注意事项1. 背景传统的单 Token JWT 方案存在一个矛盾过期时间短过期时间长安全性✅ Token 泄露影响小❌ Token 泄露影响大用户体验❌ 频繁重新登录✅ 无需频繁登录双 Token 方案通过引入Access Token和Refresh Token两个 Token同时兼顾安全性和用户体验。2. 架构设计2.1 总体流程┌─────────────────────────────────────────────────────────┐ │ 登录 │ │ 用户名密码 / 手机验证码 / OAuth2 │ └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 签发双 Token │ │ ├─ Access Token (30分钟, 用于 API 鉴权) │ │ └─ Refresh Token (7~14天, 仅用于刷新) │ └────────────────────┬────────────────────────────────────┘ │ ┌───────────┴───────────┐ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ 正常请求 │ │ Access Token │ │ 携带 Access │ │ 过期(401) │ │ 正常响应 │ │ │ └─────────────────┘ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 自动用 Refresh │ │ Token 换新 │ │ │ ├── 成功 → 重试请求 └── 失败 → 跳转登录 └─────────────────┘2.2 Token 类型对比属性Access TokenRefresh Token用途API 鉴权Authorization 头换取新的 Access Token有效期30 分钟7 天默认/ 14 天记住我携带用户信息✅ 含 email、avatar 等❌ 仅含用户名存储位置前端 localStorage前端 localStorage服务端状态❌ 无状态❌ 无状态JWK 验证传输频率每次 API 请求仅过期时泄露影响影响 30 分钟较长但仅能用于刷新2.3 JWT Payload 结构Access Token{sub:zhangsan,type:access,email:zhangsanexample.com,avatar:https://...,iat:1719200000,exp:1719201800}Refresh Token{sub:zhangsan,type:refresh,email:,avatar:,iat:1719200000,exp:1719804800}通过typeclaim 区分两种 TokenJwtAuthenticationFilter只认typeaccess的 Token。3. 核心实现3.1 配置app:jwt:secret:YourSecretKeyForJWT...access-token-expiration-ms:1800000# 30 分钟refresh-token-expiration-ms:604800000# 7 天默认refresh-token-remember-me-expiration-ms:1209600000# 14 天记住我3.2 Token 生成JwtTokenProvider// 生成 Access Token含用户信息publicStringgenerateAccessToken(Stringusername,Stringemail,Stringavatar){returnbuildToken(username,email,avatar,accessTokenExpirationMs,access);}// 生成 Refresh Token根据 rememberMe 决定有效期publicStringgenerateRefreshToken(Stringusername,booleanrememberMe){longexpiryrememberMe?refreshTokenRememberMeExpirationMs:refreshTokenExpirationMs;returnbuildToken(username,null,null,expiry,refresh);}3.3 登录返回双 TokenAuthControllerPostMapping(/login)publicResponseEntityApiResponseLoginResponselogin(ValidRequestBodyLoginRequestrequest){// ... 认证逻辑 ...StringaccessTokenjwtTokenProvider.generateAccessToken(username,email,avatar);StringrefreshTokenjwtTokenProvider.generateRefreshToken(username,request.isRememberMe());LoginResponseresponsenewLoginResponse(accessToken,refreshToken,username,avatar);returnResponseEntity.ok(ApiResponse.success(登录成功,response));}3.4 Token 刷新接口PostMapping(/refresh)publicResponseEntityApiResponseLoginResponserefresh(RequestBodyMapString,Stringrequest){StringrefreshTokenValuerequest.get(refreshToken);// 验证 Refresh Tokenif(!jwtTokenProvider.validateRefreshToken(refreshTokenValue)){returnResponseEntity.status(401).body(ApiResponse.error(refreshToken 无效或已过期));}// 提取用户信息StringusernamejwtTokenProvider.getUsernameFromRefreshToken(refreshTokenValue);// 签发新的双 TokenRefresh Token 轮换StringnewAccessTokenjwtTokenProvider.generateAccessToken(username,email,avatar);StringnewRefreshTokenjwtTokenProvider.generateRefreshToken(username,false);returnResponseEntity.ok(ApiResponse.success(Token 刷新成功,response));}3.5 前端 401 自动刷新拦截器// Axios 响应拦截器http.interceptors.response.use((response)response,async(error){if(error.response?.status!401)returnPromise.reject(error)// 刷新接口本身 401 → 跳转登录if(originalRequest.url/api/auth/refresh){clearAuth();window.location.href/loginreturnPromise.reject(error)}// 用 Refresh Token 换新constresawaitaxios.post(/api/auth/refresh,{refreshToken:localStorage.getItem(refreshToken)})const{token,refreshToken}res.data.data localStorage.setItem(token,token)localStorage.setItem(refreshToken,refreshToken)// 重试原始请求originalRequest.headers.AuthorizationBearer${token}returnhttp(originalRequest)})4. 安全性分析4.1 Access Token 泄露有效期仅 30 分钟攻击者只能在这 30 分钟内使用用户刷新 Token 后旧的 Access Token 自动失效因为 Refresh Token 轮换但 Access Token 本身是无状态的需等自然过期4.2 Refresh Token 泄露传输频率极低仅在过期时通过/api/auth/refresh传输一次Refresh Token 轮换每次刷新签发新的 Refresh Token旧的不再有效后续可升级为 Redis 存储服务端有状态支持手动撤销4.3 type claim 验证privatebooleanvalidateToken(Stringtoken,StringexpectedType){ClaimsclaimsparseClaims(token);returnexpectedType.equals(claims.get(type,String.class));}JwtAuthenticationFilter只验证typeaccess的 TokenRefresh Token 即使被传给 API 接口也会因 type 不匹配而被拒绝5. 与单 Token 方案对比对比项单 Token双 Token本方案Token 数量1 个2 个过期时间7~14 天统一Access 30min Refresh 7~14d安全性泄露影响大泄露影响小30 分钟窗口用户体验无需用户额外操作无感刷新实现复杂度简单中等服务端存储无无完全无状态Token 撤销只能等过期可通过 Refresh Token 轮换实现软撤销6. 适用场景Web 应用Token 存储在 localStorageAccess Token 短过期 Refresh Token 自动续期移动端Token 存储在安全存储区减少重新登录频率单点登录SSORefresh Token 作为长期凭证Access Token 用于各子系统鉴权7. 注意事项前端必须处理并发刷新多个请求同时 401 时只发起一次刷新请求其他请求排队等待见isRefreshingpendingRequests实现Refresh Token 也要有合理有效期7 天默认 14 天记住我不宜过长HTTPS 传输所有 Token 必须通过 HTTPS 传输防止中间人窃取后续可升级点Refresh Token 存入 Redis → 支持手动撤销登录设备管理 → 一个用户可撤销指定设备的 Refresh Token指纹识别 → 绑定设备信息到 Refresh Token 中