下面介绍几种刷新token的实现方案:

  1. 基于 Axios 的请求拦截器实现:
// auth.js - Token管理类
class TokenService {
  constructor() {
    this.accessToken = localStorage.getItem('accessToken');
    this.refreshToken = localStorage.getItem('refreshToken');
    this.refreshRequest = null; // 存储刷新请求
  }

  // 保存token
  setTokens(accessToken, refreshToken) {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', refreshToken);
  }

  // 清除token
  clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  }

  // 刷新token
  async refreshTokens() {
    try {
      // 确保同时只有一个刷新请求
      if (!this.refreshRequest) {
        this.refreshRequest = axios.post('/api/auth/refresh', {
          refreshToken: this.refreshToken
        });
      }

      const response = await this.refreshRequest;
      const { accessToken, refreshToken } = response.data;
      this.setTokens(accessToken, refreshToken);
      
      // 重置刷新请求
      this.refreshRequest = null;
      return accessToken;
    } catch (error) {
      this.clearTokens();
      // 刷新失败,需要重新登录
      window.location.href = '/login';
      return Promise.reject(error);
    }
  }
}

const tokenService = new TokenService();

// http.js - Axios实例配置
const http = axios.create({
  baseURL: process.env.VUE_APP_API_URL,
  timeout: 10000
});

// 请求拦截器
http.interceptors.request.use(
  config => {
    const token = tokenService.accessToken;
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

// 响应拦截器
http.interceptors.response.use(
  response => response.data,
  async error => {
    const originalRequest = error.config;

    // 如果是401错误且不是刷新token的请求
    if (error.response?.status === 401 && 
        !originalRequest._retry &&
        !originalRequest.url.includes('/auth/refresh')) {
      originalRequest._retry = true;

      try {
        // 刷新token
        const newToken = await tokenService.refreshTokens();
        // 更新原始请求的token
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        // 重试原始请求
        return http(originalRequest);
      } catch (refreshError) {
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default http;
  1. 登录和刷新Token的接口实现 (Node.js + Express):
// auth.controller.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

class AuthController {
  constructor() {
    // 用于存储刷新token,实际项目中应使用Redis
    this.refreshTokens = new Map();
  }

  // 生成token
  generateTokens(userId) {
    // 访问token,短期有效
    const accessToken = jwt.sign(
      { userId },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    // 刷新token,长期有效
    const refreshToken = crypto.randomBytes(40).toString('hex');
    const refreshTokenExp = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7天

    // 存储刷新token
    this.refreshTokens.set(refreshToken, {
      userId,
      expires: refreshTokenExp
    });

    return { accessToken, refreshToken };
  }

  // 登录
  async login(req, res) {
    try {
      const { username, password } = req.body;
      
      // 验证用户名密码(示例)
      const user = await UserModel.findOne({ username });
      if (!user || !user.comparePassword(password)) {
        return res.status(401).json({
          message: 'Invalid credentials'
        });
      }

      // 生成token
      const tokens = this.generateTokens(user.id);

      res.json({
        ...tokens,
        user: {
          id: user.id,
          username: user.username
        }
      });
    } catch (error) {
      res.status(500).json({
        message: 'Login failed'
      });
    }
  }

  // 刷新token
  async refreshToken(req, res) {
    try {
      const { refreshToken } = req.body;
      
      // 验证刷新token
      const tokenData = this.refreshTokens.get(refreshToken);
      if (!tokenData || tokenData.expires < Date.now()) {
        return res.status(401).json({
          message: 'Invalid refresh token'
        });
      }

      // 删除旧的刷新token
      this.refreshTokens.delete(refreshToken);

      // 生成新token
      const tokens = this.generateTokens(tokenData.userId);

      res.json(tokens);
    } catch (error) {
      res.status(500).json({
        message: 'Token refresh failed'
      });
    }
  }

  // 登出
  logout(req, res) {
    try {
      const { refreshToken } = req.body;
      // 删除刷新token
      this.refreshTokens.delete(refreshToken);
      
      res.json({ message: 'Logged out successfully' });
    } catch (error) {
      res.status(500).json({
        message: 'Logout failed'
      });
    }
  }
}

// auth.routes.js
const router = express.Router();
const authController = new AuthController();

router.post('/login', authController.login.bind(authController));
router.post('/refresh', authController.refreshToken.bind(authController));
router.post('/logout', authController.logout.bind(authController));

export default router;
  1. 完整的前端实现:
// auth.store.js - Vuex store
import http from '@/utils/http';

const state = {
  user: null,
  accessToken: null,
  refreshToken: null
};

const mutations = {
  SET_USER(state, user) {
    state.user = user;
  },
  SET_TOKENS(state, { accessToken, refreshToken }) {
    state.accessToken = accessToken;
    state.refreshToken = refreshToken;
  },
  CLEAR_AUTH(state) {
    state.user = null;
    state.accessToken = null;
    state.refreshToken = null;
  }
};

const actions = {
  // 登录
  async login({ commit }, credentials) {
    try {
      const response = await http.post('/auth/login', credentials);
      const { user, accessToken, refreshToken } = response;
      
      commit('SET_USER', user);
      commit('SET_TOKENS', { accessToken, refreshToken });
      
      // 存储token
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);
      
      return user;
    } catch (error) {
      throw new Error('Login failed');
    }
  },

  // 登出
  async logout({ commit, state }) {
    try {
      await http.post('/auth/logout', {
        refreshToken: state.refreshToken
      });
    } finally {
      // 清除状态和本地存储
      commit('CLEAR_AUTH');
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
    }
  },

  // 检查并恢复认证状态
  async checkAuth({ commit }) {
    const accessToken = localStorage.getItem('accessToken');
    const refreshToken = localStorage.getItem('refreshToken');

    if (accessToken && refreshToken) {
      try {
        // 验证token
        const user = await http.get('/auth/verify');
        
        commit('SET_USER', user);
        commit('SET_TOKENS', { accessToken, refreshToken });
        
        return true;
      } catch (error) {
        // token无效,清除状态
        commit('CLEAR_AUTH');
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
      }
    }
    
    return false;
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

// router.js - 路由守卫
router.beforeEach(async (to, from, next) => {
  const store = useStore();
  
  // 检查是否需要认证
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 检查认证状态
    const isAuthenticated = await store.dispatch('auth/checkAuth');
    
    if (!isAuthenticated) {
      // 未认证,重定向到登录页
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      });
    } else {
      next();
    }
  } else {
    next();
  }
});
  1. 使用范例:
// 登录
async function handleLogin() {
  try {
    await store.dispatch('auth/login', {
      username: this.username,
      password: this.password
    });
    
    // 登录成功,跳转到首页
    const redirect = this.$route.query.redirect || '/';
    this.$router.push(redirect);
  } catch (error) {
    this.error = 'Login failed';
  }
}

// 登出
async function handleLogout() {
  try {
    await store.dispatch('auth/logout');
    this.$router.push('/login');
  } catch (error) {
    console.error('Logout failed:', error);
  }
}
  1. token刷新策略建议:

  2. 合理的过期时间:

  • Access Token: 15分钟到1小时
  • Refresh Token: 7天到30天
  1. 刷新时机:
  • 主动刷新:在Access Token过期前刷新
  • 被动刷新:收到401响应时刷新
  1. 并发请求处理:
  • 使用请求队列
  • 避免重复刷新
  • 刷新完成后重试失败请求
  1. 安全考虑:
  • 使用HTTPS
  • 存储在安全位置
  • 实现token轮换
  • 设置适当的作用域
  1. 错误处理:
  • 刷新失败处理
  • 重试机制
  • 异常状态恢复