LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

双 Token(Access Token + Refresh Token)认证机制:从原理到实践的完整实现

admin
2025年9月26日 9:50 本文热度 522
在现代 Web 应用中,用户认证是保障系统安全的核心环节。随着前后端分离架构的普及,传统的 Session 认证方式逐渐被 Token 认证取代。其中,「双 Token(Access Token + Refresh Token)机制」凭借其在安全性与用户体验之间的出色平衡,成为主流的认证方案。本文将结合完整的 Express 后端与 Vue 前端代码,详细解析双 Token 机制的实现原理与实践细节。

双 Token 机制的核心原理

双 Token 机制通过两种不同特性的令牌协同工作,解决了 "安全性" 与 "用户体验" 之间的矛盾。其核心设计思想是:

  • 「Access Token(访问令牌)」 :短期有效,用于直接访问受保护资源,有效期通常设置为几分钟(示例中为 12 秒,仅用于演示)
  • 「Refresh Token(刷新令牌)」 :长期有效,仅用于获取新的 Access Token,有效期可设置为几天甚至几周(示例中为 7 天)

这种设计的优势在于:当 Access Token 被盗取时,攻击者仅有很短的时间窗口可以利用;而 Refresh Token 虽然长期有效,但通常存储在更安全的环境中,且一旦发现异常可立即吊销。

后端实现:Express 框架下的双 Token 系统

后端作为 Token 的签发者和验证者,承担着整个认证系统的核心逻辑。以下从初始化配置、核心工具函数、认证中间件到具体接口,逐步解析实现过程。

基础配置与依赖

