背景介紹
在傳統的Spring Boot項目中,用戶登錄認證常見的方案是使用JWT(JSON Web Token)來實現無狀態的身份驗證。JWT憑借自包含用戶信息、方便前后端分離、性能較好等優勢被廣泛采用。
然而,在實際項目中,JWT也有一定缺點,比如:
-
不能主動失效(除非設計復雜的黑名單機制)
-
token刷新邏輯復雜
-
服務器無法靈活控制單點登出
為了提升認證的靈活性和安全性,我將項目的登錄鑒權方案由JWT切換成了 Redis + UUID Token 的模式,實現了服務端存儲與校驗,且支持token的自動續期。
為什么切換?
-
支持主動注銷:退出登錄時,服務器可以直接刪除Redis中對應的Token。
-
便于統一管理Token生命周期:可以靈活設置過期時間和續期策略。
-
方便單點登出和多端管理。
-
實現滑動過期(自動續期),提升用戶體驗。
方案架構
流程圖
用戶登錄 -> 服務器生成UUID Token -> 保存Token對應的用戶ID到Redis并設置過期 -> 返回Token給客戶端 -> 客戶端請求時攜帶Token -> 網關/服務端從Redis驗證Token有效性 -> 每次請求時刷新Token過期時間(滑動過期) -> 用戶退出登錄時刪除Redis中的Token
關鍵代碼實現
1. 登錄接口
@PostMapping("/login")
public Result<UserVO> login(@RequestBody LoginRequest loginRequest) {// 認證邏輯驗證用戶名密碼(略)User user = userService.findByUsername(loginRequest.getUsername());if (user == null || !passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {return Result.fail("用戶名或密碼錯誤");}// 生成 UUID TokenString token = UUID.randomUUID().toString();// 構建 Redis 鍵String redisKey = RedisKeyConstant.LOGIN_TOKEN_PREFIX + token;// 保存用戶 ID 到 Redis,設置過期時間(30分鐘)stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(user.getId()), 30, TimeUnit.MINUTES);// 返回給前端UserVO userVO = new UserVO();userVO.setToken(token);// 其他用戶信息設置略return Result.ok(userVO);
}
2. 網關全局過濾器(校驗Token + 自動續期)
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {private final StringRedisTemplate redisTemplate;private final AntPathMatcher antPathMatcher = new AntPathMatcher();private final long TOKEN_EXPIRE_MINUTES = 30;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getPath().toString();// 跳過無需認證路徑if (isExclude(path)) {return chain.filter(exchange);}List<String> tokenList = exchange.getRequest().getHeaders().get("Authorization");if (tokenList == null || tokenList.isEmpty()) {return unauthorized(exchange);}String token = tokenList.get(0);// Redis校驗Token是否有效String redisKey = RedisKeyConstant.LOGIN_TOKEN_PREFIX + token;String userId = redisTemplate.opsForValue().get(redisKey);if (userId == null) {return unauthorized(exchange);}// 續期(滑動過期)redisTemplate.expire(redisKey, TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES);// 把用戶ID放入請求頭,供后端服務使用ServerHttpRequest newRequest = exchange.getRequest().mutate().header("user-info", userId).build();return chain.filter(exchange.mutate().request(newRequest).build());}private boolean isExclude(String path) {// 這里可以配置白名單路徑return path.startsWith("/public") || path.equals("/user/login");}private Mono<Void> unauthorized(ServerWebExchange exchange) {exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}@Overridepublic int getOrder() {return 0;}
}
3. 退出登錄接口
@PostMapping("/logout")
public Result<Void> logout(@RequestHeader("Authorization") String token) {if (token == null || token.isEmpty()) {return Result.fail("未登錄");}String redisKey = RedisKeyConstant.LOGIN_TOKEN_PREFIX + token;Boolean deleted = stringRedisTemplate.delete(redisKey);if (Boolean.TRUE.equals(deleted)) {return Result.ok();} else {return Result.fail("退出失敗或已過期");}
}
4. 前端請求示例(基于axios)
import axios from 'axios';const myAxios = axios.create({baseURL: 'http://localhost:9090',withCredentials: true,
});myAxios.interceptors.request.use(config => {const token = localStorage.getItem('token');if (token) {config.headers['Authorization'] = token;}return config;
});myAxios.interceptors.response.use(response => {if (response.data.code === 40101) {alert('未登錄,請重新登錄');window.location.href = '/user/login';}return response.data;
});export default myAxios;
總結
-
通過Redis + UUID Token方案,避免了JWT token的復雜管理。
-
支持服務器主動失效token,支持滑動過期,提升了安全性和用戶體驗。
-
網關統一校驗token,簡化后端服務實現。
-
適合對token管理要求較高,需要靈活控制用戶登錄狀態的項目。
未來展望
-
可以結合Redis的Hash數據結構實現多端登錄管理。
-
增加刷新token接口,實現無感刷新。
-
結合Spring Security進行更細粒度的權限控制。
希望這篇文章對你有所幫助,歡迎點贊和關注!如果你也在用Spring Boot做項目,不妨試試這個思路,靈活又實用。