Token详解
¶前后端token的生成,发送和验证流程
Token 全面解析:概念、安全设计、存储方案与当前适用性
Token(令牌)是计算机系统中用于身份验证、授权或信息交换的一串字符序列,本质是 “临时凭证”—— 替代传统密码或 Session ID,在客户端与服务端之间传递 “信任信息”。它广泛应用于前后端分离、微服务、跨域系统及移动端应用中,是现代身份认证体系的核心组件之一。
¶一、Token 基础详解
要理解 Token,需先明确其核心定位、与传统方案的差异及常见类型,避免概念混淆。
¶1. Token 的核心定义与作用
Token 的核心是 “无状态信任凭证”:服务端通过加密 / 签名生成 Token 后,无需在本地存储用户状态(如 Session 的服务器内存存储),只需通过 Token 本身的校验(签名、有效期等)即可确认用户身份或权限。
其核心作用包括:
- 身份认证:验证 “你是谁”(如登录后生成 Token,后续请求携带 Token 证明身份);
- 权限授权:验证 “你能做什么”(如 Token 中包含 “只读”“管理员” 等权限标识);
- 信息传递:安全携带非敏感业务信息(如用户 ID、角色),减少数据库查询。
¶2. Token 与传统 Session 的核心区别
传统 Session(会话)与 Token 是两种主流认证方案,差异主要体现在 “状态存储” 和 “扩展性” 上,具体对比如下:
| 对比维度 | Session(会话) | Token(令牌) |
|---|---|---|
| 状态存储位置 | 服务端(内存 / 数据库 / 缓存) | 客户端(Cookie/LocalStorage 等) |
| 服务端依赖 | 需维护 Session 状态,分布式需共享 Session(如 Redis) | 无状态,仅需校验 Token 合法性 |
| 跨域支持 | 弱(依赖 Cookie,跨域需特殊配置) | 强(可在 Header/Body 中携带,无跨域限制) |
| 移动端适配 | 差(移动端无 Cookie 默认存储,需手动处理) | 优(可存储在 App 本地,灵活携带) |
| 扩展性 | 低(服务端状态存储限制集群扩容) | 高(微服务 / 多服务可直接复用 Token) |
¶3. 常见 Token 类型
不同场景下的 Token 设计差异较大,主流类型分为三类:
¶(1)JWT(JSON Web Token)
最常用的 Token 格式,本质是 “带签名的 JSON 数据”,结构为Header.Payload.Signature(三点分隔的 Base64 编码字符串):
- Header:指定 Token 类型(JWT)和签名算法(如 HS256、RS256);
- Payload:存储 “声明”(非敏感信息,如用户 ID、过期时间
exp、角色role),Base64 编码可解码(不可存密码、手机号等敏感数据); - Signature:用 Header 指定的算法,结合 “密钥” 对 Header+Payload 签名,确保 Token 未被篡改。
适用场景:前后端分离、API 接口认证、短期权限校验(如 15 分钟有效期)。
¶(2)Access Token + Refresh Token(双令牌)
OAuth2.0/OpenID Connect 协议的核心设计,分 “短期访问令牌” 和 “长期刷新令牌”:
- Access Token:短期有效(如 5-30 分钟),用于接口授权,泄露风险低;
- Refresh Token:长期有效(如 7 天 - 30 天),仅用于获取新的 Access Token,不直接参与业务接口调用。
核心逻辑:Access Token 过期后,客户端用 Refresh Token 向 “令牌端点” 申请新的 Access Token,避免用户频繁登录。
适用场景:第三方登录(如微信、GitHub 登录)、移动端 App、长期登录态维持。
¶(3)Session Token(会话令牌)
传统 Session 的 “改良版”:服务端不存储完整 Session 数据,仅存储 Token 的 “校验信息”(如 Token 对应的用户 ID、过期时间),客户端携带 Token 后,服务端通过数据库 / Redis 查询校验。
特点:兼具 Session 的 “可吊销性”(服务端删除 Token 记录即可失效)和 Token 的 “弱状态性”,适合需强制登出的场景(如账号异常冻结)。
¶二、Token 安全设计的关键策略
Token 的安全性直接决定系统的身份认证防线强度,需从 “生成、传输、校验、销毁” 全生命周期设计,核心策略如下:
¶1. 确保 Token 本身的 “不可伪造性”
Token 被伪造是最致命的风险,需通过加密签名、复杂度设计规避:
-
使用非对称加密算法签名
:优先选择
1
RS256
(RSA 非对称加密)而非
1
HS256
(HMAC 对称加密)。
- 原理:私钥(仅服务端持有)用于生成 Token 签名,公钥(可公开)用于校验签名;即使公钥泄露,攻击者也无法伪造 Token(需私钥)。
-
足够长的字符长度:Token 长度至少 32 位(如 JWT 建议总长度≥128 字符),避免被暴力破解;
-
避免 “可预测性”:Token 需包含随机因子(如
nonce随机数、用户设备信息哈希),禁止用 “用户 ID + 时间戳” 直接生成(易被猜测)。
¶2. 严格控制 Token 有效期(防长期泄露)
Token 一旦泄露,有效期越长,风险越高,需按 “类型差异化设置”:
- Access Token:短期有效(5-30 分钟),业务接口仅接受此 Token,即使泄露,攻击者可用时间窗口极短;
- Refresh Token:长期有效但需 “可吊销”(服务端存储 Refresh Token 的黑名单 / 有效期,异常时立即失效);
- 强制过期机制:即使 Token 未泄露,超过最大有效期(如 Refresh Token 最长 30 天)也需重新登录,避免 “永久凭证”。
¶3. 传输过程:杜绝 “中间人窃取”
Token 在客户端→服务端的传输中,易被中间人拦截(如 HTTP 明文传输),需通过以下方式防护:
- 强制使用 HTTPS:所有携带 Token 的请求必须走 HTTPS(TLS 1.2+),加密传输内容,防止抓包窃取;
- 避免 URL 携带 Token:URL 中的 Token 会被浏览器历史记录、服务器日志记录,应放在
Authorization请求头(如Bearer <token>)或请求体中; - 禁用 HTTP 缓存:在响应头添加
Cache-Control: no-store和Pragma: no-cache,禁止浏览器缓存 Token。
¶4. 内容设计:最小权限与无敏感信息
Token 的 Payload(如 JWT)应遵循 “最小权限原则”,避免冗余信息泄露:
- 不存敏感数据:Payload 用 Base64 编码(可解码),禁止存储密码、手机号、身份证号等;仅存用户 ID、角色、过期时间等非敏感标识;
- 最小权限标识:Token 中仅包含当前场景必需的权限(如 “订单查询” 权限,不包含 “订单修改”),避免权限滥用;
- 添加 “环境绑定” 信息:在 Token 中嵌入客户端唯一标识(如设备 ID
deviceId、IP 哈希、浏览器 UA 哈希),校验时对比当前请求环境,不一致则拒绝(防 Token 在其他设备复用)。
¶5. 防重放攻击(Replay Attack)
攻击者窃取 Token 后,重复发送请求伪造操作(如重复支付),需通过以下机制防护:
- 添加时间戳(timestamp):Token 中包含生成时间,服务端校验时判断 “当前时间 - Token 时间戳” 是否超过阈值(如 5 分钟),超过则拒绝;
- 添加随机数(nonce):Token 中包含唯一随机数,服务端存储已使用的 nonce(如 Redis,有效期与时间戳阈值一致),重复 nonce 直接拒绝;
- 业务层幂等设计:即使 Token 被重放,业务接口需通过 “订单号唯一”“操作 ID 唯一” 等机制,避免重复执行(如支付接口仅允许同一订单支付一次)。
¶6. Refresh Token 的额外安全措施
Refresh Token 是 “长期凭证”,泄露风险更高,需额外防护:
- 独立存储与访问控制:Refresh Token 不与 Access Token 存同一位置(如 Access Token 存内存,Refresh Token 存加密的本地数据库);
- 定期轮换(Rotation):每次用 Refresh Token 获取新 Access Token 时,同时返回新的 Refresh Token,旧的立即失效(避免 Refresh Token 长期不变);
- 吊销机制(Revocation):服务端维护 Refresh Token 黑名单(如 Redis),用户登出、账号冻结、密码修改时,立即将对应的 Refresh Token 加入黑名单,拒绝后续使用。
¶三、Token 的最佳存储位置
Token 的存储位置直接影响其安全性,不同存储方案的风险(如 XSS、CSRF)差异极大,需结合 “应用类型” 选择。
¶1. 主流存储位置对比
不同存储位置的安全性、可用性及适用场景如下表:
| 存储位置 | 安全性(防 XSS/CSRF) | 可用性(持久化 / 跨页面) | 适用场景 | 核心风险点 |
|---|---|---|---|---|
| HttpOnly Cookie | 高(防 XSS:JS 无法访问;需配置 SameSite 防 CSRF) | 中(自动携带,关闭浏览器后失效或持久化) | 传统后端渲染(SSR)、需防 XSS 的场景 | 未配置 SameSite 易受 CSRF 攻击 |
| LocalStorage | 低(JS 可访问,易受 XSS 攻击窃取) | 高(永久存储,跨页面共享) | 无敏感操作的静态页面(如博客) | XSS 攻击导致 Token 泄露 |
| SessionStorage | 低(JS 可访问,易受 XSS 攻击) | 低(仅当前标签页,关闭即失) | 临时会话(如一次性操作) | 标签页关闭后需重新获取 |
| 内存存储(如 SPA 的 Vue/React 状态) | 高(JS 可访问但页面刷新 / 关闭后失,XSS 需实时注入) | 低(页面刷新 / 关闭即失) | 前后端分离(SPA)、移动端 App | 页面刷新需重新获取 Token |
| 加密的本地数据库(如 App 的 SQLCipher) | 高(需解密密钥,JS / 普通应用无法访问) | 高(持久化存储) | 移动端 App(iOS/Android)、桌面应用 | 设备 root / 越狱后可能泄露 |
¶2. 不同场景的存储推荐
¶(1)前后端分离(SPA,如 Vue/React)
- Access Token:存储在内存(如 Vuex/Redux 状态),页面刷新后通过 Refresh Token 重新获取;
理由:内存存储无持久化,XSS 攻击需 “实时注入 JS”(难度高),且避免 LocalStorage 的 XSS 泄露风险。 - Refresh Token:存储在HttpOnly + Secure + SameSite=Strict/Lax 的 Cookie;
理由:HttpOnly 防止 JS 访问(防 XSS),SameSite=Strict 禁止跨域请求携带(防 CSRF),Secure 仅 HTTPS 传输。
¶(2)移动端 App(iOS/Android)
- Access Token:存储在内存(如 App 的全局变量),退出 App 后失效;
- Refresh Token:存储在加密的本地数据库(如 iOS 的 Keychain、Android 的 EncryptedSharedPreferences);
理由:移动端无浏览器 Cookie 的 CSRF 风险,加密存储可防设备被 root / 越狱后的数据窃取。
¶(3)传统后端渲染(SSR,如 JSP/PHP)
- Token(或 Session Token):存储在HttpOnly + Secure + SameSite=Strict 的 Cookie;
理由:后端渲染场景下,Cookie 会自动携带到请求中,无需手动处理;HttpOnly 防 XSS,SameSite 防 CSRF,契合传统应用的使用习惯。
¶(4)第三方登录(OAuth2.0)
- Access Token:存储在内存(短期有效,用完即弃);
- Refresh Token:存储在服务端数据库(仅客户端持有 “Refresh Token ID”,服务端通过 ID 查询真实 Token);
理由:第三方登录需严格控制 Refresh Token 泄露风险,服务端存储可直接吊销,避免客户端存储的安全隐患。
¶四、当下使用 Token 是否仍为好选择?
截至 2024 年,Token 仍是主流身份认证方案,但需客观看待其优势与局限性,结合场景判断是否适用。
¶1. Token 的核心优势(为何仍被广泛使用)
- 无状态,适配分布式 / 微服务:服务端无需存储用户状态,微服务集群可直接通过 Token 校验身份,无需共享 Session(如 Redis),降低架构复杂度;
- 跨域与多端兼容:Token 可在 Header/Body 中携带,完美支持跨域(如前端部署在 CDN,后端在另一域名),且适配移动端、桌面端、小程序等多终端;
- 轻量高效:Token(如 JWT)可直接携带用户权限信息,减少服务端查询数据库的次数,提升接口响应速度;
- 成熟生态:OAuth2.0、OpenID Connect 等基于 Token 的协议已成为行业标准,第三方登录(微信、支付宝、GitHub)均支持,开发成本低。
¶2. Token 的局限性(需规避的问题)
- JWT 无法实时吊销:JWT 的有效性仅依赖签名和有效期,一旦生成,服务端无法主动 “废除”(除非维护黑名单),不适合需 “立即登出” 的场景(如账号被盗);
- 存储风险不可完全规避:客户端存储(如 Cookie、LocalStorage)仍面临 XSS/CSRF 风险,需额外配置防护(如 HttpOnly、SameSite);
- Refresh Token 管理复杂:双令牌方案需维护 Refresh Token 的生命周期(轮换、吊销、黑名单),增加服务端开发成本;
- 不适合高安全场景:对于金融、医疗等超高安全需求,Token 需结合多因素认证(MFA,如短信验证码、U 盾),单纯 Token 不足以满足安全要求。
¶3. 当下结论:Token 仍是 “优选方案”,但需 “正确使用”
在当前的技术生态中,Token 仍是前后端分离、微服务、多端适配场景下的最优解之一,但需满足两个前提:
- 严格遵循安全设计原则:如短期 Access Token + 可吊销 Refresh Token、HTTPS 传输、HttpOnly 存储、非对称签名等;
- 结合场景选择类型:高安全场景用 “双令牌 + 服务端黑名单”,轻量场景用 JWT,传统后端用 Session Token。
替代方案对比:目前 Passkey(基于 WebAuthn 的无密码认证)是新兴方向,通过设备指纹(如指纹识别、Face ID)替代 Token,安全性更高,但兼容性仍不足(部分旧浏览器 / 设备不支持),短期内无法完全替代 Token。
¶总结
Token 是现代身份认证体系的核心,其价值在于 “无状态、跨域、多端适配”,但安全性依赖 “设计 + 存储” 的双重保障:
- 安全设计:非对称签名、短期有效期、HTTPS 传输、防重放机制、Refresh Token 轮换与吊销;
- 存储选择:SPA 优先 “内存 + HttpOnly Cookie”,移动端优先 “内存 + 加密本地库”,传统后端优先 “HttpOnly Cookie”;
- 当下适用性:Token 仍是主流方案,需结合安全措施使用,未来会与 Passkey 等新兴方案互补,而非被完全替代。
要理解 Token 的无状态,首先需要结合 Web 开发中 “状态” 的定义,以及 Token 与传统认证方式(如 Session)的核心差异。简单来说,Token 的无状态是指:服务器无需存储与 Token 关联的用户状态信息,仅通过 Token 本身携带的内容和预设规则,就能完成身份验证和权限判断。
¶一、先明确:什么是 “状态”?
在 Web 交互中,“状态” 指的是 服务器需要记忆的、与用户会话相关的信息。比如:
- 用户是否已登录?
- 用户的 ID、角色、权限范围是什么?
- 会话的有效期到什么时候?
传统的 Session 认证 是 “有状态” 的典型代表:
- 用户登录成功后,服务器会创建一个 Session(包含用户 ID、权限等信息),并生成一个 Session ID;
- 服务器需要将 Session 存储在内存、数据库或缓存中(比如 Redis);
- 后续用户请求时,需携带 Session ID,服务器要先通过 Session ID 查询存储的 Session,才能确认用户身份和权限。
这种模式下,服务器必须 “记住” Session 的存在 —— 这就是 “有状态” 的核心问题:分布式部署时需同步 Session(否则用户换服务器就会重新登录),且服务器存储压力随用户量增长而增加。
¶二、Token 的无状态:核心是 “状态藏在 Token 里,而非服务器”
Token 的无状态本质是 将原本需要服务器存储的 “用户会话状态”,直接编码到 Token 本身。服务器验证时,无需查询任何外部存储(数据库、缓存等),仅通过 Token 自带的信息和预设的验证规则(如签名校验),就能独立完成身份确认。
以最常用的 JWT(JSON Web Token) 为例,其结构完美体现了无状态特性:
JWT 由三部分组成(用.分隔):Header.Payload.Signature,每部分都与 “无状态验证” 直接相关:
| 部分 | 作用(核心是 “携带状态 + 确保不可篡改”) |
|---|---|
| Header | 声明 Token 的类型(如 JWT)和签名算法(如 HS256、RS256),告诉服务器 “用什么规则验证我”。 |
| Payload | 存储 “用户会话状态” 的核心数据(称为 “Claim”),比如: - sub:用户 ID(Subject) - role:用户角色 - exp:Token 过期时间(Expiration Time) 这些数据是明文编码(Base64URL),服务器可直接解析,无需查库。 |
| Signature | 用 Header 指定的算法,结合服务器的 “密钥”(对称密钥 HS256)或 “私钥”(非对称密钥 RS256),对 Header 和 Payload 的拼接字符串进行签名。 作用:确保 Token 未被篡改(一旦 Payload 被改,签名会失效)。 |
¶Token 无状态验证的完整流程(以 JWT 为例):
-
用户登录:用户提交账号密码,服务器验证通过后,生成 JWT(将用户 ID、角色、过期时间等状态编码到 Payload,用密钥签名),并返回给客户端;
-
客户端存储:客户端将 JWT 存到 LocalStorage、SessionStorage 或 Cookie 中;
-
后续请求:客户端每次请求时,在 Header(如
Authorization: Bearer <JWT>)中携带 Token; -
服务器验证(无状态关键步骤)
:
- 服务器接收到 Token 后,先拆分出 Header、Payload、Signature;
- 用 Header 指定的算法和自己的密钥,重新计算 Header+Payload 的签名,并与 Token 中的 Signature 对比(校验是否被篡改);
- 解析 Payload,检查
exp字段(判断 Token 是否过期); - 从 Payload 中直接读取用户 ID、角色等信息,完成身份和权限判断。
整个过程中,服务器 没有存储任何与该 Token 相关的信息—— 所有必要的 “状态” 都在 Token 里,验证仅依赖 Token 本身和服务器预设的密钥。这就是 Token 无状态的核心。
¶三、Token 无状态的关键特性(与有状态对比)
- 服务器无需存储会话信息
无需像 Session 那样维护 Session 池,减轻服务器存储压力(尤其适合高并发、大用户量场景)。 - 天然支持分布式 / 微服务
由于服务器无需共享会话状态,多台服务器(或微服务节点)只要持有相同的密钥,就能独立验证 Token。用户请求无论路由到哪台服务器,都能正常通过验证,无需同步 Session(解决了 Session 的分布式痛点)。 - 验证过程 “自包含”
验证不依赖外部存储(数据库、缓存),仅需本地计算(签名校验、过期判断),响应速度更快。 - 状态信息的 “只读性”(依赖签名)
虽然 Payload 是 Base64URL 编码(可解码),但由于 Signature 的存在,客户端无法篡改 Payload 中的状态信息(一旦篡改,服务器校验签名会失败),确保了状态的可信度。
¶四、常见误区:“无状态”≠“不可控”
很多人误以为 Token 的无状态意味着 “一旦签发就无法撤销”(比如用户登出后,Token 仍在有效期内),但这并非 “无状态” 本身的问题,而是原生 JWT 的局限性。实际上,我们可以通过 “补充机制” 在保持核心无状态的同时,实现可控性:
- 短期 Token + 刷新 Token:将访问 Token 设为短期(如 15 分钟),过期后用长期的 “刷新 Token” 重新获取,即使访问 Token 泄露,风险窗口也很小;
- Token 黑名单:对需要紧急撤销的 Token(如用户登出、账号异常),可将其加入黑名单(存储在 Redis 中,设置与 Token 过期时间一致的 TTL)。服务器验证时,先查黑名单(仅这一步依赖外部存储,但核心验证仍无状态),再做签名和过期校验。
¶五、总结
Token 的无状态是其核心优势,本质是 将 “服务器需记忆的会话状态” 转移到 Token 本身,通过签名确保状态不可篡改,服务器仅靠 Token 和密钥就能独立完成验证。这种特性让 Token 在分布式系统、微服务架构中极具优势,也大幅降低了服务器的存储和同步压力。
简单来说:Session 是 “服务器记着你是谁”,Token 是 “你带着证明自己是谁的卡片,服务器看卡片就知道”—— 这张 “卡片” 里的信息,就是 Token 无状态的核心。
¶后端实现(Node.js + Express)
后端将提供登录接口(生成 token)和一个受保护的接口(验证 token)。
1 | const express = require('express'); |
¶前端实现(HTML + JavaScript)
前端页面将包含登录表单和获取受保护数据的功能。
1 |
|
¶代码说明
- 后端:
- 使用 Express 框架创建简单的 API 服务
/login接口验证用户凭据并生成 JWT 令牌authenticateToken中间件用于验证请求中的 token/protected接口是受保护的资源,只有携带有效 token 的请求才能访问
- 前端:
- 提供登录表单用于获取 token
- 将获取到的 token 存储在 localStorage 中
- 请求受保护资源时在 Authorization 头中携带 token
- 处理各种响应和错误情况,更新 UI 显示状态
这个示例展示了 token 认证的基本流程,在实际生产环境中,还需要考虑更多安全措施,如使用 HTTPS、更安全的 token 存储方式、更复杂的密钥管理等。