前端双Token机制无感刷新

双Token机制(通常指Access Token和Refresh Token)是现代Web应用中一种常见的身份验证和会话管理方案,旨在提升安全性和用户体验。

一、 双Token机制的作用

双Token机制主要由两种令牌组成:

  1. 访问令牌(Access Token)

    • 作用:用于访问受保护的资源。每次前端向后端发送请求访问受保护的API时,都需要携带Access Token。
    • 特点:有效期通常较短(如15分钟到1小时),包含用户的身份信息和权限声明。
    • 安全性 :由于有效期短,即使Access Token被窃取,攻击者利用它进行恶意操作的时间窗口也有限,从而降低了泄露的风险 [1][2]
  2. 刷新令牌(Refresh Token)

    • 作用 :当Access Token过期后,用于获取新的Access Token,而无需用户重新登录 [3][4]
    • 特点 :有效期通常较长(如几天到几周),但不能直接用于访问受保护资源 [1][5]
    • 安全性 :通常存储在更安全的位置(如HTTP-Only Cookie),以降低被窃取的风险 [1][3]

双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过期时自动刷新。

1. 基本工作流程 [1][8]:

  • 用户登录

    • 用户在客户端输入凭据(用户名、密码)进行登录 [1]
    • 前端将凭据发送给后端认证服务器 [1]
    • 后端验证凭据,如果成功,生成一个短期的Access Token和一个长期的Refresh Token,并将其返回给前端 [1][8]
  • 令牌存储

    • 前端接收到Access Token和Refresh Token后,需要将其安全地存储起来 [1][7]
    • Access Token通常存储在内存、localStoragesessionStorage 中,以便在后续请求中方便地添加到请求头 [7][10]
    • Refresh Token由于其敏感性和长期性,建议存储在HttpOnly的Cookie中,这样JavaScript无法直接访问它,可以有效防止XSS攻击窃取Refresh Token [1][3]。如果不能使用HttpOnly Cookie,也可以加密后存储在localStoragesessionStorage中,但安全性会降低。
  • 访问受保护资源

    • 前端在发起对受保护资源的请求时,将Access Token添加到HTTP请求头中(通常是Authorization: Bearer <Access Token>[5][8]
    • 后端接收到请求后,验证Access Token的有效性。如果有效,则处理请求并返回数据 [1][8]
  • 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过期或无效处理

    • 如果Refresh Token也过期或无效(例如被服务器撤销),后端会返回相应的错误 [1][12]
    • 前端拦截器捕获到此错误后,会清除所有本地存储的令牌,并重定向用户到登录页面,要求用户重新登录 [1][12]

2. 前端具体实现步骤(以Axios为例) [7][13]:

  1. 安装Axios

    js 复制代码
    npm install axios
  2. 创建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;
  3. 在应用中使用

    在你的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]
  • 错误处理:确保对所有可能的错误情况(如网络问题、后端错误等)进行适当的处理和用户提示。

好文:

  1. 如何实现双token机制?
  2. 使用Double Toke登录的优点 - 稀土掘金
  3. What Is a Refresh Token (and How Does It Work)? - Descope
  4. Significance of a JWT Refresh Token | Baeldung on Computer Science
  5. 令牌
  6. 日常开发中,关于双token机制的介绍及双token的优点 - 勾股博客
  7. 面试官:说说前端怎么无感刷新token? - 稀土掘金
  8. 前端双token无感刷新图文详解 - 知途无界
  9. JWT refresh token flow - Stack Overflow
  10. 如何实现无感刷新Token - 最小生成树- 博客园
  11. JWT Refresh Token flow from Client point of view : r/golang - Reddit
  12. Matarialize中文技术社区-c猿人个人博客-Access Token与Refresh Token
  13. 使用双token实现无感刷新,前后端详细代码(有代码地址)
相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼5 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT5 小时前
promise & async await总结
前端
Jerry说前后端5 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天5 小时前
A12预装app
linux·服务器·前端