node.js实现双Token+Cookie存储+无感刷新机制的示例
作者:别看我只是一只杨女士1
本文主要介绍了Node.js双Token机制,以短期AccessToken和长期RefreshToken提升安全与体验,结合Cookie存储与自动刷新,实现无感登录及多设备管理,感兴趣的可以了解一下
为什么要实施双token机制?
优点 | 描述 |
---|---|
安全性 | Access Token 短期有效,降低泄露风险;Refresh Token 权限受限,仅用于获取新 Token |
用户体验 | 用户无需频繁重新登录,Token 自动刷新过程对用户透明 |
灵活性 | 独立控制不同 Token 的生命周期,适应各种场景需求 |
可管理性 | 支持多设备登录管理,便于撤销特定设备的登录状态 |
性能优化 | 减少数据库查询次数,提升系统响应速度 |
实现方案:
模块 | 实现方式 |
---|---|
登录接口 | 返回 accessToken 和 refreshToken ,分别存入 Cookie |
Access Token | 短时效 JWT,用于请求鉴权 |
Refresh Token | 长时效 JWT,用于刷新 Access Token |
Token 校验方式 | 后端从 Cookie 中读取 token (即 Access Token) |
前端 Axios | 使用响应拦截器统一处理 Token 失效和自动刷新 |
- 使用 JWT 生成两个 Token:
- Access Token(短时效):用于接口认证,例如有效期为 15 分钟
- Refresh Token(长时效):用于刷新 Access Token,例如有效期为 7 天
- 在用户登录时返回这两个 Token,并将 Refresh Token 存储在数据库中
- 当 Access Token 过期后,客户端使用 Refresh Token 请求新的 Access Token
- 如果 Refresh Token 也过期或无效,则强制重新登录
具体代码实现
1. 安装依赖:
cookie-parser用来解析 Cookie 中的 Token
npm install jsonwebtoken bcryptjs cookie-parser
2. 数据库添加两个字段
refresh_token | VARCHAR(255) | 加密后的 RefreshToken |
---|---|---|
expires_at | DATETIME | RefreshToken 过期时间 |
3. 在后端cors跨域中间中添加属性
// 将cors注册为全局中间件 app.use(cors({ origin: 'http://localhost:5173', // 前端地址 credentials: true // 👈 允许携带凭证(cookies) }))
3. 登录逻辑改造(添加双token)
- 添加配置文件config.js
module.exports = { jwtSecretKey: 'yke;eky1]239_jwt87-2up34', refreshTokenSecretKey: 'yke;eky1]239_refresh87-2up34', accessExpiresIn: '15m', // 访问令牌有效期 refreshExpiresIn: '7d', // 刷新令牌有效期 accessExpiresInSec: 15 * 60, // 秒数 refreshExpiresInSec: 7 * 24 * 60 * 60 // 秒数 }
- jwt生成accessToken访问token、refreshToken刷新token
// 生成access token const accessToken = jwt.sign( { id: user.id, username: user.username, email: user.email }, config.jwtSecretKey, { expiresIn: config.accessExpiresIn } ) // 生成refresh token const refreshToken = jwt.sign( { id: user.id, username: user.username, email: user.email }, config.refreshTokenSecretKey, { expiresIn: config.refreshExpiresIn } )
- 生成token过期时间,和refreshToken一起存入数据库
const expiresAt = new Date() expiresAt.setSeconds(expiresAt.getSeconds() + config.refreshExpiresInSec)
- 将accessToken访问token、refreshToken刷新token存入cookie
// 设置cookie res.cookie('token', accessToken, { maxAge: config.accessExpiresInSec * 1000, httpOnly: true, secure: true, path: '/' }) res.cookie('refresh_token', refreshToken, { maxAge: config.refreshExpiresInSec * 1000, httpOnly: true, secure: true, path: '/api/user/refresh-token', // 限制路径提高安全性 sameSite: 'none' })
登录逻辑完整代码:
// 用户登录的处理函数 exports.login = (req, res) => { // 接收表单数据 const userInfo = req.body console.log(userInfo) // 查询用户信息 const sqlStr_name = 'select * from user where username=?' db.query(sqlStr_name, [userInfo.username], (err, results) => { if (err) { return res.send({ status: 1, message: err }) } // 执行sql语句成功,但是获取的条数不等于1 if (results.length === 0) { return res.send({ status: 1, message: '该用户不存在' }) } // 判断密码是否正确 const cmpresult = bcrypt.compareSync(userInfo.password, results[0].password) if (!cmpresult) { return res.send({ status: 1, message: '密码错误' }) } // 在服务器端生成Token字符串 const user = { ...results[0] } // 生成access token const accessToken = jwt.sign( { id: user.id, username: user.username, email: user.email }, config.jwtSecretKey, { expiresIn: config.accessExpiresIn } ) // 生成refresh token const refreshToken = jwt.sign( { id: user.id, username: user.username, email: user.email }, config.refreshTokenSecretKey, { expiresIn: config.refreshExpiresIn } ) // 将refresh token存储到数据库中 const expiresAt = new Date() expiresAt.setSeconds(expiresAt.getSeconds() + config.refreshExpiresInSec) const sqlStr_refreshToken = 'update user set refresh_token=?, expires_at=? where id=?' db.query(sqlStr_refreshToken, [refreshToken, expiresAt, user.id], (err) => { if (err) { console.error('保存refreshToken失败:', err) return res.send({ status: 1, message: '保存refreshToken失败' }) } // 设置cookie res.cookie('token', accessToken, { maxAge: config.accessExpiresInSec * 1000, httpOnly: true, secure: true, path: '/' }) res.cookie('refresh_token', refreshToken, { maxAge: config.refreshExpiresInSec * 1000, httpOnly: true, secure: true, path: '/api/user/refresh-token', // 限制路径提高安全性 sameSite: 'none' }) res.send({ status: 0, message: '登录成功', data: { username: results[0].username } }) }) }) }
4. 实现token刷新接口
创建新路由/refreshToken
// token刷新接口 exports.refreshToken = (req, res) => { // 直接从cookie中获取刷新token => 前端不需要再单独把token传入请求头 const refreshToken = req.cookies.refresh_token // 判断refresh token是否存在 if (!refreshToken) { return res.send({ status: 1, message: '缺少refreshToken,请先登录' }) } try { // 验证refreshToken const decoded = jwt.verify(refreshToken, config.refreshTokenSecretKey) // 查询用户是否存在且refreshToken匹配 const sql = 'select * from user where id=? and refresh_token=?' db.query(sql, [decoded.id, refreshToken], (err, results) => { if (err) { return res.send({ status: 1, message: '无效的refreshToken' + err.message }) } const user = results[0] // 生成新的access token const accessToken = jwt.sign( { id: user.id, username: user.username, email: user.email }, config.jwtSecretKey, { expiresIn: config.accessExpiresIn } ) // 更新accessToken到Cookie res.cookie('token', accessToken, { maxAge: config.accessExpiresInSec * 1000, httpOnly: true, secure: true, path: '/' }) res.send({ status: 0, message: 'accessToken刷新成功', data: { token: accessToken } }) }) } catch (error) { return res.status(403).send({ status: 1, message: 'token已过期,请重新登录' }) } }
5. 响应拦截器中处理token
import axios from 'axios' import { message } from 'antd' import { refreshTokenService } from '@/api/user' const instance = axios.create({ baseURL: 'http://localhost:3333', // 你的API服务器地址 timeout: 10000, // 请求超时时间 headers: { 'Content-Type': 'application/json' }, // 必须加上这个选项才能跨域携带 withCredentials: true }) // 添加请求拦截器 instance.interceptors.request.use( (config) => { // 后端将token存在了cookie中,这里不需要携带token return config }, (err) => Promise.reject(err) ) // 标记是否正在刷新 Token(防止并发刷新) let isRefreshing = false // 保存所有因 Token 失效而等待新 Token 的请求回调函数 let refreshSubscribers = [] // 成功获取到新的 Token 后,执行所有等待的请求 function onRefreshed(newToken) { refreshSubscribers.forEach((cb) => cb(newToken)) refreshSubscribers = [] } // 将等待刷新 Token 的请求封装成一个回调函数,加入队列中 function addRefreshSubscriber(callback) { refreshSubscribers.push(callback) } // 响应拦截器 instance.interceptors.response.use( (res) => { console.log(res) // 摘取核心响应数据 if (res.data.status === 0) { return res } // 处理业务失败 message.error({type: 'error', content: res.data.message || '服务异常'}) return Promise.reject(res.data) }, async (err) => { // 错误的特殊情况 => 401权限不足或token过期 => 拦截到登录 const originalRequest = err.config // 判断是否是 401 并且不是已经重试过的请求 if (err.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true // 控制 Token 刷新流程(防止多次刷新) if (!isRefreshing) { // 标记刷新状态 isRefreshing = true try { const res = await refreshTokenService() const newToken = res.data.data.token // 重试请求 onRefreshed(newToken) } catch { // 刷新失败 message.error({ type: 'error', content: '登录已过期,请重新登录' }) // 跳转登录 if (window.location.pathname !== '/login') { history.push('/login') } } finally { isRefreshing = false } } // 把当前请求放入队列,等待 Token 刷新后再重发 return new Promise((resolve) => { addRefreshSubscriber((newToken) => { originalRequest.headers['Authorization'] = `Bearer ${newToken}` resolve(instance(originalRequest)) }) }) } else { // 错误的默认情况 =》 只给提示 message.error({ type: 'error', content: err.response.data.message || '服务异常' }) } return Promise.reject(err) } ) export default instance
到此这篇关于node.js实现双Token+Cookie存储+无感刷新机制的示例的文章就介绍到这了,更多相关node.js 双Token+Cookie存储+无感刷新内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!