ToB企服应用市场:ToB评测及商务社交产业平台

标题: Token 无感刷新:打造无缝用户体验与坚实安全防线 [打印本页]

作者: 愛在花開的季節    时间: 昨天 07:40
标题: Token 无感刷新:打造无缝用户体验与坚实安全防线
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 无感刷新技术实现

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

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


代码实现

  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,避免用户请求时忽然逾期。
代码实现

  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。
代码实现

  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

2. 请求拦截器:设置请求头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 错误)处理惩罚
  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企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4