Token 无感刷新:打造无缝用户体验与坚实安全防线

打印 上一主题 下一主题

主题 879|帖子 879|积分 2637

Token 无感刷新:打造无缝用户体验与坚实安全防线

一、前言

在前端开辟中,用户身份认证通常通过 Token 来实现。然而,Token 的有效期是有限的,逾期后用户必要重新登录,这会影响用户体验。为了解决这个问题,Token 无感刷新 成为了一种常见的优化方案。本文将具体介绍如何实现 Token 无感刷新,并探讨其在高并发场景下的优化策略。
二、Token 基础与有效期痛点

1、Token 工作原理详解

(1)Token身份认证: Token 是一种用于证明用户身份和授权的令牌,通常在用户登录成功后由服务器生成并返回给客户端。客户端在后续的请求中,会将 Token 携带在请求头或请求参数中发送给服务器,服务器通过验证 Token 的有效性来确认用户身份,并决定是否授权用户访问受掩护的资源。
(2) 常见的 Token 范例有 JSON Web Token(JWT)和 OAuth 2.0 中的 Access Token 等。
(3) JWT 由三部门组成:Header(头部)、Payload(负载)和 Signature(签名)。Header 包罗了令牌的范例和使用的签名算法;Payload 则携带了用户的相关信息,如用户 ID、用户名、权限等;Signature 用于验证 Token 的完整性和真实性。OAuth 2.0 的 Access Token 则是用于授权第三方应用访问资源服务器上的受掩护资源。
2、传统有效期设置弊端

问题分类具体问题示例场景用户体验差频繁重新登录:Token 有效期过短(如 15 分钟),用户必要频繁重新登录,影响体验。用户正在填写一个长表单,Token 忽然逾期,提交失败,数据丢失。忽然停止:用户在操作过程中 Token 忽然逾期,导致请求失败,操作停止。安全性问题长期有效的 Token:Token 有效期过长(如 7 天),一旦泄露,攻击者可长期假冒用户 。用户 Token 易被盗取,攻击者在 7 天内可以随意访问用户数据。静态有效期:固定逾期时间无法根据用户行为动态调解,增加安全风险。高并发性能问题会合逾期:大量用户的 Token 在同一时间逾期,导致服务端瞬间压力激增。系统设置 Token 有效期为 1 小时,大量用户在整点时间 Token 逾期,导致服务端刷新接口被挤爆。重复刷新:多个并发请求同时检测到 Token 逾期,大概触发多次刷新操作,浪费资源。动态适配固定有效期:无法根据用户行为(如活跃度、风险等级)动态调解 Token 有效期。高风险操作(如支付)必要更短的 Token 有效期,而普通操作(如浏览)可以适当延长。一刀切策略:所有用户使用相同的有效期设置,无法满意个性化需求。 三、什么是 Token 无感刷新?

Token 无感刷新是指:当用户的 Access Token 逾期时,系统能够自动刷新 Token,用户无需感知或手动操作。其焦点目标是:

  • 提升用户体验:用户无需频繁登录。
  • 保障安全性:通过合理的 Token 管理,防止 Token 泄露或滥用。
  • 性能优化:无感刷新 token 能按需更新,使服务器可适时释放旧会话资源, 释放服务器性能,减轻服务器压力,进步稳定性。
应用场景

适合那些必要长时间保持用户会话的应用步伐:

  • Web 应用步伐-在单页应用(SPA)中,如使用 Vue.js、React 或 Angular 构建的应用,用户大概会在一个页面上停留很长时间而不进行任何操作,导致 token 逾期。
  • 移动应用步伐-移动应用通常长时间运行于后台,期间用户的 token 大概会逾期。如果每次逾期都必要用户重新登录,会影响用户体验。
  • 多设备同步-当用户在多个设备上登录同一账户时,若此中一个设备上的 token 逾期,其他设备也大概会受到影响。
  • 实时通讯应用(如聊天应用或在线协作工具)必要持续与服务器保持连接,以保证消息的即时通报。
四、Token 无感刷新技术实现

1. 双 Token 机制(Access Token + Refresh Token)

