正常 JWT 请求流程#
sequenceDiagram
participant User as 用户
participant Frontend as 前端(浏览器/App)
participant Auth as 认证服务器
participant Backend as 后端API服务器
Note over User,Backend: 1. 登录阶段
User->>Frontend: 输入用户名/密码
Frontend->>Auth: POST /login (凭证)
Auth->>Auth: 验证凭证
Auth-->>Frontend: 返回 access_token + refresh_token
Frontend->>Frontend: 存储token(内存/localStorage)
Frontend-->>User: 登录成功
Note over User,Backend: 2. 正常请求阶段
User->>Frontend: 请求受保护资源
Frontend->>Backend: GET /api/resource
Authorization: Bearer access_token
Backend->>Backend: 验证access_token签名和过期时间
Backend-->>Frontend: 返回请求的资源
Frontend-->>User: 展示数据
Note over User,Backend: 3. Access Token过期
User->>Frontend: 继续请求
Frontend->>Backend: GET /api/resource
Authorization: Bearer access_token(已过期)
Backend-->>Frontend: 401 Unauthorized (token过期)
Frontend->>Frontend: 检测到401,触发刷新逻辑
Note over Frontend,Auth: 4. 刷新Token阶段
Frontend->>Auth: POST /refresh
refresh_token
Auth->>Auth: 验证refresh_token
Auth-->>Frontend: 返回新的 access_token(可选的新refresh_token)
Frontend->>Frontend: 更新存储的access_token
Frontend->>Backend: 重试原请求(新access_token)
Backend-->>Frontend: 返回请求的资源
Frontend-->>User: 展示数据
为什么不能只靠JWT#
- refresh token 发出去后后端不保存,会变成 不可控的长期通行证
- 无法主动踢人下线
- 无法注销单点设备
- 无法判断 token 是否被盗用(攻击者拿到了 refresh token 可以一直刷新 access token)
- 无法实现会话管理
使用 refresh tokens 表后的流程图#
sequenceDiagram
participant User as 用户
participant Frontend as 前端
participant Auth as 认证服务器
participant DB as 数据库(refresh_tokens表)
participant Backend as 后端API服务器
Note over User,DB: 1. 登录阶段(签发token + 入库)
User->>Frontend: 输入用户名/密码
Frontend->>Auth: POST /login
Auth->>Auth: 验证凭证
Auth->>DB: 生成唯一refresh_token_id
INSERT INTO refresh_tokens
(user_id, token_hash, expires_at,
device_info, ip_address, revoked)
DB-->>Auth: 插入成功
Auth->>Auth: 生成access_token + refresh_token
(refresh_token包含id引用)
Auth-->>Frontend: 返回access_token + refresh_token
Frontend->>Frontend: 存储token
Frontend-->>User: 登录成功
Note over User,Backend: 2. 正常请求(与之前相同)
User->>Frontend: 请求资源
Frontend->>Backend: GET /api/resource
Authorization: Bearer access_token
Backend->>Backend: 验证access_token
Backend-->>Frontend: 返回资源
Note over Frontend,DB: 3. Access Token过期 → 刷新
Frontend->>Backend: GET /api/resource (access_token过期)
Backend-->>Frontend: 401 Unauthorized
Frontend->>Auth: POST /refresh
refresh_token
Note over Auth,DB: 4. 刷新验证(多步校验)
Auth->>Auth: 解析refresh_token,提取token_id
Auth->>DB: SELECT * FROM refresh_tokens
WHERE id = token_id
DB-->>Auth: 返回记录
Auth->>Auth: 校验清单:
Note over Auth: ✅ token_hash是否匹配
✅ 是否过期 (expires_at > now())
✅ 是否被撤销 (revoked = false)
✅ 用户是否仍有效
✅ 设备信息是否一致(可选)
alt 校验全部通过
Auth->>DB: UPDATE refresh_tokens
SET last_used_at = now(),
last_used_ip = current_ip
WHERE id = token_id
Auth->>Auth: 生成新access_token
(可选:延长refresh_token有效期)
Auth-->>Frontend: 返回新access_token
Frontend->>Frontend: 更新access_token
Frontend->>Backend: 重试原请求(新token)
Backend-->>Frontend: 返回资源
else 校验失败
Auth-->>Frontend: 401/403 (刷新失败)
Frontend->>Frontend: 清除所有token
Frontend->>User: 跳转登录页
end
Note over User,DB: 5. 主动登出
User->>Frontend: 点击登出
Frontend->>Auth: POST /logout
refresh_token
Auth->>DB: UPDATE refresh_tokens
SET revoked = true
WHERE id = token_id
Auth-->>Frontend: 登出成功
Frontend->>Frontend: 清除本地token
Frontend-->>User: 已登出
Note over User,DB: 6. 安全场景:密码修改
User->>Frontend: 修改密码
Frontend->>Auth: POST /change-password
Auth->>DB: UPDATE refresh_tokens
SET revoked = true
WHERE user_id = current_user_id
Note over DB: 撤销该用户所有refresh_token
(强制所有设备重新登录)
Auth-->>Frontend: 密码修改成功
Frontend->>Frontend: 清除本地token
Frontend-->>User: 请重新登录