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-storePragma: no-cache,禁止浏览器缓存 Token。

4. 内容设计:最小权限与无敏感信息

Token 的 Payload(如 JWT)应遵循 “最小权限原则”,避免冗余信息泄露:

  • 不存敏感数据:Payload 用 Base64 编码(可解码),禁止存储密码、手机号、身份证号等;仅存用户 ID、角色、过期时间等非敏感标识;
  • 最小权限标识:Token 中仅包含当前场景必需的权限(如 “订单查询” 权限,不包含 “订单修改”),避免权限滥用;
  • 添加 “环境绑定” 信息:在 Token 中嵌入客户端唯一标识(如设备 IDdeviceId、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 仍是前后端分离、微服务、多端适配场景下的最优解之一,但需满足两个前提:

  1. 严格遵循安全设计原则:如短期 Access Token + 可吊销 Refresh Token、HTTPS 传输、HttpOnly 存储、非对称签名等;
  2. 结合场景选择类型:高安全场景用 “双令牌 + 服务端黑名单”,轻量场景用 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 认证 是 “有状态” 的典型代表:

  1. 用户登录成功后,服务器会创建一个 Session(包含用户 ID、权限等信息),并生成一个 Session ID;
  2. 服务器需要将 Session 存储在内存、数据库或缓存中(比如 Redis);
  3. 后续用户请求时,需携带 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 为例):

  1. 用户登录:用户提交账号密码,服务器验证通过后,生成 JWT(将用户 ID、角色、过期时间等状态编码到 Payload,用密钥签名),并返回给客户端;

  2. 客户端存储:客户端将 JWT 存到 LocalStorage、SessionStorage 或 Cookie 中;

  3. 后续请求:客户端每次请求时,在 Header(如Authorization: Bearer <JWT>)中携带 Token;

  4. 服务器验证(无状态关键步骤)

    • 服务器接收到 Token 后,先拆分出 Header、Payload、Signature;
    • 用 Header 指定的算法和自己的密钥,重新计算 Header+Payload 的签名,并与 Token 中的 Signature 对比(校验是否被篡改);
    • 解析 Payload,检查exp字段(判断 Token 是否过期);
    • 从 Payload 中直接读取用户 ID、角色等信息,完成身份和权限判断。

整个过程中,服务器 没有存储任何与该 Token 相关的信息—— 所有必要的 “状态” 都在 Token 里,验证仅依赖 Token 本身和服务器预设的密钥。这就是 Token 无状态的核心。

三、Token 无状态的关键特性(与有状态对比)

  1. 服务器无需存储会话信息
    无需像 Session 那样维护 Session 池,减轻服务器存储压力(尤其适合高并发、大用户量场景)。
  2. 天然支持分布式 / 微服务
    由于服务器无需共享会话状态,多台服务器(或微服务节点)只要持有相同的密钥,就能独立验证 Token。用户请求无论路由到哪台服务器,都能正常通过验证,无需同步 Session(解决了 Session 的分布式痛点)。
  3. 验证过程 “自包含”
    验证不依赖外部存储(数据库、缓存),仅需本地计算(签名校验、过期判断),响应速度更快。
  4. 状态信息的 “只读性”(依赖签名)
    虽然 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const express = require('express');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const app = express();

// 中间件
app.use(cors());
app.use(express.json()); // 解析JSON请求体

// 密钥(实际生产环境中应存储在环境变量中)
const SECRET_KEY = 'your-secret-key-keep-it-safe';

// 模拟用户数据库
const users = [
{ id: 1, username: 'user1', password: 'password1' },
{ id: 2, username: 'user2', password: 'password2' }
];

// 登录接口 - 生成token
app.post('/login', (req, res) => {
const { username, password } = req.body;

// 查找用户
const user = users.find(u => u.username === username && u.password === password);

if (!user) {
return res.status(401).json({ message: '用户名或密码错误' });
}

// 生成JWT令牌
const token = jwt.sign(
{ userId: user.id, username: user.username }, // payload
SECRET_KEY, // 密钥
{ expiresIn: '1h' } // 过期时间
);

res.json({
message: '登录成功',
token: token,
user: { id: user.id, username: user.username }
});
});

// 验证token的中间件
const authenticateToken = (req, res, next) => {
// 从请求头获取token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

if (!token) {
return res.status(401).json({ message: '未提供token' });
}

// 验证token
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ message: 'token无效或已过期' });
}
req.user = user;
next();
});
};

// 受保护的接口 - 需要验证token
app.get('/protected', authenticateToken, (req, res) => {
res.json({
message: '这是受保护的数据',
user: req.user,
data: [
{ id: 1, name: '敏感数据1' },
{ id: 2, name: '敏感数据2' }
]
});
});

// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});

前端实现(HTML + JavaScript)

前端页面将包含登录表单和获取受保护数据的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Token认证示例</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-12 max-w-4xl">
<h1 class="text-3xl font-bold text-center mb-10 text-gray-800">
<i class="fa fa-key mr-2"></i>Token认证示例
</h1>

