首页 > 编程语言 >双token无感刷新nodejs+vue3(保姆级教程)

双token无感刷新nodejs+vue3(保姆级教程)

时间:2024-11-06 11:52:00浏览次数:4  
标签:const accessToken nodejs token error refreshToken message 无感

什么是双 Token 无感刷新?

双 Token 无感刷新机制使用两个不同的 token 来管理用户的身份验证和会话。通常情况下,这两个 token 是:

  1. 访问 Token(Access Token):用于访问受保护的资源,通常具有较短的有效期(如 15 分钟到 1 小时)。当用户进行 API 请求时,附带此 token 以证明其身份。

  2. 刷新 Token(Refresh Token):用于获取新的访问 token,通常具有较长的有效期(如几天到几个月)。刷新 token 不会频繁发送到服务器,而是在访问 token 过期后用于请求新的访问 token。

工作流程

  1. 用户登录:用户通过用户名和密码登录,服务器验证凭据后生成访问 token 和刷新 token,并将其返回给客户端。

  2. 使用访问 Token:客户端使用访问 token 进行 API 请求。当访问 token 过期时,客户端将无法访问受保护的资源。

  3. 无感刷新

    • 当访问 token 过期时,客户端会自动使用刷新 token 请求新的访问 token,而无需用户重新登录。
    • 服务器验证刷新 token 的有效性,如果有效,则生成新的访问 token 并返回给客户端。
    • 客户端使用新的访问 token 继续进行 API 请求。
  4. 刷新 Token 的管理

    • 刷新 token 也可以设置过期时间,通常在用户长时间不活动时使其失效。
    • 服务器可以在刷新 token 失效时要求用户重新登录。

优势

  1. 用户体验:用户在使用应用时不需要频繁登录,提供了无缝的体验。

  2. 安全性:访问 token 的有效期较短,可以降低 token 被盗用的风险。即使访问 token 被盗,攻击者也只能在短时间内使用它。

  3. 灵活性:可以根据需要调整访问 token 和刷新 token 的有效期,以平衡安全性和用户体验。

  4. 支持登出:当用户选择登出时,可以使刷新 token 失效,从而阻止用户重新获取访问 token。

话不多说直接上代码

后端:nodejs下的express框架

        app.js文件:

// 引入所需的模块
var createError = require('http-errors'); // 用于创建 HTTP 错误
var express = require('express'); // 引入 Express 框架
var path = require('path'); // 用于处理文件和目录路径
var cookieParser = require('cookie-parser'); // 用于解析 Cookie
var logger = require('morgan'); // 用于记录请求日志
const jwt = require('jsonwebtoken'); // 用于处理 JSON Web Tokens
const dotenv = require('dotenv'); // 用于加载环境变量
dotenv.config(); // 加载 .env 文件中的环境变量
const cors = require('cors'); // 用于处理跨域请求

// 引入路由模块
var indexRouter = require('./routes/index'); // 主路由
var usersRouter = require('./routes/users'); // 用户相关路由
var loginRouter = require('./routes/login'); // 登录相关路由

// 创建 Express 应用
var app = express();

// 使用 CORS 中间件,允许跨域请求
app.use(cors());

// 自定义中间件,用于处理 JWT 验证
app.use((req, res, next) => {
    // 定义不需要验证的路径
    let pathArr = [
        '/login/userLogin', // 用户登录路径
        '/login/refresh' // 刷新 token 的路径
    ]
    
    // 如果请求路径在不需要验证的路径中,直接调用 next() 继续处理
    if (pathArr.includes(req.path)) {
        return next()
    }

    // 获取请求头中的 accessToken 和 refreshToken
    const accessToken = req.headers.accesstoken; // 注意:这里的 'accesstoken' 虽然前端传过来是驼峰命名法'accessToken'
    const refreshToken = req.headers.refreshtoken; //但是这里也要全部小写,http的机制导致

    // 判断 refreshToken 是否过期
    try {
        jwt.verify(refreshToken, 'WANGJIALONG'); // 验证 refreshToken
    } catch (error) {
        console.log(error); // 打印错误信息
        return res.status(403).send({ message: 'Forbidden' }); // 如果验证失败,返回 403 Forbidden
    }

    // 如果没有 accessToken,返回 401 Unauthorized
    if (!accessToken) {
        return res.status(401).send({ message: 'Unauthorized' });
    }

    // 验证 accessToken
    try {
        const user = jwt.verify(accessToken, 'WANGJIALONG'); // 验证 accessToken
        res.locals.user = user; // 将用户信息存储在 res.locals 中,供后续中间件使用
        return next(); // 验证成功,调用 next() 继续处理请求
    } catch (error) {
        return res.status(401).send({ message: 'Unauthorized' }); // 如果验证失败,返回 401 Unauthorized
    }
})

