下面介绍几种刷新token的实现方案:
- 基于 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;
- 登录和刷新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;
- 完整的前端实现:
// 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();
}
});
- 使用范例:
// 登录
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);
}
}
-
token刷新策略建议:
-
合理的过期时间:
- Access Token: 15分钟到1小时
- Refresh Token: 7天到30天
- 刷新时机:
- 主动刷新:在Access Token过期前刷新
- 被动刷新:收到401响应时刷新
- 并发请求处理:
- 使用请求队列
- 避免重复刷新
- 刷新完成后重试失败请求
- 安全考虑:
- 使用HTTPS
- 存储在安全位置
- 实现token轮换
- 设置适当的作用域
- 错误处理:
- 刷新失败处理
- 重试机制
- 异常状态恢复
评论区