<div class="grid md:grid-cols-2 gap-8">
<!-- 登录表单 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h2 class="text-xl font-semibold mb-4 text-gray-700">
<i class="fa fa-sign-in mr-2"></i>登录
</h2>
<form id="loginForm" class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium text-gray-700">用户名</label>
<input type="text" id="username" name="username"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
required>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">密码</label>
<input type="password" id="password" name="password"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
required>
</div>
<button type="submit"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa fa-paper-plane mr-1"></i>登录
</button>
</form>
<div id="loginMessage" class="mt-4 text-sm hidden"></div>
<div id="tokenDisplay" class="mt-4 text-xs bg-gray-100 p-3 rounded break-all hidden">
<strong>Token:</strong> <span id="tokenValue"></span>
</div>
</div>

<!-- 受保护数据区域 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h2 class="text-xl font-semibold mb-4 text-gray-700">
<i class="fa fa-lock mr-2"></i>受保护数据
</h2>
<button id="fetchDataBtn"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 mb-4"
disabled>
<i class="fa fa-download mr-1"></i>获取受保护数据
</button>
<div id="dataMessage" class="mt-4 text-sm hidden"></div>
<div id="dataDisplay" class="mt-4 text-sm overflow-auto max-h-60 hidden">
<pre id="dataContent"></pre>
</div>
</div>
</div>

<!-- 状态信息 -->
<div class="mt-8 bg-white rounded-lg p-6 card-shadow">
<h2 class="text-xl font-semibold mb-4 text-gray-700">
<i class="fa fa-info-circle mr-2"></i>状态信息
</h2>
<div id="status" class="text-gray-600">
<p>请先登录获取token,然后才能访问受保护的数据。</p>
</div>
</div>
</div>

<script>
// DOM元素
const loginForm = document.getElementById('loginForm');
const loginMessage = document.getElementById('loginMessage');
const tokenDisplay = document.getElementById('tokenDisplay');
const tokenValue = document.getElementById('tokenValue');
const fetchDataBtn = document.getElementById('fetchDataBtn');
const dataMessage = document.getElementById('dataMessage');
const dataDisplay = document.getElementById('dataDisplay');
const dataContent = document.getElementById('dataContent');
const statusElement = document.getElementById('status');

// 后端API地址
const API_URL = 'http://localhost:3000';

// 检查本地存储中是否有token
const storedToken = localStorage.getItem('authToken');
if (storedToken) {
updateUIWithToken(storedToken);
}

// 登录表单提交
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();

const username = document.getElementById('username').value;
const password = document.getElementById('password').value;

try {
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.message || '登录失败');
}

// 存储token
localStorage.setItem('authToken', data.token);
updateUIWithToken(data.token);

showMessage(loginMessage, data.message, 'green');
statusElement.innerHTML = `<p class="text-green-600">已登录为: ${data.user.username}</p>`;

} catch (error) {
showMessage(loginMessage, error.message, 'red');
statusElement.innerHTML = `<p class="text-red-600">登录失败: ${error.message}</p>`;
}
});

// 获取受保护数据
fetchDataBtn.addEventListener('click', async () => {
const token = localStorage.getItem('authToken');

if (!token) {
showMessage(dataMessage, '未找到token,请先登录', 'red');
return;
}

try {
const response = await fetch(`${API_URL}/protected`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.message || '获取数据失败');
}

showMessage(dataMessage, '成功获取受保护数据', 'green');
dataContent.textContent = JSON.stringify(data, null, 2);
dataDisplay.classList.remove('hidden');

statusElement.innerHTML = `<p class="text-green-600">已成功使用token访问受保护资源</p>`;

} catch (error) {
showMessage(dataMessage, error.message, 'red');
dataDisplay.classList.add('hidden');

// 如果token无效,清除存储
if (error.message.includes('token无效') || error.message.includes('过期')) {
localStorage.removeItem('authToken');
updateUIWithoutToken();
statusElement.innerHTML = `<p class="text-red-600">${error.message},请重新登录</p>`;
}
}
});

// 辅助函数:显示消息
function showMessage(element, text, color) {
element.textContent = text;
element.classList.remove('hidden', 'text-green-600', 'text-red-600');
element.classList.add(`text-${color}-600`);
}

// 辅助函数:使用token更新UI
function updateUIWithToken(token) {
tokenValue.textContent = token;
tokenDisplay.classList.remove('hidden');
fetchDataBtn.disabled = false;
loginForm.reset();
}

// 辅助函数:没有token时更新UI
function updateUIWithoutToken() {
tokenDisplay.classList.add('hidden');
fetchDataBtn.disabled = true;
dataDisplay.classList.add('hidden');
}
</script>
</body>
</html>

代码说明

  • 后端:
    • 使用 Express 框架创建简单的 API 服务
    • /login接口验证用户凭据并生成 JWT 令牌
    • authenticateToken中间件用于验证请求中的 token
    • /protected接口是受保护的资源,只有携带有效 token 的请求才能访问
  • 前端:
    • 提供登录表单用于获取 token
    • 将获取到的 token 存储在 localStorage 中
    • 请求受保护资源时在 Authorization 头中携带 token
    • 处理各种响应和错误情况,更新 UI 显示状态

这个示例展示了 token 认证的基本流程,在实际生产环境中,还需要考虑更多安全措施,如使用 HTTPS、更安全的 token 存储方式、更复杂的密钥管理等。