这是最常见的无感刷新方案,通过两个 Token 实现:

  • 得到双token

    • 用户登录,服务端返回 Access Token 和 Refresh Token,访问接口时则携带 access_token 访问
    • Access Token 访问令牌:短期有效(如 15 分钟),用于业务请求。
    • Refresh Token 刷新令牌:长期有效(如 7 天),用于刷新 Access Token。
    • 焦点价值:通过分离 Token 的生命周期,减少 Access Token 的刷新频率,同时进步安全性。

  • Access Token 逾期

    • 客户端检测到 Access Token 逾期(如接口返回 401 错误)。
    • 使用 refresh_token 请求新的 Access Token,客户端更新本地存储。

  • 重新请求

    • 使用新获取的access_token重新发起之前的请求

注意点:



  • Access Token 存储在客户端(如 localStorage 或内存)。
  • Refresh Token 存储在安全位置(如 HttpOnly Cookie),可有效防御 XSS攻击(跨站脚本攻击)。
代码实现

  1. import axios from 'axios'
  2. const service = axios.create({
  3. baseURL: 'https://api.example.com',
  4. timeout: 10000
  5. })
  6. // 请求拦截器:自动添加 Access Token
  7. service.interceptors.request.use(config => {
  8.   const accessToken = localStorage.getItem('access_token');
  9.   if (accessToken) {
  10.     config.headers.Authorization = `Bearer ${accessToken}`;
  11.   }
  12.   return config;
  13. });
  14. // 响应拦截器:处理 Token 过期
  15. service.interceptors.response.use(
  16.   response => response,
  17.   async error => {
  18.     const originalRequest = error.config;
  19.     if (error.response?.status === 401 && !originalRequest._retry) {
  20.       originalRequest._retry = true;
  21.       
  22.       try {
  23.         // 使用 Refresh Token 刷新 access Token
  24.         const accessToken = await refreshToken();
  25.         localStorage.setItem('access_token', accessToken);
  26.         // 更新请求头并重试原始请求
  27.         originalRequest.headers.Authorization = `Bearer ${accessToken}`;
  28.         return service(originalRequest);
  29.       } catch (refreshError) {
  30.         // 刷新失败,跳转登录
  31.       router.push('/login')
  32.       }
  33.     }
  34.     return Promise.reject(error);
  35.   }
  36. );
  37. //刷新token函数
  38. async function refreshToken() {
  39.   try {
  40.     const response = await service.get('/refresh', {
  41.       params: {
  42.         token: getRefreshTokenFromCookie(); // 从 HttpOnly Cookie 获取 Refresh Token
  43.       },
  44.       timeout: 30000, // 单独设置超时时间
  45.     });
  46.     return response.data.accessToken; // 返回新的 access_token
  47.   } catch (error) {
  48.     // 清除本地存储的 token
  49.     localStorage.removeItem('access_token');
  50.     localStorage.removeItem('refresh_token');
  51.     throw error; // 抛出错误
  52.   }
  53. }
  54. export default service;
复制代码
2.前端定时刷新

在 Access Token 逾期前主动刷新 Token,避免用户请求时忽然逾期。

  • 设置 Token 有效期

    • Access Token 有效期 15 分钟。
    • 在 Token 逾期前 1 分钟主动刷新。

  • 定时刷新

    • 客户端定时查抄 Token 的剩余有效期。
    • 如果剩余时间小于阈值(如 1 分钟),则主动刷新 Token。

代码实现

  1. let refreshTimeout;
  2. // 检查并刷新 Token 的函数
  3. function checkAndRefreshToken() {
  4.   const accessToken = localStorage.getItem('accessToken');
  5.   if (accessToken) {
  6.     const expiresIn = getTokenExpiresIn(accessToken); // 获取 Token 剩余时间
  7.     if (expiresIn < 60) { // 剩余时间小于 60 秒
  8.       refreshAccessToken().then(newAccessToken => {
  9.         localStorage.setItem('accessToken', newAccessToken);
  10.         // 重新设置定时器
  11.         refreshTimeout = setTimeout(checkAndRefreshToken, (getTokenExpiresIn(newAccessToken) - 60) * 1000);
  12.       });
  13.     } else {
  14.       // 设置定时器,在过期前 1 分钟刷新
  15.       refreshTimeout = setTimeout(checkAndRefreshToken, (expiresIn - 60) * 1000);
  16.     }
  17.   }
  18. }
  19. // 初始化时启动检查
  20. checkAndRefreshToken();
