SpringBoot前后端token自動(dòng)續(xù)期方案
1. 背景
在前后端分離架構(gòu)下,用戶登錄成功后,后端服務(wù)會(huì)頒發(fā)一個(gè) token 作為用戶身份憑證。前端(如 Vue 應(yīng)用)在接收到 token 后,通常將其存儲(chǔ)到 LocalStorage,并在每次請(qǐng)求時(shí)通過請(qǐng)求頭攜帶該 token 訪問后端服務(wù)。
后端服務(wù)會(huì)在過濾器中對(duì) token 進(jìn)行校驗(yàn),包括合法性和是否過期。當(dāng) token 已過期時(shí),后端會(huì)返回錯(cuò)誤信息,引導(dǎo)前端跳轉(zhuǎn)至登錄頁(yè)面,要求用戶重新登錄。
圖片
這種機(jī)制雖然簡(jiǎn)單,但在實(shí)際使用中存在一個(gè)明顯的用戶體驗(yàn)問題。例如:
? 用戶正在填寫一個(gè)較復(fù)雜的表單,提交時(shí)發(fā)現(xiàn) accessToken 已過期,不得不重新登錄并重新填寫;
? 用戶在持續(xù)操作系統(tǒng),但由于 accessToken 有固定時(shí)效,依舊會(huì)在某一時(shí)刻被強(qiáng)制退出。
這類場(chǎng)景都會(huì)造成較差的用戶體驗(yàn)。那么,有沒有辦法在用戶活躍操作時(shí),自動(dòng)延長(zhǎng) token 的有效期呢?
本文將介紹兩種常見的自動(dòng)續(xù)期方案:基于前端的刷新機(jī)制 和 基于后端的自動(dòng)續(xù)期機(jī)制。
2. 自動(dòng)續(xù)期實(shí)現(xiàn)方案
2.1. 基于前端的自動(dòng)續(xù)期(Refresh Token)
如果系統(tǒng)采用 OAuth2 協(xié)議進(jìn)行認(rèn)證,并且支持 Refresh Token,就可以通過前端實(shí)現(xiàn)自動(dòng)續(xù)期。
典型的認(rèn)證響應(yīng)如下:
{
"access_token": "eyJhbGciOiJFUzI1NiIs**********X6wrZHYKDxJkWwhdkrYg",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA",
"refresh_token_expires_in": 604800
......
}說明:
? access_token:訪問令牌(通常有效期 1~2 小時(shí)),前端調(diào)用后端接口時(shí)使用。
? refresh_token:刷新令牌(有效期較長(zhǎng),常見 7~30 天),用于在 access_token 過期時(shí)獲取新的 access_token。
此時(shí),可以在前端通過 響應(yīng)攔截器 自動(dòng)處理續(xù)期邏輯:
// Axios響應(yīng)攔截器實(shí)現(xiàn)自動(dòng)續(xù)期
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refresh_token');
const refreshResponse = await axios.post('/auth/refresh', {
refresh_token: refreshToken
});
// 更新Token
localStorage.setItem('access_token', refreshResponse.data.access_token);
localStorage.setItem('refresh_token', refreshResponse.data.refresh_token);
// 重新發(fā)送原始請(qǐng)求
originalRequest.headers.Authorization = `Bearer ${refreshResponse.data.access_token}`;
returnaxios(originalRequest);
} catch (refreshError) {
// 續(xù)期失敗,清除Token并跳轉(zhuǎn)登錄
localStorage.clear();
window.location.href = '/login';
returnPromise.reject(refreshError);
}
}
returnPromise.reject(error);
}
);這種方式的優(yōu)點(diǎn)是清晰易懂,基于 OAuth2 標(biāo)準(zhǔn)實(shí)現(xiàn);缺點(diǎn)是必須有 refresh_token 機(jī)制 支持,否則無法使用。
2.2 基于后端實(shí)現(xiàn)自動(dòng)續(xù)期
在很多項(xiàng)目中,認(rèn)證并未采用雙 token 模式,而是只有一個(gè) accessToken(通常為 JWT)。此時(shí),可以在后端引入一層“間接認(rèn)證”來實(shí)現(xiàn)自動(dòng)續(xù)期。
實(shí)現(xiàn)思路如下:
登錄時(shí)生成雙 token
? 登錄成功后生成一個(gè) uuidToken(隨機(jī) UUID),同時(shí)生成一個(gè) accessToken;
? 將 uuidToken 作為 key,accessToken(及用戶信息)作為 value 存入緩存,緩存過期時(shí)間為 accessToken 有效期的 2 倍;
? 返回 uuidToken 給前端。
注意:JWT 本身長(zhǎng)度較長(zhǎng),不適合作為緩存 key,因此使用短 UUID 替代。
請(qǐng)求攔截與校驗(yàn)
? 前端請(qǐng)求時(shí)攜帶 uuidToken;
? 后端通過 uuidToken 從緩存中獲取 accessToken;
? 若緩存不存在,則判定為用戶長(zhǎng)時(shí)間未操作,要求重新登錄;
? 若 accessToken 已過期,但 uuidToken 未過期,說明用戶仍在活躍操作,此時(shí)后端可為其生成新的 accessToken 并覆蓋緩存,從而實(shí)現(xiàn)自動(dòng)續(xù)期。
前端無感知
? 前端始終只持有 uuidToken;
? accessToken 的變化僅在后端進(jìn)行更新,對(duì)前端無影響。
登錄邏輯示例
public String login(String userName, String password) {
StringuuidToken= UUID.randomUUID().toString();
SysUsersysUser= userService.getUserByUserName(userName);
// ...認(rèn)證邏輯...
StringaccessToken= JwtUtil.createJWT(sysUser);
LoginUserVOloginUserVO=newLoginUserVO();
loginUserVO.setUserAccount(sysUser.getUserId());
loginUserVO.setName(sysUser.getName());
loginUserVO.setRole("Manager");
loginUserVO.setAccessToken(accessToken);
// 存儲(chǔ)用戶信息至緩存
userTokenService.storeUserToken(uuidToken, loginUserVO);
return uuidToken;
}后端請(qǐng)求過濾器示例
@Slf4j
@Component
publicclassTokenFilterextendsOncePerRequestFilter {
@Override
protectedvoiddoFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)throws IOException {
try {
StringrequestPath= request.getRequestURI();
// 白名單放行
if (isWhiteListPath(requestPath)) {
filterChain.doFilter(request, response);
return;
}
// 獲取UUID Token
StringuuidToken= getAccessToken(request);
if (uuidToken == null) {
sendUnauthorizedResponse(response, "Missing token");
return;
}
// 獲取用戶信息
LoginUserVOloginUserVO= userTokenService.getUserByToken(uuidToken);
if (loginUserVO == null) {
sendUnauthorizedResponse(response, "Token expired or invalid");
return;
}
// 檢查并續(xù)期
loginUserVO = userTokenService.checkAndRefreshIfNeeded(uuidToken, loginUserVO);
// 設(shè)置用戶上下文
UserContextHolder.setContext(UserContext.fromUserToken(uuidToken, loginUserVO));
filterChain.doFilter(request, response);
} finally {
UserContextHolder.clearContext();
}
}
private String getAccessToken(HttpServletRequest request) {
Stringtoken= request.getHeader("Authorization");
return (token != null) ? token : request.getParameter("Authorization");
}
}3. 總結(jié)
在前后端分離的應(yīng)用中,token 的續(xù)期機(jī)制直接影響用戶體驗(yàn)。
- ? 基于前端的方案(Refresh Token)適用于標(biāo)準(zhǔn) OAuth2 認(rèn)證體系,方案清晰,但依賴協(xié)議支持;
- ? 基于后端的方案(UUID Token + 自動(dòng)刷新)則適用于只有單一
accessToken的場(chǎng)景,能在后端無感知地為用戶自動(dòng)續(xù)期。
在實(shí)際項(xiàng)目中,應(yīng)根據(jù)系統(tǒng)架構(gòu)和認(rèn)證機(jī)制選擇合適的續(xù)期方案,從而在安全性和用戶體驗(yàn)之間取得平衡。


































