node.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > node.js > 双token无感刷新nodejs+React

双token无感刷新nodejs+React详细解释(保姆级教程)

作者:妄春山_1

双token系统可以更好地管理用户的权限,这篇文章主要介绍了双token无感刷新nodejs+React的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

双token无感刷新是一种在Web开发中常用的安全及用户体验优化技术。以下是对双token无感刷新的详细解释:

一、基本概念

双token无感刷新主要涉及两种类型的token:Access Token(访问令牌)和Refresh Token(刷新令牌)。

二、工作原理

双token无感刷新的核心在于自动在后台处理Access Token的过期和刷新过程,而无需用户重新登录或感知到这一过程。具体流程如下:

三、实现方式

双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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文