js实现无感刷新的实践(附前后端实现)
作者:接着奏乐接着舞。
无感刷新的核心思路:
无感刷新机制的目的是在用户不知情的情况下,自动更新其认证令牌(通常是Access Token),以保证用户的会话不会中断。这通常涉及到两种类型的令牌:
Access Token:它是用户进行认证后得到的令牌,允许用户访问服务器的受保护资源。它有一个较短的有效期。
Refresh Token:它是在同一时间发放给用户的另一个令牌,用于在Access Token过期时获取一个新的Access Token。它的有效期比Access Token长。
当Access Token即将过期或已经过期时,客户端会使用Refresh Token向认证服务器请求一个新的Access Token。如果Refresh Token仍然有效,认证服务器则发放一个新的Access Token给客户端,并且可能会同时发放一个新的Refresh Token。这个过程对用户来说是没有感知的,因此被称为“无感”刷新。
在项目中实施无感刷新:
后端实施步骤:
认证端点设置:
- 设计一个认证API端点,当用户初次登录时,返回Access Token和Refresh Token。
- 设计一个Token刷新API端点,只接受Refresh Token并返回新的Access Token(可选地返回新的Refresh Token)。
Token管理:
- Access Token应有一个短暂的生命周期,例如15分钟。
- Refresh Token应有一个长期的生命周期,例如7天或更长,且应该存储在一个安全的存储中。
安全考虑:
- 对Refresh Token进行旋转(每次使用后就废弃旧的Refresh Token并发放一个新的)。
- 通过HTTPS交换所有Token。
- 应用适当的加密措施来保护Token的安全。
前端实施步骤:
存储Token:
- 在客户端安全地存储Access Token和Refresh Token(例如使用Web的localStorage或SecureStorage)。
拦截请求和响应:
- 使用拦截器监视所有出站请求和进站响应。
- 在请求头中自动加入Access Token。
处理过期的Access Token:
- 当接收到表示Token过期的HTTP状态码(例如401)时,暂停发出的请求。
- 使用Refresh Token请求新的Access Token。
处理新的Access Token:
- 更新存储中的Access Token。
- 重新发送之前因Token过期而暂停的请求。
处理Refresh Token过期:
- 如果Refresh Token也过期或无效,引导用户重新登录。
无感刷新机制的大概思路就是这些,下面是具体的示例,分为简化版和完整版,简化版目的是更好的了解无感刷新的原理,而完整版就要考虑一些其他问题,比如说安全问题。
下面是实现无感刷新机制的具体示例(简化版)
这个例子涵盖前端(使用JavaScript)和后端(Node.js环境下使用Express框架)
后端(Node.js/Express)
const express = require('express'); const jwt = require('jsonwebtoken'); const app = express(); const accessTokenSecret = 'YOUR_ACCESS_TOKEN_SECRET'; const refreshTokenSecret = 'YOUR_REFRESH_TOKEN_SECRET'; let refreshTokens = []; app.post('/login', (req, res) => { // 用户登录逻辑,验证用户凭证 const { username, password } = req.body; // 这里应该有逻辑来验证用户凭证 // 如果验证成功: const accessToken = jwt.sign({ username }, accessTokenSecret, { expiresIn: '15m' }); const refreshToken = jwt.sign({ username }, refreshTokenSecret); refreshTokens.push(refreshToken); res.json({ accessToken, refreshToken }); }); app.post('/refresh', (req, res) => { // 用户发送refresh token来获取新的access token const { refreshToken } = req.body; if (!refreshToken || !refreshTokens.includes(refreshToken)) { return res.sendStatus(403); } jwt.verify(refreshToken, refreshTokenSecret, (err, user) => { if (err) { return res.sendStatus(403); } const newAccessToken = jwt.sign({ username: user.username }, accessTokenSecret, { expiresIn: '15m' }); res.json({ accessToken: newAccessToken }); }); }); app.listen(3000, () => { console.log('Authentication service started on port 3000'); });
前端(JavaScript/AJAX)
假设使用了axios
作为HTTP客户端,可以设置拦截器来自动处理Token刷新。
axios.interceptors.response.use(response => { return response; }, error => { const originalRequest = error.config; if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; return axios.post('/refresh', { refreshToken: localStorage.getItem('refreshToken') }).then(res => { if (res.status === 200) { localStorage.setItem('accessToken', res.data.accessToken); axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('accessToken'); return axios(originalRequest); } }); } return Promise.reject(error); });
这段代码中,我们在响应拦截器中检查任何错误的响应。如果我们得到一个401响应,并且这是第一次重试,我们就发送一个带有refreshToken
的请求到/refresh
端点。如果刷新Token成功,我们就保存新的accessToken
,更新请求头,并重新发起失败的请求。
完整示例
实现非简化的登录无感刷新机制通常会涉及更详细的认证流程、错误处理、日志记录和安全性措施。这包括使用数据库存储Refresh Tokens、自动撤销机制、双因素认证等。
后端实现(Node.js/Express + MongoDB)
首先,你需要设置一个数据库来存储Refresh Tokens。出于安全考虑,每个Refresh Token都应该与一个用户账户关联,并且能够被追踪和撤销。
const express = require('express'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const User = require('./models/User'); // 导入User模型 const RefreshToken = require('./models/RefreshToken'); // 导入RefreshToken模型 const app = express(); // ...其他必要的中间件和数据库连接代码... app.post('/login', async (req, res) => { // 用户登录逻辑,验证用户凭证 const { username, password } = req.body; const user = await User.findOne({ username }); if (user && bcrypt.compareSync(password, user.password)) { const accessToken = jwt.sign({ userId: user._id }, accessTokenSecret, { expiresIn: '15m' }); const refreshToken = jwt.sign({ userId: user._id }, refreshTokenSecret); // 保存refresh token到数据库 const newRefreshToken = new RefreshToken({ token: refreshToken, user: user._id }); await newRefreshToken.save(); res.json({ accessToken, refreshToken }); } else { res.status(401).send('Username or password incorrect'); } }); app.post('/refresh', async (req, res) => { // 用户发送refresh token来获取新的access token const { refreshToken } = req.body; const dbToken = await RefreshToken.findOne({ token: refreshToken }); if (!dbToken) { return res.status(403).send('Refresh token not found'); } jwt.verify(refreshToken, refreshTokenSecret, async (err, decoded) => { if (err) { return res.status(403).send('Refresh token invalid'); } // 生成新的access token和refresh token const newAccessToken = jwt.sign({ userId: decoded.userId }, accessTokenSecret, { expiresIn: '15m' }); const newRefreshToken = jwt.sign({ userId: decoded.userId }, refreshTokenSecret); // 更新数据库中的refresh token dbToken.token = newRefreshToken; await dbToken.save(); res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken }); }); }); app.listen(3000, () => { console.log('Authentication service started on port 3000'); });
在此示例中,我们使用了MongoDB来存储用户和他们的Refresh Tokens。登录时,我们验证用户凭证,并且如果认证成功,我们就生成Access Token和Refresh Token,然后将Refresh Token保存到数据库。在无感刷新流程中,我们验证提供的Refresh Token是否存在于数据库中,并且是否有效,然后发放新的Access Token。
前端实现(JavaScript/AJAX + Local Storage)
在前端实现中,我们需要确保存储Token的方法是安全的。在生产环境中,你可能需要考虑使用更安全的存储方式,如HTTPOnly Cookies或Secure Local Storage。
axios.interceptors.response.use( response => response, error => { const originalRequest = error.config; // 检测token过期的错误代码,比如401 if (error.response.status === 401 && originalRequest.url === '/refresh') { // 刷新Token失败,直接登出用户 logoutUser(); return Promise.reject(error); } if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; return axios.post('/refresh', { refreshToken: localStorage.getItem('refreshToken') }) .then(res => { if (res.status === 200) { // 将新token设置到本地存储和axios默认头部 localStorage.setItem('accessToken', res.data.accessToken); localStorage.setItem('refreshToken', res.data.refreshToken); axios.defaults.headers.common['Authorization'] = 'Bearer ' + res.data.accessToken; // 更新失败请求的头部并重新发送 originalRequest.headers['Authorization'] = 'Bearer ' + res.data.accessToken; return axios(originalRequest); } }) .catch(error => { // 任何错误都直接登出用户 logoutUser(); return Promise.reject(error); }); } return Promise.reject(error); } ); function logoutUser() { // 清除本地存储和状态,重定向到登录页面 localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); // ...重定向到登录页的代码... }
在前端的实现中,我们创建了一个axios拦截器,它会在遇到401未授权的响应时自动尝试刷新Token。如果刷新Token请求也失败,则触发用户登出的逻辑。
注意:此功能一定要注意其安全性,具体的话要结合实际的项目,以下是我的一些建议:
安全方面通常包括以下几个方法:
1. 使用HTTPS来加密所有的通信。
2. 安全地存储Access Token和Refresh Token,如使用httpOnly和Secure属性的Cookies。
3. 对Token进行定期轮换,特别是Refresh Token。
4. 在服务器端验证Token的签名。
5. 设置适当的Token过期时间。
6. 实现Token撤销逻辑,以便在检测到异常时能够立即废弃使用。
7. 避免在客户端暴露敏感的认证逻辑。
8. 对所有的认证请求和Token刷新请求进行率限制和异常监测。
9. 使用跨站请求伪造(CSRF)保护措施。
10. 确保客户端和服务端都有充分的错误处理和日志记录机制。
到此这篇关于js实现无感刷新的实践(附前后端实现)的文章就介绍到这了,更多相关js 无感刷新内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!