认证鉴权——JWT 过期了,用户怎么办 / Authentication, Authorization, and JWT Expiration
📅 创建时间:2026-05-08 🏷️ 标签:#JWT #Session #OAuth2 #Token #幂等性 #接口安全 📚 前置知识:[[00-backend-overview]] [[06-network]](HTTP 基础) 📚 相关知识:[[/03-web/06-databases-and-data-access/05-redis]](Token 存储) [[10-cache-strategy]](缓存策略)
场景:用户正在支付,JWT 过期了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 用户在支付页面点了"确认支付",请求发出。 │
│ │
│ 服务端:401 Unauthorized │
│ 前端:用户的钱已经扣了,但页面显示"登录过期"! │
│ │
│ 用户投诉:钱扣了,但订单没创建。 │
│ │
│ 你被叫去排查。 │
│ │
└─────────────────────────────────────────────────────────────┘这一章,我们理解认证鉴权的原理和常见陷阱。
第1节:Session vs Token——两种身份验证方式
Session 模式
┌─────────────────────────────────────────────────────────────┐
│ Session 认证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户登录: │
│ 浏览器 ──▶ POST /login(用户名+密码)───▶ 服务端 │
│ 服务端验证通过 ──▶ 创建 Session ──▶ 存 Redis ──▶ 返回 SessionID │
│ │
│ 后续请求: │
│ 浏览器 ──▶ Cookie: session_id=xxx ──▶ 服务端 │
│ 服务端 ──▶ 查 Redis ──▶ 获取用户信息 ──▶ 处理请求 │
│ │
│ 优点: │
│ ✅ 可随时撤回(删 Redis 中的 Session 即可) │
│ ✅ Session 内容存在服务端,不怕泄露 │
│ ✅ 适合复杂业务(Session 可存大量数据) │
│ │
│ 缺点: │
│ ❌ 依赖 Redis(Redis 挂了 = 全部用户登出) │
│ ❌ 不适合分布式(Session 共享需要额外配置) │
│ ❌ 移动端不友好(Cookie 不方便) │
│ │
└─────────────────────────────────────────────────────────────┘JWT 模式
┌─────────────────────────────────────────────────────────────┐
│ JWT 认证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户登录: │
│ 浏览器 ──▶ POST /login(用户名+密码)───▶ 服务端 │
│ 服务端验证通过 ──▶ 生成 JWT ──▶ 返回给浏览器 │
│ │
│ JWT 结构: │
│ Header.Payload.Signature │
│ eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0. │
│ [Base64] [Base64] [HMAC-SHA256] │
│ │
│ 后续请求: │
│ 浏览器 ──▶ Header: Authorization: Bearer <jwt> ──▶ 服务端│
│ 服务端验证 JWT 签名 ──▶ 解析 Payload ──▶ 获取用户信息 │
│ │
│ 优点: │
│ ✅ 无状态(服务端不存任何东西) │
│ ✅ 不依赖 Redis │
│ ✅ 天然适合分布式 │
│ ✅ 移动端友好 │
│ │
│ 缺点: │
│ ❌ 无法撤回(发出去后,只能等它过期) │
│ ❌ Payload 是 Base64 编码,可被解码(不能存敏感信息) │
│ ❌ Token 越长,请求体积越大 │
│ │
└─────────────────────────────────────────────────────────────┘JWT 内部结构
python
import jwt
import datetime
# JWT 组成
payload = {
"sub": "user_1001", # 用户 ID
"name": "张三", # 用户名
"role": "admin", # 角色
"iat": 1715145600, # 签发时间
"exp": 1715152800, # 过期时间(2小时后)
}
# 签名
token = jwt.encode(payload, "secret_key", algorithm="HS256")
# Header + Payload + HMAC-SHA256(Header.Payload, secret)
# 解码(验证签名 + 检查过期)
decoded = jwt.decode(token, "secret_key", algorithms=["HS256"])
print(decoded["sub"]) # user_1001第2节:JWT 的三个致命问题
问题一:Token 过期了怎么办
python
# 错误做法:让用户重新登录
# 用户体验极差:支付到一半被踢出去
# 正确做法:Refresh Token
# Access Token:短期有效(15 分钟)
# Refresh Token:长期有效(7 天)
def login(username, password):
user = verify(username, password)
# Access Token:用于 API 调用,过期快
access_token = jwt.encode({
"sub": user.id,
"exp": datetime.datetime.now() + datetime.timedelta(minutes=15)
}, SECRET, algorithm="HS256")
# Refresh Token:用于获取新的 Access Token,过期慢
refresh_token = jwt.encode({
"sub": user.id,
"type": "refresh",
"exp": datetime.datetime.now() + datetime.timedelta(days=7)
}, REFRESH_SECRET, algorithm="HS256")
return access_token, refresh_token
def refresh_access_token(refresh_token):
# 验证 Refresh Token
payload = jwt.decode(refresh_token, REFRESH_SECRET, algorithms=["HS256"])
# 检查类型
if payload.get("type") != "refresh":
raise AuthError("Invalid token type")
# 生成新的 Access Token
new_access_token = jwt.encode({
"sub": payload["sub"],
"exp": datetime.datetime.now() + datetime.timedelta(minutes=15)
}, SECRET, algorithm="HS256")
return new_access_token问题二:Refresh Token 泄露了怎么办
用户 A 登录 → 获取了 access_token + refresh_token
黑客 B 拿到了 refresh_token
风险:B 可以用 refresh_token 获取新的 access_token解决方案:Refresh Token 轮换 + 追踪
python
# 方案:Refresh Token 只能使用一次
# 用户用 refresh_token 换取新的 access_token 时,
# 同时颁发新的 refresh_token,旧的一律作废
refresh_tokens = {} # {token: user_id} 存在 Redis
def use_refresh_token(old_refresh_token):
user_id = refresh_tokens.get(old_refresh_token)
if not user_id:
raise AuthError("Token 已失效,可能被盗用")
# 删除旧 token(只能用一次)
del refresh_tokens[old_refresh_token]
# 颁发新的 token 对
return generate_tokens(user_id)问题三:如何强制用户登出(JWT 无法撤回)
python
# 方案 1:黑名单(Redis)
blacklist = set() # 存在 Redis
def logout(token):
# 把 token 的 exp 加入黑名单
payload = jwt.decode(token, options={"verify_exp": False})
blacklist.add(payload["exp"])
# 每次验证 JWT 时,检查 exp 是否在黑名单中
# 方案 2:Token 版本号
user_token_version = {} # 存在 Redis: {user_id: version}
def verify_token(token):
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
user_id = payload["sub"]
# 检查版本号
if payload.get("version") != user_token_version.get(user_id, 0):
raise AuthError("Token 已失效")
return payload
def force_logout(user_id):
# 用户被封禁 → 版本号 +1 → 所有旧 token 失效
user_token_version[user_id] += 1第3节:OAuth2——第三方授权
场景:你的网站想让用户用微信登录
┌─────────────────────────────────────────────────────────────┐
│ │
│ 你(Client)想访问用户的微信好友列表(Protected Resource)│
│ │
│ 问题:用户不想把微信密码给你 │
│ 解决:OAuth2,让微信(Authorization Server)授权 │
│ │
└─────────────────────────────────────────────────────────────┘OAuth2 四种授权模式
┌─────────────────────────────────────────────────────────────┐
│ OAuth2 四种授权模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 授权码模式(Authorization Code)- 最安全,推荐! │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户点击"微信登录" │ │
│ │ 重定向到微信授权页面 │ │
│ │ 用户授权 │ │
│ │ 微信重定向回你的网站 + 授权码(code) │ │
│ │ 你的后端用 code + client_secret 换 Access Token │ │
│ │ 用 Access Token 访问用户数据 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 隐式授权(Implicit)- 不安全,已不推荐 │
│ 直接在 URL 返回 Access Token(可能被浏览器历史记录) │
│ │
│ 3. 密码凭证(Resource Owner Password Credentials) │
│ 用户直接给你用户名+密码,换 Access Token │
│ 适用:用户高度信任你的应用 │
│ │
│ 4. 客户端凭证(Client Credentials) │
│ 没有用户参与,服务端用 client_id + client_secret │
│ 适用:服务端之间的 API 调用 │
│ │
└─────────────────────────────────────────────────────────────┘第4节:接口幂等性——用户手抖点了两次下单
场景:用户网不好,连点了两下"确认支付"
┌─────────────────────────────────────────────────────────────┐
│ │
│ 用户点了两次"确认支付"。 │
│ │
│ 请求 1: POST /order/create {user:1, amount:100} │
│ 请求 2: POST /order/create {user:1, amount:100} │
│ │
│ 结果:创建了两个订单,用户被扣了两次钱。 │
│ │
└─────────────────────────────────────────────────────────────┘幂等性方案
┌─────────────────────────────────────────────────────────────┐
│ 接口幂等性方案对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方案 1:唯一订单号(幂等键) │
│ 客户端生成唯一 ID(UUID / 雪花 ID) │
│ 服务端存入 Redis,带过期时间 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 请求 1: POST /order idempotency_key=abc123 │ │
│ │ 请求 2: POST /order idempotency_key=abc123 │ │
│ │ │ │
│ │ 服务端: │ │
│ │ if redis.setnx("idempotent:abc123", 1, ex=300): │ │
│ │ create_order() │ │
│ │ else: │ │
│ │ return existing_order() # 返回已创建的订单 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 方案 2:数据库唯一索引 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CREATE TABLE orders ( │ │
│ │ id BIGINT PRIMARY KEY, │ │
│ │ idempotency_key VARCHAR(64) UNIQUE, │ │
│ │ ... │ │
│ │ ); │ │
│ │ │ │
│ │ INSERT ... ON CONFLICT (idempotency_key) DO NOTHING│ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 方案 3:状态机幂等 │
│ 订单状态:pending → paid → shipped → completed │
│ 只有 pending 才能 → paid │
│ 重复请求:已 paid 的订单再请求 paid → 返回原订单 │
│ │
└─────────────────────────────────────────────────────────────┘第5节:接口安全基础
┌─────────────────────────────────────────────────────────────┐
│ 接口安全 Checklist │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 参数校验 │
│ ✅ 永远不要相信前端数据 │
│ ✅ 校验类型、长度、范围、正则 │
│ ✅ SQL 注入:用参数化查询,不要拼接 SQL │
│ ✅ XSS:转义用户输入 │
│ ✅ CSRF:使用 CSRF Token 或 SameSite Cookie │
│ │
│ 2. 认证鉴权 │
│ ✅ JWT 验证:检查签名、过期时间、版本号 │
│ ✅ 权限检查:Admin 接口只能 Admin 用户访问 │
│ ✅ 频率限制:防止暴力破解(配合 [[05-middleware]]) │
│ │
│ 3. 数据保护 │
│ ✅ 敏感数据加密存储(密码加盐 hash) │
│ ✅ 日志脱敏(手机号 138****5678) │
│ ✅ 响应过滤(不要返回不该返回的字段) │
│ │
│ 4. HTTPS │
│ ✅ 强制 HTTPS(HSTS) │
│ ✅ TLS 1.3(比 1.2 快 30%) │
│ │
└─────────────────────────────────────────────────────────────┘升华:JWT vs Session 的选择
┌─────────────────────────────────────────────────────────────┐
│ JWT vs Session 选择指南 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 选 JWT: │
│ ✅ 纯 API 服务,无状态 │
│ ✅ 跨域认证(单点登录) │
│ ✅ 移动端 / SPA / 微前端 │
│ ✅ 不想依赖 Redis │
│ │
│ 选 Session: │
│ ✅ 需要快速撤回权限(封禁用户) │
│ ✅ 需要存大量用户数据 │
│ ✅ 团队对安全要求极高(Token 泄露风险) │
│ │
│ 实际中: │
│ 很多公司用 JWT Access Token + Redis 存储 Refresh Token │
│ → 兼顾无状态(快速验证)+ 可撤回(Refresh Token 管理) │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ JWT 库的具体 API(PyJWT / node-jsonwebtoken)
✅ OAuth2 各授权模式的参数配置
✅ CSRF Token 的具体实现代码
必须理解:
🔴 JWT 的三个组成部分(Header/Payload/Signature)
🔴 HS256 vs RS256 的区别(对称 vs 非对称)
🔴 JWT 无法主动撤回的问题及解决方案
🔴 Refresh Token 轮换的原理
🔴 接口幂等性的三种实现方案学习状态:🟡 开始学习