SpringBoot 實現 Token 無感刷新機制
作者:一安
在前后端分離架構主導的今天,身份認證已成為系統安全的第一道防線。Token認證憑借其無狀態特性成為主流方案,但Token有效期始終是平衡安全性與用戶體驗的難點 —— 過短導致頻繁登錄,過長增加安全風險。
前言
在前后端分離架構主導的今天,身份認證已成為系統安全的第一道防線。Token認證憑借其無狀態特性成為主流方案,但Token有效期始終是平衡安全性與用戶體驗的難點 —— 過短導致頻繁登錄,過長增加安全風險。
傳統 Token 認證的痛點
傳統單Token機制存在難以調和的矛盾:
- 安全性需求:
Token應短期有效,降低被盜用后的風險窗口 - 用戶體驗需求:
Token應長期有效,避免頻繁登錄打斷操作流程 - 業務連續性:金融、醫療等領域對接口連續性要求極高,
Token過期可能導致業務中斷
雙 Token 機制:無感刷新的核心
無感刷新的解決方案是引入雙Token架構:
Access Token(訪問令牌):短期有效(如2小時),用于接口訪問Refresh Token(刷新令牌):長期有效(如7天),僅用于獲取新Access Token
工作流程如下:
- 用戶登錄時,服務器同時生成
Access Token和Refresh Token - 客戶端請求接口時攜帶
Access Token Access Token過期時,客戶端使用Refresh Token靜默獲取新Access Token- 若
Refresh Token過期,則要求用戶重新登錄
這種機制既保證了接口訪問的短期安全性,又通過Refresh Token延長了用戶會話周期。
效果圖
圖片
- 用戶登錄功能,獲取
Access Token和Refresh Token - 展示
Token信息和過期時間 API請求測試區域,支持自定義請求路徑、方法和請求體- 自動處理
Token過期情況,當Access Token過期時會自動使用Refresh Token刷新 - 記錄
Token刷新歷史,方便查看刷新情況
實現
JWT 工具類:Token 生成與解析
@Component
public class JwtUtil {
private final String SECRET_KEY = "yianweilai"; // 實際項目中應從配置文件獲取
/**
* 從 token 中提取用戶名
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 從 token 中提取過期時間
*/
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
/**
* 檢查 token 是否過期
*/
public Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* 生成 token
*/
public String generateToken(String username, int seconds) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username, seconds);
}
private String createToken(Map<String, Object> claims, String subject, int seconds) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + seconds * 1000))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
/**
* 驗證 token
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}Token 服務:管理 Token 生命周期
@Service
public class TokenService {
private static final String REFRESH_TOKEN_KEY_PREFIX = "refresh_token:";
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate<String, String> redisTemplate;
public String generateAccessToken(String username) {
return jwtUtil.generateToken(username, 5);
}
public String generateRefreshToken(String username) {
String refreshToken = jwtUtil.generateToken(username, 10);
// 將 Refresh Token 存儲到 Redis
redisTemplate.opsForValue().set(REFRESH_TOKEN_KEY_PREFIX + username, refreshToken, 10, TimeUnit.SECONDS);
return refreshToken;
}
public boolean validateRefreshToken(String username, String refreshToken) {
// 從 Redis 獲取存儲的 Refresh Token
String storedRefreshToken = redisTemplate.opsForValue().get(REFRESH_TOKEN_KEY_PREFIX + username);
if (storedRefreshToken != null && storedRefreshToken.equals(refreshToken)) {
// 驗證 Refresh Token 是否過期
return !jwtUtil.isTokenExpired(refreshToken);
}
returnfalse;
}
}以上測試需要,所以時間周期比較短。
攔截器:自動處理 Token 刷新
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
private final TokenService tokenService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService, TokenService tokenService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
// 從請求頭中獲取 token
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String accessToken = null;
String refreshToken = null;
// 解析 token
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
accessToken = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(accessToken);
} catch (ExpiredJwtException e) {
// Access Token 已過期,嘗試使用 Refresh Token 刷新
refreshToken = request.getHeader("Refresh-Token");
if (refreshToken != null) {
try {
String refreshUsername = jwtUtil.extractUsername(refreshToken);
if (refreshUsername != null) {
// 驗證 Refresh Token 是否有效
boolean isValidRefreshToken = tokenService.validateRefreshToken(refreshUsername, refreshToken);
if (isValidRefreshToken) {
// 生成新的 Access Token
String newAccessToken = tokenService.generateAccessToken(refreshUsername);
response.setHeader("x-new-access-token", newAccessToken);
username = refreshUsername;
accessToken = newAccessToken;
}
}
} catch (Exception ex) {
// Refresh Token 無效,讓用戶重新登錄
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Refresh-Token失效,請重新登錄!");
return;
}
}
} catch (Exception e) {
// 其他 token 解析錯誤
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Token解析錯誤!");
return;
}
}
// 設置認證信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(accessToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}前端配合方案
Token 存儲策略
Access Token:存儲在內存或localStorage(短期有效,風險較低)Refresh Token:存儲在HttpOnly Cookie(防止XSS攻擊)
請求攔截器:自動攜帶 Token
// 創建請求攔截器和響應攔截器
const apiClient = {
// 請求攔截器 - 為每個請求添加認證頭
requestInterceptor: function(config) {
if (tokens.accessToken) {
config.headers = config.headers || {};
config.headers['Authorization'] = `Bearer ${tokens.accessToken}`;
// 添加Refresh Token到請求頭
if (tokens.refreshToken) {
config.headers['Refresh-Token'] = tokens.refreshToken;
}
}
return config;
},
// 響應攔截器 - 處理響應和錯誤
responseInterceptor: async function(response) {
// 檢查是否有新的Access Token
const newAccessToken = response.headers.get('x-new-access-token');
if (newAccessToken) {
// 更新本地存儲的Token
tokens.accessToken = newAccessToken;
// 保存到本地存儲
saveTokensToStorage();
// 更新UI顯示
updateTokenDisplay();
// 記錄刷新歷史
addRefreshHistory(Date.now(), '響應攔截器檢測到新Token', '自動更新成功');
showToast('Access Token 已自動更新', 'success');
}
return response;
},
// 錯誤處理攔截器
errorInterceptor: async function(error) {
const originalRequest = error.config;
// 處理401錯誤
if (error.response && error.response.status === 401) {
// 檢查是否已經嘗試過刷新Token
if (originalRequest._retry) {
// 已經嘗試過刷新,仍然失敗,需要重新登錄
showToast('Token驗證失敗,請重新登錄', 'error');
logout();
return Promise.reject(error);
}
// 標記已經嘗試刷新
originalRequest._retry = true;
try {
// 嘗試使用Refresh Token刷新
showToast('Token已過期,嘗試刷新...');
const isRefreshed = await refreshAccessToken();
if (isRefreshed) {
// 刷新成功,重新嘗試原始請求
showToast('Token刷新成功,重試請求...');
return this.fetch(originalRequest.url, originalRequest);
}
} catch (refreshError) {
showToast('Token刷新失敗,請重新登錄', 'error');
logout();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
},
// 封裝fetch請求,應用攔截器
fetch: async function(url, config = {}) {
try {
// 應用請求攔截器
const requestConfig = this.requestInterceptor({
method: 'GET',
...config,
url
});
// 發送請求
const response = await fetch(url, requestConfig);
// 處理非成功狀態碼
if (!response.ok) {
throw {
response,
config: requestConfig
};
}
// 應用響應攔截器
return this.responseInterceptor(response);
} catch (error) {
// 應用錯誤攔截器
return this.errorInterceptor(error);
}
}
};安全考慮
實現Token無感刷新時,需要注意以下安全問題:
Refresh Token的安全性Refresh Token有效期較長,應妥善保管,建議存儲在HttpOnly Cookie中,防止XSS攻擊Token泄露風險:使用HTTPS協議,防止Token在傳輸過程中被截獲- 單點登錄控制:每個用戶只允許有一個有效的
Refresh Token,當用戶登錄時,生成新的Refresh Token并使舊的失效 Token過期策略:合理設置Access Token和Refresh Token的有效期,平衡安全性和用戶體驗- 異常處理:當檢測到
Token被盜用時,應立即失效相關Token,并通知用戶
責任編輯:武曉燕
來源:
一安未來




























