Spring Boot结合Flutter的JWT登录/登出令牌过期处理方案咨询
JWT登录/登出完整实现方案
核心结论
仅靠JWT本身无法实现你的需求,必须结合额外的服务端存储机制(比如Redis)来解决令牌续期和即时失效的问题。JWT是无状态的,一旦签发就无法主动作废,且过期时间硬编码在令牌内,所以需要补充以下机制:
一、解决令牌自动续期(问题a)
方案:双令牌机制(Access Token + Refresh Token)
通过长短令牌配合,既保证安全性,又实现用户未登出时的持续授权:
- Access Token:有效期短(如15分钟),用于日常API请求的身份验证
- Refresh Token:有效期长(如7天),仅用于在Access Token过期时获取新的Access Token
实现步骤
- 用户登录验证通过后,服务器同时签发Access Token和Refresh Token,返回给Flutter客户端
- 客户端将两个令牌持久化存储(比如用
shared_preferences),每次API请求携带Access Token - 当服务器返回
401 Unauthorized(Access Token过期),客户端自动调用刷新令牌接口,用Refresh Token换取新的Access Token - 服务器验证Refresh Token:
- 校验签名和自身过期时间
- 确认该Refresh Token未被服务器删除(和登出逻辑联动)
- 验证通过后,签发新的Access Token(可选同时刷新Refresh Token并更新存储)
核心代码示例
// 登录接口返回双令牌 @PostMapping("/login") public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) { // 省略用户身份验证逻辑 User user = userService.validateUser(request.getUsername(), request.getPassword()); String accessToken = jwtUtil.generateAccessToken(user); String refreshToken = jwtUtil.generateRefreshToken(user); // 将Refresh Token存入Redis,关联用户ID并设置过期时间 redisTemplate.opsForValue().set("refresh_token:" + user.getId(), refreshToken, 7, TimeUnit.DAYS); return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken)); } // 刷新令牌接口 @PostMapping("/refresh") public ResponseEntity<AuthResponse> refreshToken(@RequestBody RefreshTokenRequest request) { String refreshToken = request.getRefreshToken(); if (!jwtUtil.isValidRefreshToken(refreshToken)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } String userId = jwtUtil.extractUserId(refreshToken); String storedRefreshToken = redisTemplate.opsForValue().get("refresh_token:" + userId); // 验证Refresh Token是否有效(未被登出删除) if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } User user = userService.getById(userId); String newAccessToken = jwtUtil.generateAccessToken(user); // 可选:生成新的Refresh Token并更新Redis String newRefreshToken = jwtUtil.generateRefreshToken(user); redisTemplate.opsForValue().set("refresh_token:" + userId, newRefreshToken, 7, TimeUnit.DAYS); return ResponseEntity.ok(new AuthResponse(newAccessToken, newRefreshToken)); }
二、解决登出即时失效(问题b)
方案:令牌黑名单机制(基于Redis)
由于JWT无法主动作废,需要在服务器端维护黑名单记录已失效的令牌:
实现步骤
- 用户发起登出请求时,客户端将Access Token和Refresh Token发送到服务器
- 服务器将Access Token加入黑名单,设置过期时间为该令牌的剩余有效期(避免永久占用存储)
- 同时删除Redis中存储的对应Refresh Token
- 每次API请求验证时,先检查Access Token是否在黑名单中,若存在则直接返回未授权
核心代码示例
@PostMapping("/logout") public ResponseEntity<Void> logout(@RequestHeader("Authorization") String authHeader) { String accessToken = authHeader.replace("Bearer ", ""); // 计算令牌剩余有效期,设置黑名单过期时间 Long remainingTime = jwtUtil.extractExpiration(accessToken).getTime() - System.currentTimeMillis(); redisTemplate.opsForValue().set("blacklist:" + accessToken, "invalid", remainingTime, TimeUnit.MILLISECONDS); // 删除对应的Refresh Token String userId = jwtUtil.extractUserId(accessToken); redisTemplate.delete("refresh_token:" + userId); return ResponseEntity.ok().build(); } // 在JWT验证过滤器中添加黑名单检查 public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String accessToken = extractTokenFromRequest(request); if (accessToken != null && jwtUtil.isValidAccessToken(accessToken)) { // 检查令牌是否在黑名单中 if (redisTemplate.hasKey("blacklist:" + accessToken)) { response.sendError(HttpStatus.UNAUTHORIZED.value(), "Token has been invalidated"); return; } // 后续身份验证逻辑(加载用户信息、设置SecurityContext等) } chain.doFilter(request, response); } }
整体流程梳理
- 登录:用户提交账号密码,服务器验证后返回双令牌,同时将Refresh Token存入Redis
- API请求:客户端携带Access Token,服务器依次验证签名、有效期、黑名单状态,通过则返回业务数据
- 令牌过期:Access Token过期后,客户端自动调用刷新接口,服务器验证Refresh Token有效性后返回新的Access Token
- 登出:客户端发送令牌到登出接口,服务器将Access Token加入黑名单并删除Refresh Token,后续该令牌无法再使用
注意事项
- 必须使用支持过期时间的键值存储,Redis是最优选择,轻量且性能高效
- Flutter端需在拦截器中自动处理
401错误,触发令牌刷新逻辑;同时注意令牌的安全存储(比如Refresh Token存入移动端Keystore) - 可根据业务需求调整双令牌的有效期,平衡安全性和用户体验
内容的提问来源于stack exchange,提问作者ayush
相关产品推荐
相关产品推荐