复制代码
3.服务端主动刷新

服务端可以在每次请求时查抄 Token 的剩余有效期,并在接近逾期时返回新的 Token。

  • 服务端查抄

    • 每次请求时,服务端轮询Token的剩余有效期。
    • 判定,如果剩余时间小于阈值(如 1 分钟),返回新的 Access Token。

  • 客户端更新

    • 客户端检测到相应中包罗新的 Token,更新本地存储。

代码实现

  1. // 服务端逻辑
  2. app.use((req, res, next) => {
  3.   const accessToken = req.headers.authorization?.split(' ')[1];
  4.   if (accessToken) {
  5.     const expiresIn = getTokenExpiresIn(accessToken);
  6.     if (expiresIn < 60) { // 剩余时间小于 60 秒
  7.       const newAccessToken = generateAccessToken(req.user);
  8.       res.set('New-Access-Token', newAccessToken);
  9.     }
  10.   }
  11.   next();
  12. });
  13. // 客户端逻辑
  14. service.interceptors.response.use(response => {
  15.   const newAccessToken = response.headers['new-access-token'];
  16.   if (newAccessToken) {
  17.     localStorage.setItem('accessToken', newAccessToken);
  18.   }
  19.   return response;
  20. });
复制代码
4.双token+并发请求锁机制

1. 用户登录 & 获取双 Token


  • 客户端:发送登录请求(账号暗码/验证码等)
  • 服务端:

    • 验证身份后生成 Access Token(短效) 和 Refresh Token(长效)。
    • Access Token:返回给客户端存储(如 localStorage)。
    • Refresh Token:通过 HttpOnly Cookie 返回(防 XSS)。

2. 请求拦截器:设置请求头token


  • 配置请求头添加token,判定请求url,设置差别token
  1. // 添加请求拦截器
  2. service.interceptors.request.use(
  3. config => {
  4. if (config.url !== '/login') {
  5.    const accessToken = localStorage.getItem('access_token')
  6.    config.headers["Authorization"] = `Bearer ${accessToken}`
  7. }
  8. //判断是否是获取新token
  9. if (config.url === '/refresh_token') {
  10.    const refreshToken = localStorage.getItem('refresh_token')
  11.    config.headers["Authorization"] = `Bearer ${refreshToken}`
  12. }
  13. return config
  14. },
  15. error => {
  16. return Promise.reject(error)
  17. })
复制代码
3. 相应拦截器:检测 Token 逾期(401 错误)处理惩罚

  • 判定返回 的401,不是重新获取token的401
  • 判读锁变量 isRefreshing:标记是否正在刷新 Token,则进行token刷新,处理惩罚高并发请求。
  • 请求队列 processQueue:存储等候刷新的请求。
  • 使用 refreshToken 请求新 accessToken
  • 处理惩罚队列中的其他请求,以及重新发起失败的请求
  • 释放锁 isRefreshing = false
  1. // 添加响应拦截器
  2. service.interceptors.response.use(
  3.   response => response, // 成功的响应直接返回
  4.   async error => {
  5.     const originalRequest = error.config;
  6.     //originalRequest._retry 是一个自定义属性,用于标记请求是否已经重试过。
  7.     //1、判断是不是token过期
  8.     if (error.response.status === 401 && !originalRequest._retry) {
  9.       originalRequest._retry = true; // 显式标记请求为重试
  10.       //2、并且不是重新获取token的401,则进行token刷新
  11.       if (!isRefreshing) {
  12.         isRefreshing = true;
  13.         // 重新请求access_token
  14.         try {
  15.           const accessToken = await refreshToken()
  16.           // 更新localstorage中的access_token
  17.           localStorage.setItem('access_token', accessToken);
  18.           //配置请求头
  19.           originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
  20.           // 处理队列中的其他请求
  21.           processQueue(null, accessToken);
  22.           // 重新发起失败的请求
  23.           return service(originalRequest)
  24.         } catch (err) {
  25.           // 处理队列中的请求
  26.           processQueue(err, null);
  27.           console.log("刷新token失败,跳转登录界面", err)
  28.           // 重定向到登录页
  29.           router.push('/login')
  30.         } finally {
  31.           isRefreshing = false;  //isRefreshing设置为false
  32.         }
  33.       }
  34.       else {
  35.         //如果正在刷新token,则将请求加入队列
  36.         return new Promise((resolve, reject) => {
  37.           failedQueue.push({ resolve, reject });
  38.         })
  39.       }
  40.     }
  41.     // 如果不是401错误,则直接抛出错误
  42.     return Promise.reject(error);
  43.   }
  44. );
