前端双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实现无感刷新,前后端详细代码(有代码地址)
相关推荐
coding随想5 小时前
JavaScript ES6 解构:优雅提取数据的艺术
前端·javascript·es6
小小小小宇5 小时前
一个小小的柯里化函数
前端
灵感__idea5 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
小小小小宇5 小时前
重提React闭包陷阱
前端
小小小小宇5 小时前
前端XSS和CSRF以及CSP
前端
UFIT5 小时前
NoSQL之redis哨兵
java·前端·算法
超级土豆粉6 小时前
CSS3 的特性
前端·css·css3
星辰引路-Lefan6 小时前
深入理解React Hooks的原理与实践
前端·javascript·react.js
wyn200011286 小时前
JavaWeb的一些基础技术
前端