const express require('express');const cors require('cors');const cookieParser require('cookie-parser');const app express();app.use(cors({    origin: [        'http://localhost:5173''http://localhost:5174'        'http://localhost:5175''http://localhost:5176'    ],    credentialstrue // 允许跨域请求携带Cookie}));app.use(express.json());app.use(cookieParser());

这段代码完成了三件关键工作:

  1. 引入 Express 框架及必要中间件(cors 处理跨域,cookie-parser 解析 Cookie)
  1. 配置跨域规则,允许指定前端域名访问并支持跨域携带 Cookie
  1. 启用 JSON 请求体解析和 Cookie 解析功能,为后续处理打好基础

令牌存储与核心工具函数

// 内存存储(生产环境应使用Redis或数据库)const accessTokens = new Map();const refreshTokens = new Map();// 获取当前时间戳(秒)function now() {    return Math.floor(Date.now() / 1000);}// 生成带前缀的随机令牌function getRandom(prefix) {    return `${prefix}-${Math.random().toString(26).slice(2)}${Date.now()}`;}

为模拟令牌存储,示例使用 Map 对象临时存储令牌信息。在生产环境中,应替换为 Redis 等分布式存储系统,以支持多实例部署和令牌过期自动清理。

令牌签发函数

function getAccessToken(userId, ttlSec = 12{    const at getRandom('AccessToken');    accessTokens.set(at, { userId, expiresInnow() + ttlSec });    return at;}function getRefreshToken(userId, ttlSec = 3600 * 24 * 7{    const rt getRandom('RefreshToken');    refreshTokens.set(rt, {         userId,         expiresInnow() + ttlSec,         revokedfalse     });    return rt;}

这两个函数分别负责生成 Access Token 和 Refresh Token:

  • 为令牌添加前缀便于区分类型
  • 记录令牌关联的用户 ID 和过期时间
  • 为 Refresh Token 额外添加 "revoked" 状态,支持主动吊销

令牌验证与吊销函数

function verifyAccessToken(at) {    const result = accessTokens.get(at);    if (!result || result.expiresIn <= now()) return null;    return result.userId;}function verifyRefreshToken(rt) {    const result = refreshTokens.get(rt);    if (!result || result.revoked || result.expiresIn <= now()) return null;    return result.userId;}function revokeRefreshToken(rt) {    const result = refreshTokens.get(rt);    if (result) result.revoked = true;}

验证函数通过检查令牌是否存在、是否过期、是否被吊销等状态,决定是否返回有效的用户 ID。这种设计确保了只有符合条件的令牌才能通过验证。

认证中间件:请求的第一道防线

app.use((req, res, next) => {    // 登录、刷新、登出接口跳过验证    if (['/auth/login''/auth/refresh''/auth/logout''/login'].includes(req.path)) {        return next();    }    // 从请求头获取AT    const token = req.headers.token || '';    const userId verifyAccessToken(token);    if (userId) {        req.userId = userId; // 挂载用户ID到请求对象        return next();    }    // 验证失败    res.send({ status401, msg"未登录或令牌过期" });});

这个中间件实现了 "守门人" 功能:

  • 对登录、刷新、登出等特殊接口直接放行
  • 对其他所有请求验证 Access Token 的有效性
  • 验证通过则将用户 ID 挂载到请求对象,供后续接口使用
  • 验证失败则返回 401 错误,提示客户端进行处理

核心接口实现

登录接口:令牌的初始发放

app.post('/auth/login', (req, res) => {    const { username } = req.body || {};    const userId = username || 'demoUser';    const at getAccessToken(userId, 12);    const rt getRefreshToken(userId);    // 将RT存入httpOnly Cookie    res.cookie('rt', rt, {        httpOnlytrue, // 禁止前端JS访问,防XSS攻击        sameSite'lax', // 限制跨站请求携带,防CSRF攻击        securefalse, // 本地开发为false,生产需设为true        path'/',        maxAge7 * 24 * 3600 * 1000    });    res.send({ status200, data: at });});

登录接口是用户获取初始令牌的入口:

  1. 接收用户身份信息(示例简化为用户名)
  1. 生成 Access Token 和 Refresh Token
  1. 将 Access Token 直接返回给前端(通常存储在 localStorage)
  1. 将 Refresh Token 存入 httpOnly Cookie,提升安全性

特别注意 Cookie 的配置:httpOnly: true防止前端 JavaScript 访问,有效抵御 XSS 攻击;sameSite: 'lax'限制跨站请求携带 Cookie,降低 CSRF 攻击风险。

令牌刷新接口:无感续期的关键

app.post('/auth/refresh', (req, res) => {    const rt = req.cookies.rt; // 从Cookie获取RT    if (!rt) return res.send({ status401, msg'无刷新令牌' });    const userId verifyRefreshToken(rt);    if (!userId) return res.send({ status401, msg'刷新令牌失效' });    // 令牌旋转:吊销旧RT,生成新RT和新AT    revokeRefreshToken(rt);    const newRt getRefreshToken(userId);    const newAt getAccessToken(userId);    // 写入新RT到Cookie,返回新AT    res.cookie('rt', newRt, { ... });    res.send({ status200, data: newAt });});

刷新接口实现了 Token 的无感续期:

  1. 从 Cookie 中获取 Refresh Token 并验证其有效性
  1. 采用 "令牌旋转" 机制:吊销旧的 Refresh Token,生成新的一对令牌
  1. 将新的 Refresh Token 存入 Cookie,新的 Access Token 返回给前端

令牌旋转机制大幅提升了安全性,即使 Refresh Token 被盗取,攻击者也只能使用一次。

登出接口:安全终止会话

app.post('/auth/logout', (req, res) => {    const rt = req.cookies.rt;    if (rt) revokeRefreshToken(rt); // 吊销RT    res.clearCookie('rt', { path'/' }); // 清除Cookie中的RT    res.send({ status200, msg'已登出' });});

登出接口通过吊销 Refresh Token 并清除 Cookie,确保用户会话被安全终止,防止后续被恶意使用。

前端实现:Vue 中的令牌管理

前端作为令牌的持有者和使用方,需要妥善处理令牌的存储、传递和刷新逻辑。以下从路由守卫、请求拦截器到页面组件,解析前端实现细节。

路由守卫:控制页面访问权限

router.beforeEach((to, from, next) => {    // 处理URL中的token参数    const token = to.query.token;    if (token) {        localStorage.setItem("token", token);        next({ path: to.pathquery: {} });        return;    }    // 检查是否需要认证    if (to.meta.requiresAuth) {        const currentToken = localStorage.getItem('token');        if (!isValidToken(currentToken)) {            // 没有有效token,跳转到登录中心            window.open(`http://localhost:5174/login?resource=${window.location.origin}${to.path}`);            return;        }    }    next();})

路由守卫实现了页面级别的访问控制:

  • 处理 URL 中携带的 token 参数,存储到 localStorage
  • 对标记为需要认证的路由(如/about),检查 token 有效性
  • 没有有效 token 时,引导用户到登录页面

Axios 拦截器:自动处理令牌

// 请求拦截器:添加token到请求头request.interceptors.request.use((config) => {    const token = localStorage.getItem("token");    config.headers = config.headers || {}    if (isValidToken(token)){        config.headers.token = token;    }else{        localStorage.removeItem('token');    }    return config;})// 响应拦截器:处理token过期request.interceptors.response.use(async (res) => {    if (res.data && res.data.status === 401) {        const original = res.config || {}        if (original._retried) {            // 已重试过仍失败,跳登录中心            window.open(`http://localhost:5174/login?resource=${window.location.origin}`)            return res;        }        original._retried = true        if (!isRefreshing) {            isRefreshing = true            refreshPromise = request.post('/auth/refresh', {})                .then(r => {                    if (r.data && r.data.status === 200) {                        const newToken = r.data.data                        localStorage.setItem('token', newToken)                        return newToken                    }                    throw new Error('refresh failed')                })                .catch(() => {                    localStorage.removeItem('token')                    throw new Error('refresh failed')                })                .finally(() => {                    isRefreshing = false                })        }        try {            const newToken = await refreshPromise            original.headers.token = newToken            return request(original)        } catch (e) {            window.open(`http://localhost:5174/login?resource=${window.location.origin}`)            return res        }    }    return res;})

拦截器是实现 "无感刷新" 的核心:

  • 请求拦截器自动为每个请求添加 Access Token
  • 响应拦截器在收到 401 错误时,自动尝试刷新令牌
  • 使用isRefreshing和refreshPromise避免并发刷新请求
  • 刷新成功则用新令牌重试原请求,失败则引导用户重新登录

登录页面组件:处理身份验证

<script setup>import request from "../server/request";import { useRoute } from "vue-router";import { watch, ref } from "vue";const route = useRoute();const resource = ref("");const token = localStorage.getItem("token");function windowPostMessage(token, resource) {  if (window.opener) {    window.opener.postMessage({ token }, resource.value)  }}watch(  () => route.query.resource,  (val) => {    resource.value = val ? decodeURIComponent(val) : "";    if (token) {      windowPostMessage(token, resource.value)    }  },  { immediatetrue });function login() {  request.get("/auth/login").then((res) => {    const apitoken = res.data.data;    localStorage.setItem("token", apitoken);    windowPostMessage(apitoken, resource.value)    window.location.href = `${resource.value}?token=${apitoken}`;    window.close()  });}</script>

登录页面处理用户身份验证流程:

  • 通过 URL 参数接收跳转来源(resource)
  • 登录成功后,通过 postMessage 通知父窗口
  • 将新令牌通过 URL 参数传递给来源页面
  • 关闭登录窗口,完成登录流程

双 Token 机制操作流程演示

为了更直观地理解双 Token 机制的实际运行过程,以下是一个 GIF 演示,展示了从用户登录到令牌刷新、访问资源以及登出的完整操作流程:

演示内容说明:

  1. 用户未登录时在Home页面跳转登录页面后输入信息并登录,前端获取 Access Token 并存储,Refresh Token 通过 Cookie 存储
  1. 登录后访问受保护资源/api1,请求头携带 Access Token,成功获取资源
  1. 等待 Access Token 过期后再次访问/api1,前端拦截 401 错误,自动调用刷新接口获取新令牌
  1. 使用新的 Access Token 重新请求/api1,成功获取资源,用户无感知
  1. 点击登出按钮,前端清除本地存储的 Access Token,后端吊销 Refresh Token 并清除 Cookie

通过这个演示可以清晰看到,整个过程中用户无需多次输入账号密码,在 Access Token 过期时实现了无感续期,既保证了安全性又提升了用户体验。

双 Token 机制的安全性考量

双 Token 机制的安全性建立在多个层面的防护措施上:

  1. 「令牌存储安全」
  • Access Token 存储在 localStorage,便于前端管理但存在 XSS 风险
  • Refresh Token 存储在 httpOnly Cookie,防止前端 JS 访问,抵御 XSS 攻击
  1. 「通信安全」
  • 生产环境应启用 HTTPS,防止令牌在传输过程中被窃听
  • 合理设置 Cookie 的 secure 属性,确保仅通过 HTTPS 传输
  1. 「令牌生命周期」
  • Access Token 短期有效,减少被盗用后的风险窗口
  • Refresh Token 长期有效但支持主动吊销,平衡安全性与用户体验
  1. 「防御机制」
  • 令牌旋转机制确保 Refresh Token 只能使用一次
  • sameSite Cookie 属性降低 CSRF 攻击风险
  • 严格的令牌验证逻辑防止无效令牌被使用

总结与扩展

双 Token 机制通过 Access Token 和 Refresh Token 的协同工作,在安全性和用户体验之间取得了出色的平衡。本文提供的完整代码实现了从令牌签发、验证、刷新到登出的全流程,包含了前端和后端的关键处理逻辑。

在实际应用中,还可以进一步扩展:

  • 使用 Redis 等分布式存储替换内存存储,支持集群部署
  • 实现令牌黑名单机制,处理已吊销但未过期的令牌
  • 添加令牌撤销通知,在用户修改密码等场景立即失效所有令牌
  • 结合 JWT(JSON Web Token)实现无状态令牌验证,减轻服务器负担

通过理解和实践双 Token 机制,开发者可以为 Web 应用构建更加安全、可靠的认证系统,为用户提供流畅的使用体验同时保障系统安全。


阅读原文:https://mp.weixin.qq.com/s/vFR_hNx_yBCbdHdiW3kHFA


该文章在 2025/9/26 9:52:54 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved