双token无感刷新nodejs+React详细解释(保姆级教程)
作者:妄春山_1
双token系统可以更好地管理用户的权限,这篇文章主要介绍了双token无感刷新nodejs+React的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
双token无感刷新是一种在Web开发中常用的安全及用户体验优化技术。以下是对双token无感刷新的详细解释:
一、基本概念
双token无感刷新主要涉及两种类型的token:Access Token(访问令牌)和Refresh Token(刷新令牌)。
- Access Token:用户直接用于访问受保护资源的凭证。有效期较短,通常几分钟到几小时不等。一旦过期,用户需要重新认证以获取新的Access Token。即使Access Token被泄露,由于其有效期短,攻击者利用它进行不当操作的时间窗口有限。
- Refresh Token:用于在Access Token过期后或过期前的一定时间内重新获取新的Access Token。有效期通常较长,甚至可以说是永久的,但出于安全考虑,一般会设置过期时间或使用次数限制。Refresh Token通常不会直接发送给客户端,而是保存在服务器端或经过加密后存储在客户端本地(如localStorage或sessionStorage)。
二、工作原理
双token无感刷新的核心在于自动在后台处理Access Token的过期和刷新过程,而无需用户重新登录或感知到这一过程。具体流程如下:
- 用户通过用户名(账号)、密码或其他认证方式向认证服务器请求授权。
- 认证成功后,服务器返回Access Token和Refresh Token给前端。前端将这两个Token保存到本地存储中,以便在需要时使用。
- 前端在访问受保护资源时,将Access Token放入请求头中发送给后端。
- 如果Access Token有效,后端正常处理请求并返回结果。
- 如果Access Token过期,后端会返回一个错误响应(如HTTP 401 Unauthorized)。
- 前端在接收到错误响应后,自动在后台使用Refresh Token向认证服务器请求新的Access Token。
- 认证服务器验证Refresh Token的有效性后,返回一个新的Access Token和(可选的)新的Refresh Token。
- 前端更新本地存储的Access Token和Refresh Token,并重新发起之前的请求。
三、实现方式
双token无感刷新的实现通常依赖于前端和后端的配合。以下是一个简化的实现流程:
前端实现:
- 设置全局请求拦截器,在发送请求前检查Access Token的有效性。
- 如果Access Token即将过期或已过期,自动使用Refresh Token请求新的Access Token。
- 更新本地存储中的Access Token和Refresh Token。
- 重新发起因Access Token过期而失败的请求。
后端实现:
- 提供一个用于刷新Token的接口,该接口接收Refresh Token作为参数。
- 验证Refresh Token的有效性,如果有效,则生成新的Access Token和(可选的)新的Refresh Token,并返回给客户端。
- 在受保护资源的访问接口中验证Access Token的有效性,并在其过期时返回相应的错误响应。
四、优势与应用场景
双token无感刷新的优势在于提高了系统的安全性和用户体验。通过分离短期和长期的凭证,降低了Access Token被泄露的风险,同时避免了用户因Token过期而频繁重新登录的麻烦。
这一机制广泛应用于需要高安全性和良好用户体验的场景,如Web应用和移动应用。在这些场景中,双token无感刷新能够提升用户在持续操作过程中的连贯性和安全性。
五、后端代码解析(nodejs+express)
(1)app.js引入所需要的模块
//app.js var createError = require('http-errors'); //用于创建各种HTTP错误 var express = require('express'); //引入express模块 var path = require('path'); //用于处理文件和目录的路径 var cookieParser = require('cookie-parser'); //解析HTTP请求中的Cookie头 var logger = require('morgan'); //用于将请求日志输出到stdout或指定文件 var indexRouter = require('./routes/index'); //主路由 var usersRouter = require('./routes/users'); //用户相关路由 var jwt = require("jsonwebtoken"); //引入jsonwebtoken模块 var app = express(); var cors = require("cors") app.use(cors()) //处理跨域
(2)自定义中间件,用于处理jwt验证
//app.js app.use((req, res, next) => { // 定义不需要验证的路径 let pathArr = [ '/userLogin', '/refresh', '/sendYzm', '/resetPassword', '/findUser', '/upload' ] // 如果请求路径在不需要验证的路径中,直接调用next()继续处理 if (pathArr.includes(req.path)) { return next() } // 获取请求头中的accessToken和refreshToken const accessToken = req.headers.accesstoken const refreshToken = req.headers.refreshtoken // 判断refreshToken是否过期 try { jwt.verify(refreshToken, "MTHGH") } catch (error) { console.log(error); return res.status(403).send({ message: 'refreshToken验证失败' }) } // 如果没有accessToken 返回401 if (!accessToken) { return res.status(401).send({ message: '未获取到accessToken' }) } // 验证accessToken try { const user = jwt.verify(accessToken, "MTHGH") res.locals.user = user//将用户信息存储在res.locals中,供后续中间件使用 return next() } catch (error) { return res.status(401).send({ message: 'accessToken验证失败' }) } })
(3)index.js所需引入模块
// index.js var express = require('express'); //引入了Express模块 var router = express.Router(); //用于处理HTTP请求 const jwt = require('jsonwebtoken'); //实现用户身份验证和授权等功能
(4)封装生成长token和短token的函数
// index.js // 访问令牌有效期 const ACCESS_TOKEN_EXPIRATION='1h' // 刷新令牌有效期 const REFRESH_TOKEN_EXPIRATION='1d' // 令牌密钥 const SECRET_KEY="MTHGH" // 这里创建了一个Map对象,用于存储用户名和对应的刷新令牌列表。这样,当用户登录或刷新令牌时,可以跟踪和验证他们的刷新令牌。 const refreshTokenMap=new Map() // 生成函数令牌 function generateToken(name,expiration){ return jwt.sign({name},SECRET_KEY,{expiresIn:expiration}) } // 封装生成长token和短token function getToken(name){ let accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) let refreshToken = generateToken(name,REFRESH_TOKEN_EXPIRATION) const refreshTokens = refreshTokenMap.get(name)||[] refreshTokens.push(refreshToken) refreshTokenMap.set(name,refreshTokens) return { accessToken, refreshToken } }
(5)账号密码登录
// index.js router.post('/userLogin', async (req, res) => { // 获取前端传递的数据 const {username,password} = req.body // 通过用户查找是否存在 let user = await userModel.findOne({userName:username}) // 不存在 if(!user){ return res.status(200).send({message:"账号错误",code:1}) } // 用户存在,密码不正确 if(user.passWord!==password){ return res.status(200).send({message:"密码错误",code:2}) } // 调用函数,生成token let {accessToken,refreshToken}=getToken(user.userName) // 返回用户、token res.status(200).send({ data:user, accessToken, refreshToken, message:'登陆成功', code:200 }) })
(6)刷新短token
// index.js router.get('/refresh',async(req,res)=>{ // 获取请求头的token信息 const refreshToken = req.headers.refreshtoken // token不存在 if(!refreshToken){ res.status(403).send("没有短token") } // 验证token是否过期 try{ const {name} = jwt.verify(refreshToken,SECRET_KEY) const accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) res.status(200).send({accessToken}) }catch(error){ console.log('长token已经过期'); res.status(403).send('长token已经过期') } })
六、前端代码解析(React)
(1)添加axios重试机制
// api.jsx let retryCount = 0; // 初始化重试计数 const customRetryCondition = async (error) => { // 自定义重试条件 if (axios.isAxiosError(error) && error.response?.status !== 200) { // 如果是 Axios 错误且响应状态不是 200 if (error.response?.status === 403) { // 如果后端返回 403(禁止访问) localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken console.log('请重新登录'); // 打印提示信息 window.location.href='/login' // 跳转到登录页面 return false; // 不重试 } if (error.response?.status === 401) { // 如果后端返回 401(未授权) await refresh(); // 尝试刷新 token console.log('刷新token'); // 打印提示信息 return true; // 允许重试 } retryCount++; // 增加重试计数 console.log(`第${retryCount}次重试`); // 打印当前重试次数 return ( error.response.status >= 500 || // 如果响应状态是 500 或以上 (error.response.status < 500 && error.response?.status !== 401) // 或者状态小于 500 但不等于 401 ); } return false; // 如果不符合条件,则不重试 }; // 配置 axios 实例的重试机制 axiosRetry(instance, { retries: 3, // 设置最多重试次数为 3 次 retryCondition: customRetryCondition, // 使用自定义的重试条件 retryDelay: axiosRetry.exponentialDelay, // 使用指数退避算法设置重试延迟 });
(2)axios添加请求拦截器
// api.jsx // 请求拦截器 instance.interceptors.request.use( async function (config) { console.log('开始请求'); // 打印请求开始信息 const accessToken = localStorage.getItem('accessToken'); // 从 localStorage 获取 accessToken const refreshToken = localStorage.getItem('refreshToken'); // 从 localStorage 获取 refreshToken console.log(accessToken); console.log(refreshToken); config.headers['accessToken'] = accessToken // 设置请求头中的 accessToken config.headers['refreshToken'] = refreshToken // 设置请求头中的 refreshToken return config; // 返回配置 }, function (error) { return Promise.reject(error); // 拒绝请求错误 } );
(3)axios添加响应拦截器
// api.jsx /** * 响应拦截器 */ instance.interceptors.response.use( async function (response) { // alert(222) if (response.status === 200) { return response; // 如果响应状态是 200,返回响应 } else { return Promise.reject(response.data.message || '未知错误'); // 否则拒绝并返回错误信息 } }, function (error) { if (error && error.response) { // 如果有响应错误 switch (error.response.status) { case 400: error.message = '错误请求'; // 处理 400 错误 break; case 401: error.message = '未授权,请重新登录'; // 处理 401 错误 break; case 403: error.message = '拒绝访问'; // 处理 403 错误 localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken // Router.push('/login'); // 跳转到登录页面 window.location.href='/login' break; case 404: error.message = '请求错误,未找到该资源'; // 处理 404 错误 break; case 405: error.message = '请求方法未允许'; // 处理 405 错误 break; case 408: error.message = '请求超时'; // 处理 408 错误 break; case 500: error.message = '服务器端出错'; // 处理 500 错误 break; case 501: error.message = '网络未实现'; // 处理 501 错误 break; case 502: error.message = '网络错误'; // 处理 502 错误 break; case 503: error.message = '服务不可用'; // 处理 503 错误 break; case 504: error.message = '网络超时'; // 处理 504 错误 break; case 505: error.message = 'http版本不支持该请求'; // 处理 505 错误 break; default: error.message = `连接错误${error.response.status}`; // 处理其他未知错误 } } else { error.message = '连接服务器失败'; // 如果没有响应,打印连接失败信息 } return Promise.reject(error.message); // 拒绝并返回错误信息 } );
(4)刷新token
// api.jsx // 重新刷新token async function refresh() { let res =await instance.get('/refresh'); // 发送请求以刷新 token localStorage.setItem('accessToken', res.data.accessToken); // 将新的 accessToken 存储到 localStorage }
七、后端完整代码
(1)app.js
// app.js var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var jwt = require("jsonwebtoken"); var app = express(); // cors var cors = require("cors") app.use(cors()) // 自定义中间件 用于处理jwt验证 app.use((req, res, next) => { // 定义不需要验证的路径 let pathArr = [ '/userLogin', '/refresh', '/sendYzm', '/resetPassword', '/findUser', '/upload' ] // 如果请求路径在不需要验证的路径中,直接调用next()继续处理 if (pathArr.includes(req.path)) { return next() } // 获取请求头中的accessToken和refreshToken const accessToken = req.headers.accesstoken const refreshToken = req.headers.refreshtoken // 判断refreshToken是否过期 try { jwt.verify(refreshToken, "MTHGH") } catch (error) { console.log(error, 111); return res.status(403).send({ message: 'refreshToken验证失败' }) } // 如果没有accessToken 返回401 if (!accessToken) { return res.status(401).send({ message: '未获取到accessToken' }) } // 使用中间件 // app.use('/index',validateRefreshToken,validateAccessToken) // app.use('/user',validateRefreshToken,validateAccessToken) // 验证accessToken try { const user = jwt.verify(accessToken, "MTHGH") res.locals.user = user//将用户信息存储在res.locals中,供后续中间件使用 return next() } catch (error) { return res.status(401).send({ message: 'accessToken验证失败' }) } }) // token // var expressJWT = require("express-jwt") // app.use(expressJWT({ // secret: "123", // algorithms: ["HS256"] // }).unless({ // path: ["/login", "/upload", {url: /^\/upload/, methods: ['GET']}] // })) // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/upload', express.static(path.join(__dirname, 'upload'))); app.use('/', indexRouter); app.use('/users', usersRouter); // catch 404 and forward to error handler app.use(function (req, res, next) { next(createError(404)); }); // error handler app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app;
(2)index.js
// index.js var express = require('express'); var router = express.Router(); const jwt = require('jsonwebtoken'); var { userModel } = require("../model/model") const app = express(); app.use(express.json()); // 解析请求中的 JSON 数据 // 访问令牌有效期 const ACCESS_TOKEN_EXPIRATION='1h' // 刷新令牌有效期 const REFRESH_TOKEN_EXPIRATION='1d' const SECRET_KEY="MTHGH" const refreshTokenMap=new Map() // 生成函数令牌 function generateToken(name,expiration){ return jwt.sign({name},SECRET_KEY,{expiresIn:expiration}) } // 封装生成长token和短token function getToken(name){ let accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) let refreshToken = generateToken(name,REFRESH_TOKEN_EXPIRATION) const refreshTokens = refreshTokenMap.get(name)||[] refreshTokens.push(refreshToken) refreshTokenMap.set(name,refreshTokens) return { accessToken, refreshToken } } // 账号密码登录 router.post('/userLogin', async (req, res) => { const {username,password} = req.body let user = await userModel.findOne({userName:username}) if(!user){ return res.status(200).send({message:"账号错误",code:1}) } if(user.passWord!==password){ return res.status(200).send({message:"密码错误",code:2}) } let {accessToken,refreshToken}=getToken(user.userName) res.status(200).send({ data:user, accessToken, refreshToken, message:'登陆成功', code:200 }) }) // 刷新短token router.get('/refresh',async(req,res)=>{ const refreshToken = req.headers.refreshtoken // console.log(111); if(!refreshToken){ res.status(403).send("没有短token") } try{ const {name} = jwt.verify(refreshToken,SECRET_KEY) const accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) res.status(200).send({accessToken}) }catch(error){ console.log('长token已经过期'); res.status(403).send('长token已经过期') } }) module.exports = router;
八、前端完整代码
(1)api.jsx
// api.jsx import axios from "axios"; import axiosRetry from 'axios-retry'; // 基础配置 const instance = axios.create({ baseURL: "http://localhost:3010", timeout: 5000, headers: { 'Content-Type': 'application/json' }, }); /** * 重试机制 */ let retryCount = 0; // 初始化重试计数 const customRetryCondition = async (error) => { // 自定义重试条件 if (axios.isAxiosError(error) && error.response?.status !== 200) { // 如果是 Axios 错误且响应状态不是 200 if (error.response?.status === 403) { // 如果后端返回 403(禁止访问) localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken console.log('请重新登录'); // 打印提示信息 window.location.href='/login' // 跳转到登录页面 return false; // 不重试 } if (error.response?.status === 401) { // 如果后端返回 401(未授权) await refresh(); // 尝试刷新 token console.log('刷新token'); // 打印提示信息 return true; // 允许重试 } retryCount++; // 增加重试计数 console.log(`第${retryCount}次重试`); // 打印当前重试次数 return ( error.response.status >= 500 || // 如果响应状态是 500 或以上 (error.response.status < 500 && error.response?.status !== 401) // 或者状态小于 500 但不等于 401 ); } return false; // 如果不符合条件,则不重试 }; // 配置 axios 实例的重试机制 axiosRetry(instance, { retries: 3, // 设置最多重试次数为 3 次 retryCondition: customRetryCondition, // 使用自定义的重试条件 retryDelay: axiosRetry.exponentialDelay, // 使用指数退避算法设置重试延迟 }); // 请求拦截器 instance.interceptors.request.use( async function (config) { console.log('开始请求'); // 打印请求开始信息 const accessToken = localStorage.getItem('accessToken'); // 从 localStorage 获取 accessToken const refreshToken = localStorage.getItem('refreshToken'); // 从 localStorage 获取 refreshToken console.log(accessToken); console.log(refreshToken); config.headers['accessToken'] = accessToken // 设置请求头中的 accessToken config.headers['refreshToken'] = refreshToken // 设置请求头中的 refreshToken return config; // 返回配置 }, function (error) { return Promise.reject(error); // 拒绝请求错误 } ); /** * 响应拦截器 */ instance.interceptors.response.use( async function (response) { // alert(222) if (response.status === 200) { return response; // 如果响应状态是 200,返回响应 } else { return Promise.reject(response.data.message || '未知错误'); // 否则拒绝并返回错误信息 } }, function (error) { if (error && error.response) { // 如果有响应错误 switch (error.response.status) { case 400: error.message = '错误请求'; // 处理 400 错误 break; case 401: error.message = '未授权,请重新登录'; // 处理 401 错误 break; case 403: error.message = '拒绝访问'; // 处理 403 错误 localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken // Router.push('/login'); // 跳转到登录页面 window.location.href='/login' break; case 404: error.message = '请求错误,未找到该资源'; // 处理 404 错误 break; case 405: error.message = '请求方法未允许'; // 处理 405 错误 break; case 408: error.message = '请求超时'; // 处理 408 错误 break; case 500: error.message = '服务器端出错'; // 处理 500 错误 break; case 501: error.message = '网络未实现'; // 处理 501 错误 break; case 502: error.message = '网络错误'; // 处理 502 错误 break; case 503: error.message = '服务不可用'; // 处理 503 错误 break; case 504: error.message = '网络超时'; // 处理 504 错误 break; case 505: error.message = 'http版本不支持该请求'; // 处理 505 错误 break; default: error.message = `连接错误${error.response.status}`; // 处理其他未知错误 } } else { error.message = '连接服务器失败'; // 如果没有响应,打印连接失败信息 } return Promise.reject(error.message); // 拒绝并返回错误信息 } ); // 重新刷新token async function refresh() { let res =await instance.get('/refresh'); // 发送请求以刷新 token localStorage.setItem('accessToken', res.data.accessToken); // 将新的 accessToken 存储到 localStorage } // 导出封装后的 axios 实例 export default instance;
综上所述,双token无感刷新是一种有效的安全及用户体验优化技术,通过合理的Token管理和自动刷新机制,实现了在不影响用户体验的前提下提升系统安全性的目标。
总结
到此这篇关于双token无感刷新nodejs+React的文章就介绍到这了,更多相关双token无感刷新nodejs+React内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!