实现无感刷新 Token:提升用户登录体验的技术方案

628 阅读3分钟

Token的无感刷新是一种在用户不感知的情况下自动刷新访问令牌(Access Token)的技术,以避免因Token过期而导致的用户重新登录或页面刷新。这种机制通常依赖于双Token机制,即使用一个短期的Access Token和一个长期的Refresh Token。

  1. 双Token机制
    • Access Token:用于访问受保护的资源,通常具有较短的过期时间(如30分钟),每次请求时需要携带此Token。
    • Refresh Token:用于在Access Token过期后获取新的Access Token,通常具有较长的过期时间(如几天或几周),不需要频繁更换。
  2. 无感刷新实现原理
    • 当客户端检测到即将过期的Access Token时,会自动向服务器请求新的Access Token。
    • 客户端携带Refresh Token向服务器请求新的Access Token。
    • 如果服务器验证刷新成功,它会返回新的Access Token和可能更新的Refresh Token。
    • 客户端更新本地存储的Token,并继续使用新的Access Token进行后续请求,从而实现无感知的Token刷新。
  3. 实现步骤
    • 前端实现:在前端应用中,通过监听Token即将过期的时间点,自动触发刷新操作。例如,在Vue中可以使用axios拦截器来捕获Token过期的情况,并自动刷新Token。
  4. 优化与注意事项
    • 避免频繁刷新:可以通过设置全局变量来控制Token刷新的频率,确保只有在Token即将过期时才进行刷新,避免不必要的请求。
    • 安全性:确保Refresh Token的安全性,防止其被泄露或滥用。例如,可以限制其使用次数或设置较短的过期时间。

实现Token无感刷新的详细步骤与代码示例:

1. 定义常量

首先,定义一些常量用于存储Token和HTTP请求头字段。

// config/constant.js
export const ACCESS_TOKEN = "s_tk"; // 短token
export const REFRESH_TOKEN = "l_tk"; // 长token
export const AUTH = "Authorization"; // 存放短token
export const PASS = "PASS"; // 存放长token

2. 定义返回码

定义一些返回码用于处理不同的业务逻辑。

// config/returnCodeMap.js
export const CODE_LOGGED_OTHER = 106; // 在其它客户端被登录
export const CODE_RELOGIN = 108; // 重新登陆
export const CODE_TOKEN_EXPIRED = 104; // token过期
export const CODE_SUCCESS = 0; // 接口请求成功

3. 封装Axios服务

封装Axios服务,添加请求拦截器和响应拦截器。

// service/index.js
import axios from "axios";
import { refreshAccessToken, addSubscriber } from "./refresh";
import { clearAuthAndRedirect } from "./clear";
import { CODE_LOGGED_OTHER, CODE_RELOGIN, CODE_TOKEN_EXPIRED, CODE_SUCCESS } from "../config/returnCodeMap";
import { ACCESS_TOKEN, AUTH } from "../config/constant";

const service = axios.create({
  baseURL: "//127.0.0.1:4000",
  timeout: 30000,
});

service.interceptors.request.use((config) => {
  let { headers } = config;
  const s_tk = localStorage.getItem(ACCESS_TOKEN);
  s_tk && Object.assign(headers, { [AUTH]: s_tk });
  return config;
}, (error) => {
  return Promise.reject(error);
});

service.interceptors.response.use((response) => {
  let { config, data } = response;
  // retry: 第一次请求过期,接口调用refreshAccessToken,第二次重新请求,还是过期则reject出去
  let { retry } = config;
  return new Promise((resolve, reject) => {
    if (data["returncode"] !== CODE_SUCCESS) {
      if ([CODE_LOGGED_OTHER, CODE_RELOGIN].includes(data.returncode)) {
        clearAuthAndRedirect();
      } else if (data["returncode"] === CODE_TOKEN_EXPIRED && !retry) {
        config.retry = true;
        addSubscriber(() => resolve(service(config)));
        refreshAccessToken();
      } else {
        return reject(data);
      }
    } else {
      resolve(data);
    }
  });
}, (error) => {
  return Promise.reject(error);
});

export default service;

4. 刷新Token逻辑

实现刷新Token的逻辑,并处理多个请求同时触发Token刷新的情况。

// service/refresh.js
let pending = false;
let subscribers = [];

const onAccessTokenFetched = (accessToken) => {
  subscribers.forEach(callback => callback(accessToken));
  subscribers = [];
};

const addSubscriber = (callback) => {
  subscribers.push(callback);
};

export const refreshAccessToken = async () => {
  if (!pending) {
    try {
      pending = true;
      const l_tk = localStorage.getItem(REFRESH_TOKEN);
      if (l_tk) {
        // 重新获取短token
        const { accessToken } = await service.get("/refresh", Object.assign({}, { headers: { [PASS]: l_tk } }));
        localStorage.setItem(ACCESS_TOKEN, accessToken);
        onAccessTokenFetched(accessToken);
      }
    } catch (e) {
      clearAuthAndRedirect();
    } finally {
      pending = false;
    }
  }
};

export { addSubscriber };

5. 清除Token并重定向到登录页

当Token过期或被其他客户端登录时,清除Token并重定向到登录页。

// service/clear.js
import { ACCESS_TOKEN } from '../config/constant';

export const clearAuthAndRedirect = () => {
  localStorage.removeItem(ACCESS_TOKEN);
  window.location.href = '/login';
};

Token的无感刷新技术通过合理利用双Token机制,实现了在用户不感知的情况下自动刷新访问令牌,从而提升了用户体验并减少了因Token过期导致的重复登录问题。