在前后端分离的项目中,Token 认证通常采用 JWT(JSON Web Token)机制来实现用户的身份认证。为了确保用户体验良好、安全性高,并且避免用户频繁登录,需要合理设计 Token 的时效及更新机制。
前后端分离的项目通常会使用两个 Token:
Authorization
头部携带访问 Token 进行身份认证。用户登录时生成两个 Token:
客户端存储 Token:
Authorization
头部传递给后端。使用访问 Token 进行身份验证:
Authorization: Bearer <token>
中发送给后端。访问 Token 过期时:
刷新 Token 更新访问 Token:
刷新 Token 过期处理:
+-------------+ +-------------+ +--------------------+
| Client | | Backend | | Authentication Flow |
+-------------+ +-------------+ +--------------------+
| | |
| Login Request | |
| ------------------------> | |
| | Verify User |
| | ------------------------> |
| | |
| | Return Access & Refresh |
| <------------------------| Token |
| | |
| Store Access Token in | |
| memory, Refresh Token | |
| in HTTP-Only Cookie | |
| | |
| Send API Requests with | |
| Access Token | |
| ------------------------> | Verify Access Token |
| | |
| | |
| | [Access Token Expired] |
| | <-------------------------|
| | |
| [Request New Access | |
| Token using Refresh Token]| |
| ------------------------> | Verify Refresh Token |
| | |
| | Generate New Access Token |
| | <-------------------------|
| | |
| [Update Access Token] | |
| | |
| [Refresh Token Expired] | |
| <-------------------------| Return 401, Re-login |
| | |
下面是一个实现 Token 自动刷新机制 的 Vue.js 和 Axios 配置代码:
import axios from 'axios';
// 创建 Axios 实例
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
});
// 请求拦截器,携带 Access Token
api.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem('accessToken'); // 或从 Vuex 中获取
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器,处理 Token 过期的情况
api.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = getCookie('refreshToken'); // 从 HTTP-Only Cookie 获取 Refresh Token
if (refreshToken) {
try {
const { data } = await api.post('/auth/refresh-token', { refreshToken });
localStorage.setItem('accessToken', data.accessToken); // 更新 Access Token
originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
return api(originalRequest); // 重新发起原始请求
} catch (refreshError) {
console.log('Refresh token expired, please re-login');
// 跳转到登录页
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);
export default api;
以下是使用 jsonwebtoken
来生成和验证 Token 的示例:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const ACCESS_TOKEN_SECRET = 'your-access-token-secret';
const REFRESH_TOKEN_SECRET = 'your-refresh-token-secret';
let refreshTokens = []; // 存储有效的刷新 Token
// 登录接口
app.post('/login', (req, res) => {
const username = req.body.username;
// 验证用户身份,略...
const accessToken = jwt.sign({ username }, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ username }, REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
refreshTokens.push(refreshToken);
res.json({
accessToken,
refreshToken,
});
});
// 刷新 Token 接口
app.post('/auth/refresh-token', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.sendStatus(403);
}
jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
const accessToken = jwt.sign({ username: user.username }, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
res.json({ accessToken });
});
});
// 保护路由示例
app.get('/protected', authenticateToken, (req, res) => {
res.send('This is a protected route');
});
// 中间件验证 Access Token
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
避免 XSS 攻击。