引言
在前后端分離的應用中,使用Token進行認證是一種較為常見的方式。但是,由于Token的有效期限制,需要不斷刷新Token,否則會導致用戶認證失敗。為了解決這個問題,可以實現無感知刷新Token的功能,本文將介紹如何實現無感知刷新Token。
Token認證的原理
在Web應用中,常見的Token認證方式有基于Cookie和基于Token的認證。基于Cookie的認證方式是將認證信息保存在Cookie中,每次請求時將Cookie發送給服務器進行認證;而基于Token的認證方式是將認證信息保存在Token中,每次請求時將Token發送給服務器進行認證。
在基于Token的認證方式中,客戶端將認證信息保存在Token中,而不是保存在Cookie中。在認證成功后,服務器將生成一個Access Token和一個Refresh Token,并將它們返回給客戶端。Access Token用于訪問受保護的API,Refresh Token用于獲取新的Access Token。
什么是無感知刷新Token
無感知刷新Token是指,在Token過期之前,系統自動使用Refresh Token獲取新的Access Token,從而實現Token的無感知刷新,用戶可以無縫繼續使用應用。
在實現無感知刷新Token的過程中,需要考慮以下幾個方面:
- 如何判斷Token是否過期?
- 如何在Token過期時自動使用Refresh Token獲取新的Access Token?
- 如何處理Refresh Token的安全問題?
下面將介紹如何實現無感知刷新Token的具體步驟。
實現步驟
步驟一:獲取Access Token和Refresh Token
在認證成功后,需要將Access Token和Refresh Token發送給客戶端。Access Token用于訪問受保護的API,Refresh Token用于獲取新的Access Token??梢允褂肑WT(jsON Web Token)或OAuth2(開放授權)等方式實現認證。
在JWT中,可以使用如下代碼生成Access Token和Refresh Token:
- const accessToken = jwt.sign({userId: '123'}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});
- const refreshToken = jwt.sign({userId: '123'}, 'REFRESH_TOKEN_SECRET', {expiresIn: '7d'});
步驟二:在請求中攜帶Access Token
在每個需要認證的API請求中,需要在請求頭中攜帶Access Token,如下所示:
- GET /api/user HTTP/1.1
- Host: example.com
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
在前端中,可以使用AxIOS等庫設置請求頭:
- axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
步驟三:攔截401 Unauthorized響應
在服務器返回401 Unauthorized響應時,說明Access Token已經過期,需要使用Refresh Token獲取新的Access Token??梢允褂肁xios攔截器或Fetch API的中間件實現攔截。
在Axios中,可以使用如下代碼實現攔截器:
- axios.interceptors.response.use(response => {
- return response;
- }, error => {
- const originalRequest = error.config;
- if (error.response.status === 401 && !originalRequest._retry) {
- originalRequest._retry = true; //防止無限調用
- return axios.post('/api/refresh_token', {refreshToken})
- .then(response => {
- const { access_token, refresh_token } = response.data;
- localStorage.setItem('access_token', access_token);
- localStorage.setItem('refresh_token', refresh_token);
- axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
- originalRequest.headers.Authorization = `Bearer ${access_token}`;
- return axios(originalRequest);
- });
- }
- return Promise.reject(error);
- });
在Fetch中,可以使用如下代碼實現中間件:
- function authMiddleware(request) {
- const access_token = localStorage.getItem('access_token');
- if (access_token) {
- request.headers.set('Authorization', `Bearer ${access_token}`);
- }
- return request;
- }
- function tokenRefreshMiddleware(response) {
- if (response.status === 401) {
- const refreshToken = localStorage.getItem('refresh_token');
- return fetch('/api/refresh_token', {
- method: 'POST',
- headers: {
- 'Content-Type': 'Application/json'
- },
- body: JSON.stringify({ refreshToken })
- }).then(response => {
- if (response.ok) {
- return response.json();
- }
- throw new Error('Refresh Token failed');
- }).then(data => {
- localStorage.setItem('access_token', data.access_token);
- localStorage.setItem('refresh_token', data.refresh_token);
- return Promise.resolve('refreshed');
- }).catch(error => {
- localStorage.removeItem('access_token');
- localStorage.removeItem('refresh_token');
- return Promise.reject(error);
- });
- }
- return Promise.resolve('ok');
- }
- fetch('/api/user', {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json'
- },
- middleware: [authMiddleware, tokenRefreshMiddleware]
- }).then(response => {
- console.log(response);
- }).catch(error => {
- console.error(error);
- });
在上述代碼中,使用Axios或Fetch攔截器攔截401 Unauthorized響應,如果發現Access Token已經過期,則發送Refresh Token請求獲取新的Access Token,并將新的Access Token設置到請求頭中,重新發送請求。
步驟四:服務器處理Refresh Token請求
在服務器端,需要編寫API處理Refresh Token請求,生成新的Access Token,并返回給客戶端。
在JWT中,可以使用如下代碼生成新的Access Token:
- const accessToken = jwt.sign({userId: '123'}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});
在刷新Token時,需要驗證Refresh Token的合法性,可以使用如下代碼驗證Refresh Token:
- try {
- const payload = jwt.verify(refreshToken, 'REFRESH_TOKEN_SECRET');
- const accessToken = jwt.sign({userId: payload.userId}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});
- const refreshToken = jwt.sign({userId: payload.userId}, 'REFRESH_TOKEN_SECRET', {expiresIn: '7d'});
- res.json({access_token: accessToken, refresh_token: refreshToken});
- } catch (err) {
- res.sendStatus(401);
- }
在上述代碼中,使用JWT的verify方法驗證Refresh Token的合法性,如果驗證成功,則生成新的Access Token和Refresh Token,并返回給客戶端。
步驟五:設置定時刷新Token
為了避免Access Token過期時間太長,可以設置定時刷新Token的功能??梢允褂枚〞r器或Web Workers等方式實現定時刷新Token。在每次刷新Token時,需要重新獲取新的Access Token和Refresh Token,并保存到客戶端。
- function refreshToken() {
- const refreshToken = localStorage.getItem('refresh_token');
- axios.post('/api/refresh_token', {refreshToken})
- .then(response => {
- const { access_token, refresh_token } = response.data;
- localStorage.setItem('access_token', access_token);
- localStorage.setItem('refresh_token', refresh_token);
- axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
- })
- .catch(error => {
- console.error(error);
- });
- }
- setInterval(refreshToken, 14 * 60 * 1000); // 每14分鐘刷新Token
在上述代碼中,使用定時器每14分鐘刷新Token。在刷新Token成功后,將新的Access Token和Refresh Token保存到客戶端,并將新的Access Token設置到請求頭中。
安全性考慮
在實現無感知刷新Token的過程中,需要考慮到Refresh Token的安全性問題。因為Refresh Token具有長期的有效期限,一旦Refresh Token被泄露,攻擊者就可以使用Refresh Token獲取新的Access Token,從而繞過認證機制,訪問受保護的API。
為了增加Refresh Token的安全性,可以考慮以下幾種措施:
- 將Refresh Token保存在HttpOnly Cookie中,可以避免在客戶端被JAVAScript獲??;
- 對Refresh Token進行加密或簽名,可以增加其安全性。
- 將Refresh Token保存在后端,前端通過接口和后端交互,實現刷新Access Token。