复制代码
代码实现

  1. import axios from 'axios'const service = axios.create({  baseURL: 'https://api.example.com',  timeout: 10000})//是否正在刷新tokenlet isRefreshing = false;// 定义一个队列,用于存储失败的请求let failedQueue = [];// 处理惩罚队列中的请求const processQueue = (error, token = null) => {  failedQueue.forEach(prom => {    if (error) {      prom.reject(error); // 拒绝请求    } else {      prom.resolve(token);// 使用新的 token 重新发起请求    }  });  if (!error) {    failedQueue = []; // 只有在成功刷新 token 时才清空队列  }};//刷新token函数async function refreshToken() {  try {    const response = await service.get('/refresh', {      params: {        token: localStorage.getItem('refresh_token'),      },      timeout: 30000, // 单独设置超时时间    });    return response.data.accessToken; // 返回新的 access_token  } catch (error) {    // 清除本地存储的 token    localStorage.removeItem('access_token');    localStorage.removeItem('refresh_token');    throw error; // 抛堕落误  }}// 添加请求拦截器 service.interceptors.request.use(  config => {    if (config.url !== '/login') {      const accessToken = localStorage.getItem('access_token')      config.headers["Authorization"] = `Bearer ${accessToken}`    }    //判定是否是获取新token    if (config.url === '/refresh_token') {      const refreshToken = localStorage.getItem('refresh_token')      config.headers["Authorization"] = `Bearer ${refreshToken}`    }    return config  },  error => {    return Promise.reject(error)  })// 添加响应拦截器
  2. service.interceptors.response.use(
  3.   response => response, // 成功的响应直接返回
  4.   async error => {
  5.     const originalRequest = error.config;
  6.     //originalRequest._retry 是一个自定义属性,用于标记请求是否已经重试过。
  7.     //1、判断是不是token过期
  8.     if (error.response.status === 401 && !originalRequest._retry) {
  9.       originalRequest._retry = true; // 显式标记请求为重试
  10.       //2、并且不是重新获取token的401,则进行token刷新
  11.       if (!isRefreshing) {
  12.         isRefreshing = true;
  13.         // 重新请求access_token
  14.         try {
  15.           const accessToken = await refreshToken()
  16.           // 更新localstorage中的access_token
  17.           localStorage.setItem('access_token', accessToken);
  18.           //配置请求头
  19.           originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
  20.           // 处理队列中的其他请求
  21.           processQueue(null, accessToken);
  22.           // 重新发起失败的请求
  23.           return service(originalRequest)
  24.         } catch (err) {
  25.           // 处理队列中的请求
  26.           processQueue(err, null);
  27.           console.log("刷新token失败,跳转登录界面", err)
  28.           // 重定向到登录页
  29.           router.push('/login')
  30.         } finally {
  31.           isRefreshing = false;  //isRefreshing设置为false
  32.         }
  33.       }
  34.       else {
  35.         //如果正在刷新token,则将请求加入队列
  36.         return new Promise((resolve, reject) => {
  37.           failedQueue.push({ resolve, reject });
  38.         })
  39.       }
  40.     }
  41.     // 如果不是401错误,则直接抛出错误
  42.     return Promise.reject(error);
  43.   }
  44. );
  45. export default service;
复制代码
总结

Token 无感刷新是一种提升用户体验和保障应用安全的有效技术,有多种技术都能实现,但是各有优缺如下图:
方法实用场景长处缺点双 Token 机制通用场景安全性高,刷新频率低必要服务端支持双 Token前端定时刷新高活跃用户避免请求时忽然逾期必要定时器管理双token+并发请求锁机制高并发场景解决并发刷新问题实现较复杂服务端主动刷新服务端可控性强的场景客户端无需额外逻辑服务端压力较大
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

愛在花開的季節

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表