// 设置视图引擎
app.set('views', path.join(__dirname, 'views')); // 设置视图文件夹路径
app.set('view engine', 'ejs'); // 设置视图引擎为 EJS

// 使用中间件
app.use(logger('dev')); // 记录请求日志
app.use(express.json()); // 解析 JSON 格式的请求体
app.use(express.urlencoded({ extended: false })); // 解析 URL 编码的请求体
app.use(cookieParser()); // 解析 Cookie
app.use(express.static(path.join(__dirname, '/upload'))); // 设置静态文件目录

// 使用路由
app.use('/', indexRouter); // 主路由
app.use('/users', usersRouter); // 用户路由
app.use('/login', loginRouter); // 登录路由

// 捕获 404 错误并转发到错误处理器
app.use(function (req, res, next) {
    next(createError(404)); // 创建 404 错误
});

// 错误处理器
app.use(function (err, req, res, next) {
    // 设置 locals,仅在开发环境中提供错误信息
    res.locals.message = err.message; // 错误信息
    res.locals.error = req.app.get('env') === 'development' ? err : {}; // 在开发环境中提供完整错误信息

    // 渲染错误页面
    res.status(err.status || 500); // 设置响应状态
    res.render('error'); // 渲染错误页面
});

// 导出应用
module.exports = app; // 导出 Express 应用实例

ps:这里需要注意的是 白名单的使用,对于不需要进行token验证的路由统一放到一个数组中,如果检测到包含不需要验证的路由,直接next()通过即可。

ps:前端发送过来的请求头中是利用驼峰命名法传递的,但是在后端接受时,统一小写,这是http的机制导致的,切记!!!一定小写,否则报错。

        login.js文件

var express = require('express');
var router = express.Router();
let jwt = require('jsonwebtoken');
let { userModel } = require('../model/model');

const ACCESS_TOKEN_EXPIRATION = 5; //访问令牌有效期
const REFRESH_TOKEN_EXPIRATION = '1d'; //刷新令牌有效期
const SECRET_KEY = 'WANGJIALONG';
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); //短Token
    let refreshToken = generateToken(name, REFRESH_TOKEN_EXPIRATION); //长Token
    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 }); // 根据用户名查找用户
    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;
    if (!refreshToken) {
        res.status(403).send('Forbidden');
    }
    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('Forbidden');
    }
});


router.post('/add', async (req, res) => {
    await userModel.create(req.body)
    res.send({
        code: 200
    })
})
module.exports = router;

这里我将生成token封装了一个函数,方便以后调用。这里为了方便测试,我的短token设置了5秒过期,长token设置了1天过期(在实际开发中短token设置十几分钟到一个小时,长token设置几天甚至一个月)

对于前端我是用的vue3框架

这里需要注意的点是需要引入axios的axios-retry库,用于实现请求重试机制,避免代码运行过程中出现一些意外情况导致出现某些问题,这时代码会自动重新尝试请求,无需用户操作,提高用户体验。

这里封装了axios,用于响应401,403状态码。401则为未登录,403为未授权。以下代码我都进行了详细注释!

        axios.js文件:

import axios from 'axios'; // 引入 axios 库,用于进行 HTTP 请求
import axiosRetry from 'axios-retry'; // 引入 axios-retry 库,用于实现请求重试机制
import router from './router'; // 引入 Vue Router 实例,用于页面导航

// 创建一个 axios 实例
export function createAxios(option = {}) {
    return axios.create({
        ...option, // 将传入的选项合并到 axios 实例中
    });
}

// 创建一个名为 houseApi 的 axios 实例,设置基本 URL 和超时时间
export const houseApi = createAxios({
    baseURL: 'http://localhost:3000', // 设定基础 URL
    timeout: 5000, // 请求超时时间设置为 5000 毫秒(5 秒)
});

/**
 * 重试机制
 */
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('请重新登录'); // 打印提示信息
        router.push('/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(houseApi, {
  retries: 3, // 设置最多重试次数为 3 次
  retryCondition: customRetryCondition, // 使用自定义的重试条件
  retryDelay: axiosRetry.exponentialDelay, // 使用指数退避算法设置重试延迟
});

/**
 * 请求拦截器
 */
