JWT登录鉴权与无感刷新:构建现代Web应用的安全基石
信息传递的"身份证"
在Web开发中,身份认证就像发放"身份证"——用户登录后,服务器需要通过某种方式记住用户的身份。传统的Cookie+Session方案虽然可靠,但在前后端分离架构中显得笨重。而 JWT(JSON Web Token) 就像一张轻便的电子身份证,它通过加密签名的方式,让客户端和服务器之间能够安全地传递身份信息。
Token通常被理解为令牌,也可以说通关文牒
一、JWT的三重奏:Header.Payload.Signature
JWT由三部分组成,用点号分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY7
.
HmacSHA256加密后的签名
- Header:声明算法(如HS256)和令牌类型
- Payload:核心数据(用户ID、角色、过期时间等)
- Signature:使用密钥对Header和Payload的签名
想象这是一张盖了钢印的身份证:Header是纸张材质说明,Payload是个人信息,Signature是防伪钢印。任何修改都会让钢印失效!
二、登录鉴权的完整流程
1. 用户登录:获取双Token-- access token和refresh token
// 后端(Node.js示例)
// 定义JWT生成Token的规则函数
function sign (options, duration) {
return jwt.sign(
options,
process.env.ACCESS_SECRET, // 加密口令,可以使Token变得难以破解
{expiresIn: duration, // 过期时间}
)
}
app.post('/login', async (ctx) => {
// 1. 获取请求体中的数据
// POST请求携带的参数都在ctx.request该请求体中
const { username, password } = ctx.request.body;
const user = await validateUser(username, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = sign(
{ userId: user.id, username: user.username },
{ expiresIn: '15m' }
);
const refreshToken = sign(
{ userId: user.id },
{ expiresIn: '7d' }
);
ctx.body({
accessToken,
refreshToken,
user: { id: user.id, username: user.username }
});
});
2. 前端存储Token -- 储存起来以便随时给后端发送鉴定权限
// 登录成功后存储
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
🔒 安全建议:
refreshToken建议使用HttpOnly Cookie存储,防止XSS攻击。
3. 请求拦截器:自动携带Token
// Axios请求拦截器--用于处理请求数据
axios.interceptors.request.use(request => {
// 从浏览器的本地储存中获取token
const access_token = localStorage.getItem('access_token')
// 如果token存在,则在请求头中添加Authorization字段
if (access_token) {
request.headers.Authorization = access_token
}
return request // 请求放行
})
三、无感刷新:让用户体验丝滑如流水
1. 双Token设计原理
- Access Token:短时效(15分钟),用于日常接口调用
- Refresh Token:长时效(7天),用于换取新Access Token
在 Access Token 过期时校验 Refresh Token
- 若
Refresh Token没过期,则前端向后端发送一个刷新Token的请求,做到权限长期有效即只要用户不玩长期失踪,这个有效期就一直延长 - 若两个
Token双双过期,则需要重新获取新的Token
想象这是健身房的会员卡:Access Token是每日签到卡(每天重新激活),Refresh Token是年卡(长期有效但需要定期续费)。
2. 刷新Token的流程
// 刷新Token接口
app.post('/refresh-token', async (ctx) => {
const { refreshToken } = ctx.request.body;
// 解析校验refresh_token是否有效
const decoded = refreshVerify(refresh_token)
if (decoded.id) {
// 创建新的 长短 token
// console.log(decoded);
const access_token = sign(data, '1h')
const refresh_token = sign(data, '7d')
ctx.body = {
code: '1',
msg: 'token刷新成功',
access_token: access_token,
refresh_token: refresh_token,
}
} else { // 长 token 也过期了
ctx.status = 416
ctx.body = {
code: '0',
msg: '登录状态已过期,请重新登录',
}
}
})
3. 前端自动刷新实现
// Axios响应拦截器
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 如果是401错误且未重试过
if (error.response?.status === 401 && !originalRequest._retry) {
// 记录未成功的请求
const originalRequest = error.config
// 重新请求新的 access_token 和 refresh_token
const refresh_token = localStorage.getItem('refresh_token')
if (refresh_token) {
axios.post('/user/refresh', {
refresh_token: refresh_token,
}).then(res => {
if (res.code === '1') {
// 成功获取新的 token
localStorage.setItem('access_token', res.access_token)
localStorage.setItem('refresh_token', res.refresh_token)
// 更改原请求的 Authorization 头
originalRequest.headers.Authorization = res.access_token
// 重新发送原请求
return axios(originalRequest)
}
})
}
}
if (status === 416) {
toast.error(error.response.data.msg)
// 可以在这里处理跳转到登录页
setTimeout(() => {
window.location.href = '/login'
}, 800)
}
} else {
// 网络错误或其他错误
toast.error('网络连接失败')
}
return Promise.reject(error)
},
)
⚠️ 注意事项:需要防重试标识
_retry防止无限递归,同时处理刷新失败的边界情况。
四、实战中的优化技巧
1. Token有效期策略
// 推荐配置
const accessTokenTTL = '15m'; // 15分钟
const refreshTokenTTL = '7d'; // 7天
const refreshBefore = '5m'; // 提前5分钟刷新
2. 安全增强措施
- 使用HTTPS传输Token
- 设置
SameSite=StrictCookie属性 - 对敏感操作增加二次验证(如短信验证码)
3. 刷新Token的优雅降级
// 当同时收到多个401请求时
let isRefreshing = false;
let refreshSubscribers = [];
function onAccessTokenRefreshed(token) {
refreshSubscribers.forEach(callback => callback(token));
refreshSubscribers = [];
}
axios.interceptors.response.use(null, error => {
if (error.response?.status === 401) {
if (!isRefreshing) {
isRefreshing = true;
axios.post('/refresh-token', { refreshToken })
.then(({ data }) => {
localStorage.setItem('accessToken', data.accessToken);
axios.defaults.headers.common['Authorization'] = `Bearer ${data.accessToken}`;
onAccessTokenRefreshed(data.accessToken);
})
.catch(() => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
})
.finally(() => isRefreshing = false);
}
return new Promise(resolve => {
refreshSubscribers.push((token) => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
resolve(axios(originalRequest));
});
});
}
});
五、总结:打造无缝衔接的安全体验
通过JWT双Token机制,我们实现了:
- 无状态认证:服务器无需存储会话信息
- 跨域支持:天然适应微服务架构
- 无感刷新:用户无需频繁登录
- 安全性保障:通过加密签名防止篡改
🚀 进阶建议:可以结合OAuth 2.0协议,将认证授权与业务逻辑解耦,进一步提升系统的可扩展性。
在现代Web开发中,JWT已成为身份认证的标配。通过合理的设计和实现,我们既能保障系统安全,又能为用户提供流畅的操作体验。记住:安全性和用户体验从来不是对立面,而是可以通过巧妙设计达到平衡的两个维度。