双Token机制(通常指Access Token和Refresh Token)是现代Web应用中一种常见的身份验证和会话管理方案,旨在提升安全性和用户体验。
一、 双Token机制的作用
双Token机制主要由两种令牌组成:
-
访问令牌(Access Token) :
-
刷新令牌(Refresh Token) :
双Token机制的优点:
- 提高安全性 :通过将Access Token的有效期设置得很短,即使Access Token被泄露,其被滥用的时间也有限。Refresh Token的长期性则允许用户在不频繁重新登录的情况下保持会话,但它不会随每次请求发送,降低了其泄露风险 [2][6]。
- 改善用户体验(无感刷新) :当Access Token过期时,前端可以利用Refresh Token在后台"静默"地获取新的Access Token,而无需用户感知或被迫重新登录,从而实现了"无感刷新" [7][8]。
- 减少密码传输风险 :用户只需在首次登录时传输密码,之后通过令牌进行身份验证,降低了密码被截获或泄露的风险 [2]。
- 更好的授权控制 :Refresh Token可以被服务器随时撤销,例如用户登出、修改密码或检测到可疑活动时,可以使所有相关的Access Token失效,从而更好地控制用户会话 [3][9]。
二、 前端如何实现双Token机制
前端实现双Token机制的核心在于管理Access Token和Refresh Token的存储、发送以及在Access Token过期时自动刷新。
-
用户登录:
-
令牌存储:
- 前端接收到Access Token和Refresh Token后,需要将其安全地存储起来 [1][7]。
- Access Token通常存储在内存、
localStorage
或sessionStorage
中,以便在后续请求中方便地添加到请求头 [7][10]。 - Refresh Token由于其敏感性和长期性,建议存储在
HttpOnly
的Cookie中,这样JavaScript无法直接访问它,可以有效防止XSS攻击窃取Refresh Token [1][3]。如果不能使用HttpOnly Cookie,也可以加密后存储在localStorage
或sessionStorage
中,但安全性会降低。
-
访问受保护资源:
-
Access Token过期处理(无感刷新) :
- 当Access Token过期时,后端会返回一个特定的错误码(例如HTTP 401 Unauthorized) [7][8]。
- 前端的HTTP请求拦截器(如使用Axios)捕获到这个错误 [7][8]。
- 拦截器会暂停当前的请求,然后使用Refresh Token向后端发起一个刷新Access Token的请求 [7][8].
- 后端验证Refresh Token的有效性。如果有效,则生成新的Access Token(有时也会返回新的Refresh Token,实现Refresh Token轮换策略),并返回给前端 [1][4].
- 前端接收到新的Access Token后,更新本地存储的Access Token [7][8].
- 然后,拦截器会使用新的Access Token重新发起之前失败的请求 [8][11].
-
Refresh Token过期或无效处理:
2. 前端具体实现步骤(以Axios为例) [7][13]:
-
安装Axios:
jsnpm install axios
-
创建Axios实例和请求/响应拦截器:
js// api.js import axios from 'axios'; // 创建Axios实例 const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API || '/api', // 你的API基础URL timeout: 10000 // 请求超时时间 }); // 用于存储正在刷新token的请求,避免重复刷新 let isRefreshing = false; // 存储所有待重试的请求 let requests = []; // 请求拦截器 service.interceptors.request.use( config => { const accessToken = localStorage.getItem('accessToken'); if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }, error => { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( response => { // 正常响应 return response; }, async error => { const originalRequest = error.config; // 如果响应状态码是401 (Unauthorized) 且不是刷新token的请求 if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; // 标记为已重试,避免死循环 // 如果当前没有正在刷新token,则发起刷新请求 if (!isRefreshing) { isRefreshing = true; try { const refreshToken = localStorage.getItem('refreshToken'); // 从本地存储获取refreshToken if (!refreshToken) { // 如果没有refreshToken,直接跳转到登录页 window.location.href = '/login'; // 或者其他登录路由 return Promise.reject(error); } // 调用刷新token的API const res = await axios.post('/auth/refresh-token', { refreshToken }); // 假设你的刷新token接口是 /auth/refresh-token const { accessToken: newAccessToken, refreshToken: newRefreshToken } = res.data; // 更新本地存储的token localStorage.setItem('accessToken', newAccessToken); localStorage.setItem('refreshToken', newRefreshToken); // 如果后端返回新的refreshToken,也更新 // 刷新成功后,重试之前所有因token过期而失败的请求 requests.forEach(cb => cb(newAccessToken)); requests = []; // 清空待重试请求队列 // 使用新的token重新发起原始请求 originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return service(originalRequest); // 使用service实例重新发送请求 } catch (refreshError) { // 刷新token失败(refreshToken也过期或无效) console.error('Refresh token failed:', refreshError); // 清除所有token并跳转到登录页 localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; // 或者其他登录路由 return Promise.reject(refreshError); } finally { isRefreshing = false; } } else { // 如果正在刷新token,将当前请求加入队列,等待刷新完成后重试 return new Promise(resolve => { requests.push((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; resolve(service(originalRequest)); }); }); } } return Promise.reject(error); } ); export default service;
-
在应用中使用 :
在你的Vue/React组件或服务中导入并使用这个
service
实例来发送HTTP请求。js// example.js import service from './api'; async function getUserData() { try { const response = await service.get('/user/profile'); console.log(response.data); } catch (error) { console.error('Failed to fetch user data:', error); } } // 登录成功后存储token async function login(username, password) { try { const response = await axios.post('/auth/login', { username, password }); localStorage.setItem('accessToken', response.data.accessToken); localStorage.setItem('refreshToken', response.data.refreshToken); console.log('Login successful!'); getUserData(); // 登录成功后尝试获取用户数据 } catch (error) { console.error('Login failed:', error); } } // 调用登录 // login('testuser', 'password123');
注意事项:
- Refresh Token的存储安全 :尽可能将Refresh Token存储在
HttpOnly
的Cookie中,以降低XSS攻击的风险 [1][3]。如果无法使用,前端存储时务必加密。 - Refresh Token的轮换 :为了进一步提高安全性,每次使用Refresh Token获取新的Access Token时,后端也应该颁发一个新的Refresh Token,并使旧的Refresh Token失效。这被称为Refresh Token轮换(Refresh Token Rotation) [4]。
- 并发请求处理 :在Access Token过期时,可能会有多个并发请求同时失败。上述代码中的
isRefreshing
标志和requests
队列就是为了处理这种情况,避免重复刷新和确保所有失败请求都能被重试 [13]。 - 登出操作 :用户登出时,前端应清除所有本地存储的令牌,并通知后端撤销Refresh Token,使其失效 [3]。
- 错误处理:确保对所有可能的错误情况(如网络问题、后端错误等)进行适当的处理和用户提示。
好文:
- 如何实现双token机制?
- 使用Double Toke登录的优点 - 稀土掘金
- What Is a Refresh Token (and How Does It Work)? - Descope
- Significance of a JWT Refresh Token | Baeldung on Computer Science
- 令牌
- 日常开发中,关于双token机制的介绍及双token的优点 - 勾股博客
- 面试官:说说前端怎么无感刷新token? - 稀土掘金
- 前端双token无感刷新图文详解 - 知途无界
- JWT refresh token flow - Stack Overflow
- 如何实现无感刷新Token - 最小生成树- 博客园
- JWT Refresh Token flow from Client point of view : r/golang - Reddit
- Matarialize中文技术社区-c猿人个人博客-Access Token与Refresh Token
- 使用双token实现无感刷新,前后端详细代码(有代码地址)