houseApi.interceptors.request.use(
    async function (config) {   
        console.log('开始请求'); // 打印请求开始信息
        const accessToken = localStorage.getItem('accessToken'); // 从 localStorage 获取 accessToken
        const refreshToken = localStorage.getItem('refreshToken'); // 从 localStorage 获取 refreshToken
        config.headers.accessToken = accessToken ? accessToken : ''; // 设置请求头中的 accessToken
        config.headers.refreshToken = refreshToken ? refreshToken : ''; // 设置请求头中的 refreshToken
        return config; // 返回配置
    },
    function (error) {
        return Promise.reject(error); // 拒绝请求错误
    }
);

/**
 * 响应拦截器
 */
houseApi.interceptors.response.use(
    async function (response) {
        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'); // 跳转到登录页面
                    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 houseApi.get('/login/refresh'); // 发送请求以刷新 token
    localStorage.setItem('accessToken', res.data.accessToken); // 将新的 accessToken 存储到 localStorage
}

export default houseApi; // 导出 houseApi 实例

接下来呢就是常规的登录代码和登录进去的首页代码了:

Login.vue:

<template>
    <div class="login-container">
        <h1 class="login-title">Login</h1>
        <div class="input-group">
            <input v-model="username" class="login-input" placeholder="Username" />
            <input v-model="password" type="password" class="login-input" placeholder="Password" />
            <button @click="login" class="login-button">Login</button>
        </div>

        <pre v-if="errorMessage" class="error-message">{{ errorMessage }}</pre>
    </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import houseApi from '../axios'

const username = ref('')
const password = ref('')
const errorMessage = ref(null)
const router = useRouter()

const login = async () => {
    errorMessage.value = null // 重置错误消息
    try {
        const response = await houseApi.post('/login/userLogin', {
            username: username.value,
            password: password.value
        })

        console.log(response)
        if (response.data.code === 1) {
            alert(response.data.message)
        } else if (response.data.code === 2) {
            alert(response.data.message)
        } else {
            localStorage.setItem('accessToken', response.data.accessToken)
            localStorage.setItem('refreshToken', response.data.refreshToken)
            router.push('/home') // 登录成功后跳转
        }
    } catch (error) {
        // 处理错误
        if (error.response) {
            errorMessage.value = error.response.data.message || 'Login failed'
        } else {
            errorMessage.value = 'An unexpected error occurred'
        }
    }
}
</script>

<style scoped>
body {
    margin: 0;
    padding: 0;
    font-family: 'Arial', sans-serif;
    background: linear-gradient(135deg, #74ebd5, #9face6);
}

.login-container {
    max-width: 400px;
    margin: 100px auto;
    padding: 20px;
    background: white;
    border-radius: 10px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.login-title {
    font-size: 2em;
    margin-bottom: 20px;
    color: #333;
}

.input-group {
    margin: 10px 0;
}

.login-input {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ddd;
    border-radius: 5px;
    transition: border-color 0.3s;
}

.login-input:focus {
    border-color: #74ebd5;
    outline: none;
}

.login-button {
    width: 100%;
    padding: 10px;
    background: #74ebd5;
    border: none;
    border-radius: 5px;
    color: white;
    font-weight: bold;
    cursor: pointer;
    transition: background 0.3s;
}

.login-button:hover {
    background: #9face6;
}

.error-message {
    color: red;
    margin-top: 10px;
    font-size: 0.9em;
}
</style>

Home.vue:

<template>
    <div class="form-container">
        <h2 class="form-title">添加用户</h2>
        <input v-model="username" type="text" placeholder="用户名" class="form-input" />
        <input v-model="password" type="password" placeholder="密码" class="form-input" />
        <input v-model="phone" type="text" placeholder="手机号" class="form-input" />
        <button @click="add" class="form-button">添加</button>
        <pre v-if="errorMessage" class="error-message">{{ errorMessage }}</pre>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import houseApi from '../axios';

const username = ref('');
const password = ref('');
const phone = ref('');
const errorMessage = ref(null);

const add = async () => {
    errorMessage.value = null; // 重置错误消息
    try {
        const { data: { code, message } } = await houseApi.post('/login/add', {
            username: username.value,
            password: password.value,
            phone: phone.value
        });

        if (code === 200) {
            alert('用户添加成功');
            // 可以清空输入框或其他操作
        } else {
            errorMessage.value = message || '添加失败';
        }
    } catch (error) {
        errorMessage.value = error.response?.data?.message || '网络错误';
    }
};
</script>

<style scoped>
body {
    background: linear-gradient(135deg, #74ebd5, #9face6);
}

.form-container {
    max-width: 400px;
    margin: 100px auto;
    padding: 20px;
    background: white;
    border-radius: 10px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.form-title {
    font-size: 1.5em;
    margin-bottom: 20px;
    color: #333;
}

.form-input {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ddd;
    border-radius: 5px;
    transition: border-color 0.3s;
}

.form-input:focus {
    border-color: #74ebd5;
    outline: none;
}

.form-button {
    width: 100%;
    padding: 10px;
    background: #74ebd5;
    border: none;
    border-radius: 5px;
    color: white;
    font-weight: bold;
    cursor: pointer;
    transition: background 0.3s;
}

.form-button:hover {
    background: #9face6;
}

.error-message {
    color: red;
    margin-top: 10px;
    font-size: 0.9em;
    text-align: left;
}
</style>

完整代码

下载代码请复制以下命令到终端:

git clone https://gitee.com/wjl001123/token_two.git

标签:const,accessToken,nodejs,token,error,refreshToken,message,无感
From: https://blog.csdn.net/m0_59365887/article/details/143565856

相关文章

  • ATC:多快好省,无参数token reduction方法 | ECCV'24
    来源:晓飞的算法工程笔记公众号,转载请注明出处论文:AgglomerativeTokenClustering论文地址:https://arxiv.org/abs/2409.11923论文代码:https://github.com/JoakimHaurum/ATC创新点提出了层次token聚类(AgglomerativeTokenClustering,ATC),这是一种新型的无参数层次合......
  • 基于Redis的Token认证机制
    Redis数据库设计/***rediskey前缀*/publicstaticfinalStringREDIS_KEY_PREFIX="easylive:";/***验证码key*/publicstaticfinalStringREDIS_KEY_CHECK_CODE=REDIS_KEY_PREFIX+"check_code:";/***Rediskeytokenweb*/publicstati......
  • Dify 中的 Bearer Token 与 API-Key 鉴权方式
    本文使用Difyv0.10.2版本,在Dify中包括BearerToken与API-Key鉴权这2种方式。console(URL前缀/console/api)和web(URL前缀/api)蓝图使用的是BearerToken鉴权方式,而service_api(URL前缀/v1)蓝图使用的是API-Key鉴权方式。console蓝图通过login_required装饰......
  • 微信公众号服务器配置一直提示token验证失败?
    本地使用postman,请求了要设置在微信公众号服务器回调的URL,可以正常返回echostr,点击提交,一直报错token验证失败,请问这个是什么原因呢? 解决办法:解决了,我去,好坑啊,遇到该问题的朋友,请做如下检查: 1.检查,request是不是UTF-8,避免获取的数据是乱码  2.response.setContentTyp......
  • 彻底理解cookie、session、token
    先上结论:    在前端开发中,Cookie、Session和Token是三种常用的技术,用于管理用户的认证和状态。初级程序员第一次看到这些概念第一眼一定是懵逼的,不知道如何使用,它们其实各自具有不同的特性和应用场景,共同构成了前端应用中用户会话管理的基石。下面将详细解析这三种技......
  • 基于nodejs+vue悦全公司人事管理系统[开题+源码+程序+论文]计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容一、选题背景关于人事管理系统的研究,现有研究主要集中在通用型人事管理系统的构建与功能实现上,如常见的用户管理、数据备份等功能的实现 [2] 。专门针对悦全公司这......
  • Multi-criteria Token Fusion with One-step-ahead Attention for Efficient Vision T
    对于高效的ViT架构,近期研究通过对剪枝或融合多余的令牌减少自注意力层的二次计算成本。然而这些研究遇到了由于信息损失而导致的速度-精度平衡问题。本文认为令牌之间的不同关系以最大限度的减少信息损失。本文中提出了一种多标准令牌融合(Multi-criteriaTokenFusion),该融合......
  • 基于nodejs+vue月嫂服务管理系统[开题+源码+程序+论文]计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容一、选题背景关于家政服务管理系统的研究,现有研究多集中在家政服务的整体管理框架方面,如[2]中对家政服务管理系统的一般性设计与实现的探讨。然而专门针对月嫂服务管......
  • 基于nodejs的Web的牛场管理系统(源码+文档+部署讲解等)
    课题简介基于nodejs的Web的牛场管理系统是一款针对牛场运营管理的综合性系统,包含源码、文档和部署讲解。它涵盖牛只信息管理(包括品种、年龄、性别、健康状况、繁殖记录等)、饲料管理(饲料种类、库存、投喂计划和记录)、疾病防控(疾病诊断、治疗记录、疫苗接种计划)、养殖环......