node.js如何实现双 Token + Cookie 存储 + 无感刷新机制

node.js如何实现双 Token + Cookie 存储 + 无感刷新机制

为什么要实施双token机制?

优点描述
安全性Access Token 短期有效,降低泄露风险;Refresh Token 权限受限,仅用于获取新 Token
用户体验用户无需频繁重新登录,Token 自动刷新过程对用户透明
灵活性独立控制不同 Token 的生命周期,适应各种场景需求
可管理性支持多设备登录管理,便于撤销特定设备的登录状态
性能优化减少数据库查询次数,提升系统响应速度

实现方案:

模块实现方式
登录接口返回 accessTokenrefreshToken,分别存入 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_tokenVARCHAR(255)加密后的 RefreshToken
expires_atDATETIMERefreshToken 过期时间

3. 在后端cors跨域中间中添加属性

// 将cors注册为全局中间件
app.use(cors({origin: 'http://localhost:5173', // 前端地址credentials: true // 👈 允许携带凭证(cookies)
}))

3. 登录逻辑改造(添加双token)

  1. 添加配置文件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  // 秒数
}
  1. 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 }
)
  1. 生成token过期时间,和refreshToken一起存入数据库
 const expiresAt = new Date()expiresAt.setSeconds(expiresAt.getSeconds() + config.refreshExpiresInSec)
  1. 将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.bodyconsole.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语句成功,但是获取的条数不等于1if (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 tokenconst accessToken = jwt.sign({ id: user.id, username: user.username, email: user.email },config.jwtSecretKey,{ expiresIn: config.accessExpiresIn })// 生成refresh tokenconst 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失败' })}// 设置cookieres.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 {// 验证refreshTokenconst 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 tokenconst accessToken = jwt.sign({ id: user.id, username: user.username, email: user.email },config.jwtSecretKey,{ expiresIn: config.accessExpiresIn })// 更新accessToken到Cookieres.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中,这里不需要携带tokenreturn 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 = truetry {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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/906506.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL——6、内置函数

内置函数 1、日期函数2、字符串函数3、数学函数4、其他函数 1、日期函数 1.1、获取当前日期: 1.2、获取当前时间: 1.3、获取当前时间戳: 1.4、获取当前日期时间: 1.5、提取出日期: 1.6、给日期添加天数或时间…

【Linux】Shell脚本中向文件中写日志,以及日志文件大小、数量管理

1、写日志 shell脚本中使用echo命令,将字符串输入到文件中 覆盖写入:echo “Hello, World!” > laoer.log ,如果文件不存在,则会创建文件追加写入:echo “Hello, World!” >> laoer.log转移字符:echo -e “Name:\tlaoer\nAge:\t18” > laoer.log,\t制表符 …

深度学习中ONNX格式的模型文件

一、模型部署的核心步骤 模型部署的完整流程通常分为以下阶段,用 “跨国旅行” 类比: 步骤类比解释技术细节1. 训练模型学会一门语言(如中文)用 PyTorch/TensorFlow 训练模型2. 导出为 ONNX翻译成国际通用语言(如英语…

基于两阶段交互控制框架的互联多能系统协同自治优化

摘要:从多能源集成系统的效益出发,建立了基于交互控制的双层两阶段框架,以实现互联多能源系统(MESs)间的最优能量供应。在下层,各MES通过求解成本最小化问题自主确定其可控资产的最优设定值,其中滚动时域优化用于处理负…

matlab编写的BM3D图像去噪方法

BM3D(Block-Matching and 3D Filtering)是一种基于块匹配和三维滤波的图像去噪方法,广泛应用于图像处理领域。它通过在图像中寻找相似的块,并将这些块堆叠成三维数组进行滤波处理,从而有效地去除噪声,同时保…

前端(小程序)学习笔记(CLASS 1):组件

1、小程序中组件的分类 小程序中的组件也是由宿主环境提供的,开发者可以基于组件快速搭建出漂亮的页面结构。官方把小程序的组件分为了9大类,分别是: * 视图容器,* 基础内容,* 表单组件,* 导航组件 媒体…

基于亚马逊云科技构建音视频直播审核方案

1. 前言 随着互联网内容形态的多样化发展,用户生成内容(UGC)呈现爆发式增长。社交平台、直播、短视频、语聊房等应用场景中,海量的音视频内容需要进行实时审核,以维护平台安全与用户体验。 然而,企业在构…

linux基础操作11------(运行级别)

一.前言 这个是linux最后一章节内容,主要还是介绍一下,这个就和安全有关系了,内容还是很多的,但是呢,大家还是做个了解就好了。 二.权限掩码 运行级别 0 关机 运行级别 1 单用户 ,这个类似于windows安全…

QT+Visual Studio 配置开发环境教程

一、QT架构 Qt Creator 是一个轻量级、跨平台的 IDE,专为 Qt 开发量身打造,内置对 qmake/CMake 的深度支持、Kits 配置管理、原生 QML 调试器以及较低的资源占用维基百科。 而在 Windows 环境下,Visual Studio 配合 Qt VS Tools 扩展则可将 Q…

(2)JVM 内存模型更新与 G1 垃圾收集器优化

JVM 内存模型更新与 G1 垃圾收集器优化 🚀 掌握前沿技术,成为顶尖 Java 工程师 2️⃣ JVM 内存模型更新 👉 点击展开题目 JVM内存模型在Java 17中有哪些重要更新?如何优化G1垃圾收集器在容器化环境的表现? &#x1…

TASK04【Datawhale 组队学习】构建RAG应用

目录 将LLM接入LangChain构建检索问答链运行成功图遇到的问题 langchain可以便捷地调用大模型,并将其结合在以langchain为基础框架搭建的个人应用中。 将LLM接入LangChain from langchain_openai import ChatOpenAI实例化一个 ChatOpenAI 类,实例化时传入超参数来…

springAI调用deepseek模型使用硅基流动api的配置信息

查看springai的官方文档,调用deepseek的格式如下: spring.ai.deepseek.api-key${your-api-key} spring.ai.deepseek.chat.options.modeldeepseek-chat spring.ai.deepseek.chat.options.temperature0.8 但是硅基流动的格式不是这样,这个伞兵…

SpringMVC 通过ajax 实现文件的上传

使用form表单在springmvc 项目中上传文件,文件上传成功之后往往会跳转到其他的页面。但是有的时候,文件上传成功的同时,并不需要进行页面的跳转,可以通过ajax来实现文件的上传 下面我们来看看如何来实现: 方式1&…

Docker安装Fluentd采集中间件

Fluentd 简介 :Fluentd 是一个高性能、可扩展的数据收集与聚合工具,能够统一数据收集和消费,实现各种数据源到各种数据接收器的高效传输,广泛应用于日志收集等领域。 功能特点 : 统一日志收集 :支持从各种…

07SpringMVC底层形象解析

目录 一、基于餐厅比喻的代码示例 ,帮助你理解各组件间的协作关系 1. DispatcherServlet 配置(服务员) 2. HandlerMapping 配置(菜单索引) 3. Controller 实现(厨师) 4. Service 层&#x…

eclipse 生成函数说明注释

在Eclipse中生成函数说明注释(JavaDoc风格)可以通过以下方法实现: 快捷键方式: 将光标放在函数上方输入/**后按回车键Eclipse会自动生成包含参数和返回值的注释模板 菜单方式: 选中函数点击菜单栏 Source > Gen…

【题解-洛谷】P6180 [USACO15DEC] Breed Counting S

题目:P6180 [USACO15DEC] Breed Counting S 题目描述 Farmer John 的 N N N 头奶牛,从左到右编号为 1 …

基于Android的XX校园交流APP

开发语言:Java框架:ssmAndroidJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7数据库工具:Navicat12开发软件:eclipse/myeclipse/ideaMaven包:Maven3.3.9 系统展示 APP登录 APP首页…

25、工业防火墙 - 工控网络保护 (模拟) - /安全与维护组件/industrial-firewall-dcs-protection

76个工业组件库示例汇总 工业防火墙 - 工控网络保护 (模拟) 概述 这是一个交互式的 Web 组件,旨在模拟工业防火墙在保护关键工控网络(特别是 DCS - 分布式控制系统)免受网络攻击(如勒索软件传播)方面的核心功能。组件通过可视化简化的网络拓扑、模拟网络流量、应用防火…

kotlin flow的两种SharingStarted策略的区别

一 两种 SharingStarted 策略的区别: SharingStarted.Eagerly: 立即开始收集上游流,即使没有下游订阅者持续保持活跃状态,直到 ViewModel 被清除优点:响应更快,数据始终保持最新缺点:消耗更多资源&#x…