This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/DEV/JWT.md
T

356 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [JWT, token, authentication, security, backend]
create time: 2026-05-02 16:30
---
# JWT — JSON Web Token
## 概述
JWT(JSON Web Token, RFC 7519)是一种**开放标准**,用于在各方之间安全地传输声明(claims)。在 Web 开发中,它最常被用作无状态身份认证的载体——服务器签发一个签名的令牌,客户端在后续请求中携带该令牌来证明自己的身份。
> [!tip] 为什么选 JWT 而不是 Session?
> Session 需要服务端存储用户状态,在分布式或微服务架构中必须借助 Redis 等共享存储;JWT 将所有信息打包在令牌本身,服务端只需验签即可,天然适合横向扩展的无状态场景。
> [!question] 思考
> 如果 JWT 把所有信息(包括敏感数据)都打包在令牌里,那和明文 Cookie 有什么区别?
> —— 关键差异在于 **签名机制**。JWT 附带数字签名,服务端可以验证令牌是否被篡改。但注意:默认情况下 Payload 只是 Base64 编码,并非加密!
## 核心结构
JWT 由三段 Base64Url 编码的字符串用 `.` 连接而成:
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. header (头部)
eyJpc3MiOiJleGFtcGxlLmNvbSIsInVzZXJJZCI6NDJ9. payload (载荷)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c signature (签名)
```
### Header — 元信息
声明签名算法和令牌类型:
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
常用算法对比:
| 算法 | 底层机制 | 密钥类型 | 典型场景 |
| ----------------- | ------------------ | -------------- | -------------- |
| `HS256` / `HS512` | HMAC + SHA-256/512 | 对称(同一密钥签名+验签) | 小型项目、快速原型 |
| `RS256` | RSA + SHA-256 | 非对称(私钥签名,公钥验签) | 生产环境、多服务验签 |
| `ES256` | ECDSA + P-256 | 非对称(密钥更短,性能更好) | 移动端、IoT 资源受限场景 |
### Payload — 声明数据
承载要传递的信息,分为三类标准字段和自定义字段:
```json
{
"iss": "auth.example.com", // Issuer — 签发者
"iat": 1714627200, // Issued At — 签发时间(Unix 时间戳)
"exp": 1714713600, // Expiration — 过期时间
"sub": "user_42", // Subject — 主题(通常是用户 ID)
"role": "admin", // 自定义字段
"permissions": ["read", "write"] // 自定义字段
}
```
> [!warning] 安全红线
> **Payload 绝对不要存放密码、身份证号等敏感信息!** Base64 编码不等于加密,任何人拿到令牌都可以解码看到内容。如需保密,应使用 JWE(JWT 加密格式)或在服务端查库获取。
### Signature — 签名验证
签名确保令牌在传输过程中未被篡改:
```
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
```
对于非对称算法(RS256),则使用私钥签名、公钥验签。这也是微服务架构中的推荐方案——只有鉴权服务持有私钥,其他服务用公钥验签即可。
```mermaid
flowchart LR
subgraph Sign["签名阶段"]
A[Header + Payload] --> B["HMAC(Raw string, Secret)"]
B --> C[Signature]
A -.-> D["最终令牌 = Header.Payload.Signature"]
end
subgraph Verify["验签阶段"]
E["收到的令牌"] --> F["提取前两段拼接"]
F --> G["HMAC(Raw string, Secret)"]
G --> H["本地签名"]
I["原始签名段"] --> J{"一致?"}
H --> J
J -->|"是"| K["令牌合法"]
J -->|"否"| L["被篡改"]
end
Sign --> |"下发完整令牌"| Client["Client"]
Client --> |"每次请求携带"| Verify
```
## 认证流程
典型的 JWT 认证过程涉及四个角色间的交互:
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
participant DB as Database
C->>S: 1. POST /login (username + password)
S->>DB: 2. 校验凭据
DB-->>S: 3. 用户信息
alt 凭据正确
S->>S: 4. 生成 JWT (带 exp, iss, sub)
S-->>C: 5. 返回 { accessToken, expiresIn }
C->>S: 6. 后续请求带 Authorization: Bearer <token>
S->>S: 7. 验签 + 检查 exp
alt 令牌有效
S-->>C: 8. 业务数据
else 令牌无效
S-->>C: 9. 401 Unauthorized
end
else 凭据错误
S-->>C: 10. 401 Unauthorized
end
```
> [!note] Access Token vs Refresh Token
> 实践中通常采用双令牌策略:
> - **Access Token**:生命周期短(如 15 分钟),放在请求头中,用于访问 API
> - **Refresh Token**:生命周期长(如 7 天),存储在 HTTP-only Cookie 中,用于换取新的 Access Token
>
> 这样即使 Access Token 泄露,影响范围也有限;而 Refresh Token 存放在 Cookie 中不受 XSS 直接窃取。
## 代码示例
### Go:签发与验证 JWT
使用 `golang-jwt/jwt/v5`:
```go
import "github.com/golang-jwt/jwt/v5"
// 签发令牌
func GenerateToken(userID uint, role string) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"role": role,
"iat": time.Now().Unix(),
"exp": time.Now().Add(15 * time.Minute).Unix(), // 15 分钟过期
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("your-secret-key"))
}
// 验证令牌
func ValidateToken(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 确保算法符合预期,防止算法替换攻击
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte("your-secret-key"), nil
})
}
```
### Gin 中间件:JWT 认证
结合之前 `[[hzh/GIN/3-middleware/jwt-auth-qa]]` 讨论的模式:
```go
func JWTAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
return
}
// 提取 Bearer prefix
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid format"})
return
}
token, err := ValidateToken(parts[1])
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
// 存入 Context,供下游 handler 使用
c.Set("userID", token.Claims.(jwt.MapClaims)["sub"])
c.Next()
}
}
```
### React:前端使用 JWT
```tsx
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
});
// 自动在请求头注入 Token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 统一处理 401,触发刷新或登出
api.interceptors.response.use(
(res) => res,
async (err) => {
if (err.response?.status === 401) {
// 尝试刷新 Token
const newToken = await refreshAccessToken();
if (newToken) {
localStorage.setItem('accessToken', newToken);
err.config.headers.Authorization = `Bearer ${newToken}`;
return api.request(err.config);
}
window.location.href = '/login'; // 重定向到登录页
}
return Promise.reject(err);
},
);
```
> [!example] Token 存储方式对比
>
> | 存储位置 | XSS 防护 | CSRF 防护 | 后端可读 | 适用场景 |
> |----------|----------|-----------|----------|----------|
> | **localStorage** | ❌ 易被 XSS 读取 | ✅ 不自动发送 | ❌ 需手动设置 | SPA + 有 XSRF 防护 |
> | **Memory** | ✅ 不被 XSS 直接读 | ❌ — | ❌ 需手动设置 | 高安全要求(刷新即丢失) |
> | **HTTP-only Cookie** | ✅ 不被 JS 访问 | ⚠️ 需额外防护 | ✅ 自动发送 | 服务端渲染 / 传统 Web |
## 常见安全问题与防御
### 1. 算法降级攻击(Alg: None)
攻击者将 `alg` 改为 `none`,试图绕过验签:
```json
{"alg": "none", "typ": "JWT"}
```
**防御**:始终在白名单中指定允许的算法:
```go
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
```
### 2. 密钥泄露与弱密钥
使用硬编码或弱密钥会让攻击者轻易伪造令牌。
**防御**:
- 密钥长度 ≥ 256 bit(HS256 要求至少 32 字节)
- 通过环境变量或密钥管理服务(Vault、AWS KMS)注入
- 定期轮换密钥
### 3. 过期时间设计不合理
过长的 `exp` 增加令牌泄露后的风险窗口;过短的 `exp` 又导致频繁刷新。
**建议**:
- Access Token:5 ~ 30 分钟
- Refresh Token:7 ~ 30 天
- 配合黑名单机制实现主动注销(见下文)
### 4. 重放攻击
攻击者截取合法的 JWT 令牌后反复使用。
**防御**:
- 短生命周期 Access Token
- 添加 `jti`(JWT ID)唯一标识,配合黑名单做一次性校验
- 在高价值操作中引入二次确认(如修改密码、支付)
## 进阶:Token 黑名单与撤销
JWT 的一个固有缺陷:**一旦签发,无法主动作废**(除非查询数据库)。在生产环境中,这意味著用户登出或密码修改后,已泄露的令牌仍可在过期前使用。
```mermaid
flowchart TD
subgraph Issue["签发"]
A["登录成功"] --> B["生成 JWT jti + exp + signed"]
B --> C["返回给客户端"]
end
subgraph Use["正常使用"]
D["请求携带 JWT"] --> E["验签 + 查黑名单"]
E -->|"未在册"| F["放行"]
E -->|"已在册"| G["拒绝"]
end
subgraph Revoke["撤销操作"]
H["用户登出或改密"] --> I["将jti写入黑名单Redis"]
I --> J["Set TTL = exp - iat"]
end
C --> D
H --> I
```
Go 实现黑名单校验的关键逻辑:
```go
func Blacklisted(ctx context.Context, jti string) bool {
key := "jwt:blacklist:" + jti
exists, _ := redis.Get(ctx, key).Result()
return exists == "true"
}
func InvalidateToken(ctx context.Context, jti string, ttl time.Duration) {
redis.Set(ctx, "jwt:blacklist:"+jti, "true", ttl)
}
```
> [!tip] 替代方案:短期 Token + 版本控制
> 为简化实现,可以给每个用户分配一个 `tokenVersion`(存在数据库中)。签发令牌时将版本号写入 `sub` 或自定义字段。用户登出时只递增版本号,所有旧版令牌在验签通过后对比数据库版本号即可识别为失效。无需维护黑名单集合。
## Session vs JWT:选型决策
| 维度 | Session | JWT |
|------|---------|-----|
| 服务端存储 | 需要(内存/Redis) | 不需要(仅验签) |
| 实时注销 | ✅ 立即可效 | ❌ 需黑名单或短轮询 |
| 跨域支持 | 依赖 Cookie,需处理 CORS | 任意头,天然跨域 |
| 可扩展性 | 受限于共享存储 | 天然无状态,易于水平扩展 |
| 令牌体积 | 极小(仅 sessionId) | 较大(含 payload) |
| 客户端操控 | ❌ 不可见 | ✅ 可解码(但不可篡改) |
> [!info] 如何选型?
>
> - **单体应用 / 传统 MVC** → Session(简单可靠,原生支持好)
> - **微服务 / 前后端分离 / 移动端** → JWT(无状态,多服务共享密钥即可)
> - **混合模式** → JWT 作为内部服务间通信凭证,外部入口仍可搭配 Session
## 关联笔记
- [[hzh/GIN/3-middleware/jwt-auth-qa]] — JWT 中间件中 Context 数据安全分析
- [[hzh/GIN/3-middleware]] — Gin 中间件完整机制
- [[hzh/DEV/跨域问题调试]] — CORS 配置与跨域场景