14 KiB
14 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-04-17 |
Web Application Authorization Flow
概述
Authorization(授权) 在 Web 应用中是 HTTP 层面的认证令牌验证机制。本文聚焦浏览器、服务器、Token 的交互流程和加密验证过程。
HTTP Authorization 流程
完整授权流程
sequenceDiagram
participant B as Browser
participant S as Server
participant DB as Database
Note over B,S: 1. 登录获取 Token
B->>S: POST /login (username, password)
S->>DB: 验证凭证
DB-->>S: 用户数据
S->>S: HMAC-SHA256(payload, secret)
S-->>B: {access_token: "eyJhbGc...", expires_in: 3600}
Note over B,S: 2. 带授权头访问资源
B->>B: 存储 Token (LocalStorage/Memory)
B->>S: GET /api/resource
Note right of B: Authorization: Bearer eyJhbGc...
Note over S: 3. 验证 Token
S->>S: 提取 Token
S->>S: 验证签名 (HMAC)
S->>S: 检查过期时间
S->>S: 验证 Issuer/Audience
S->>DB: 根据 user_id 查询权限
Note over S: 4. 授权决策
alt Token 有效
S-->>B: 200 OK + 数据
else Token 无效
S-->>B: 401 Unauthorized
else 权限不足
S-->>B: 403 Forbidden
end
JWT Token 验证过程
graph TD
A[接收 Authorization: Bearer <token>] --> B[解析三部分]
B --> C[Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9]
B --> D[Payload: eyJ1c2VyX2lkIjoiMTIzIiw...]
B --> E[Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c]
C --> F[获取签名算法 HS256]
D --> G[获取用户数据]
E --> H[签名部分]
F --> I[重新计算签名]
G --> I
H --> I
I --> J{签名匹配?}
J -->|是| K[检查过期时间 exp]
J -->|否| L[拒绝: 401 Unauthorized]
K -->|未过期| M[检查 nbf 时间]
K -->|已过期| L
M -->|有效| N[检查 iss 签发者]
M -->|无效| L
N -->|匹配| O[验证通过]
N -->|不匹配| L
O --> P[从 payload 提取 user_id]
P --> Q[查询用户权限]
Q --> R[返回受保护资源]
浏览器行为详解
浏览器如何处理 Authorization
graph LR
A[用户请求资源] --> B[检查 Token]
B --> C{Token 存在于?}
C -->|LocalStorage| D[从 LS 读取]
C -->|SessionStorage| E[从 SS 读取]
C -->|Memory| F[从变量读取]
C -->|Cookie| G[浏览器自动发送]
D --> H[构造 Authorization Header]
E --> H
F --> H
G --> I[无需手动添加<sup>*</sup>]
H --> J[发起 XHR/Fetch 请求]
I --> J
J --> K[设置 headers]
K --> L[发送到服务器]
style G fill:#ff9999
style H fill:#90EE90
style I fill:#90EE90
subgraph 注
G1[Cookie 方式需要 SameSite 属性]
G2[防止 CSRF 攻击]
end
G1 -.-> G
G2 -.-> G
浏览器自动发送 Cookie,但需要手动添加 Bearer Token
浏览器存储 Token 的方式
| 存储方式 | 特点 | 安全性 | 跨域访问 |
|---|---|---|---|
| LocalStorage | 持久化,手动管理 | ❌ XSS 风险 | ✅ 支持 |
| SessionStorage | 会话结束清除 | ❌ XSS 风险 | ❌ 仅同源 |
| Memory | 页面刷新丢失 | ✅ 最安全 | ❌ 仅当前页面 |
| Cookie | 可设置 HttpOnly | ✅ 防 XSS | ⚠️ 需 SameSite |
前端实现 (JavaScript)
// 1. 登录后存储 Token
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const { access_token } = await response.json();
localStorage.setItem('token', access_token);
}
// 2. 带认证头访问 API
async function fetchProtectedResource() {
const token = localStorage.getItem('token');
const response = await fetch('/api/resource', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Token 过期,需要刷新或重新登录
handleTokenExpired();
}
return response.json();
}
// 3. Axios 拦截器自动添加 Token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
加密与签名机制
JWT 签名算法对比
| 算法类型 | 算法 | 密钥类型 | 特点 |
|---|---|---|---|
| HMAC | HS256 | 对称密钥 | 服务器签发和验证都 uses 同一密钥 |
| HMAC | HS512 | 对称密钥 | 更强的哈希,性能稍低 |
| RSA | RS256 | 非对称密钥 | 私钥签名,公钥验证 |
| RSA | RS512 | 非对称密钥 | 更强的签名算法 |
| ECDSA | ES256 | 椭圆曲线 | 比更短但同样安全 |
HMAC-SHA256 签名流程
graph TD
A[原始数据] --> B[Base64URL 编码 Header]
A --> C[Base64URL 编码 Payload]
B --> D[拼接 Header.Payload]
C --> D
D --> E[HMAC-SHA256 拼接结果]
E --> F[使用 Secret Key]
F --> G[生成 256-bit 签名]
G --> H[Base64URL 编码签名]
H --> I[拼接 final JWT]
I --> J[Header.Payload.Signature]
style E fill:#ff9999
style F fill:#ff9999
style G fill:#lightblue
签名 = HMAC-SHA256(Base64URL(Header) + "." + Base64URL(Payload), Secret)
Go 实现签名与验证
package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"strings"
)
type JWTBuilder struct {
secretKey []byte
}
// 生成签名
func (j *JWTBuilder) sign(header, payload string) string {
data := strings.Join([]string{header, payload}, ".")
h := hmac.New(sha256.New, j.secretKey)
h.Write([]byte(data))
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
return signature
}
// 验证签名
func (j *JWTBuilder) verify(token string) bool {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return false
}
header, payload, signature := parts[0], parts[1], parts[2]
// 重新计算签名
expectedSignature := j.sign(header, payload)
// 比较签名 (恒定时间比较,防止计时攻击)
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
服务器验证流程
详细的验证步骤
package middleware
import (
"net/http"
"encoding/base64"
"encoding/json"
"time"
)
type Claims struct {
UserID string `json:"user_id"`
exp int64 `json:"exp"`
iat int64 `json:"iat"`
iss string `json:"iss"`
aud string `json:"aud"`
}
func (m *AuthMiddleware) validateToken(token string) (*Claims, error) {
// 步骤 1: 分割三部分
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// 步骤 2: 验证签名
if !m.verifySignature(token) {
return nil, fmt.Errorf("invalid signature")
}
// 步骤 3: 解码 Payload
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid payload encoding")
}
// 步骤 4: 解析 Claims
var claims Claims
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, fmt.Errorf("invalid payload json")
}
// 步骤 5: 验证过期时间
if time.Now().Unix() > claims.exp {
return nil, fmt.Errorf("token expired")
}
// 步骤 6: 验证签发者
if claims.iss != "my-app" {
return nil, fmt.Errorf("invalid issuer")
}
// 步骤 7: 验证受众
if claims.aud != "api-users" {
return nil, fmt.Errorf("invalid audience")
}
return &claims, nil
}
// 验证签名
func (m *AuthMiddleware) verifySignature(token string) bool {
parts := strings.Split(token, ".")
signatureData := parts[0] + "." + parts[1]
providedSignature := parts[2]
// 计算期望的签名
expectedSignature := m.sign(signatureData)
// 恒定时间比较
return hmac.Equal(
[]byte(providedSignature),
[]byte(expectedSignature),
)
}
HTTP 中间件
// AuthMiddleware 验证每个请求
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. 提取 Authorization 头
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
return
}
// 2. 解析 Bearer Token
if !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "Invalid authorization scheme", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
// 3. 验证 Token
claims, err := m.validateToken(token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// 4. 将用户信息存入 Context
ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
ctx = context.WithValue(ctx, "claims", claims)
// 5. 继续处理请求
next.ServeHTTP(w, r.WithContext(ctx))
})
}
常见 Authorization Scheme
Basic Auth(基础认证)
sequenceDiagram
participant B as Browser
participant S as Server
participant DB as Database
B->>S: GET /resource
S-->>B: 401 Unauthorized
Note right of B: WWW-Authenticate: Basic realm="Secure Area"
B->>B: 用户输入用户名密码
B->>B: Base64("username:password")
B->>S: GET /resource
Note right of B: Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
S->>B: 解码 Base64
S->>DB: 验证用户名密码
DB-->>S: 验证结果
S-->>B: 200 OK + 资源
问题:每次请求都需要用户名密码,不安全,不推荐
Digest Auth(摘要认证)
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /resource
S-->>C: 401 Unauthorized
Note right of C: WWW-Authenticate: Digest<br/>realm="Protected",<br/>nonce="xyz123",<br/>qop="auth"
C->>C: 计算 response<br/>MD5(username:realm:password)<br/>MD5(method:uri)<br/>HA1 = MD5(username:realm:password)<br/>HA2 = MD5(method:uri)<br/>response = MD5(HA1:nonce:HA2)
C->>S: GET /resource
Note right of C: Authorization: Digest<br/>username="user",<br/>realm="Protected",<br/>nonce="xyz123",<br/>uri="/resource",<br/>response="abc123"
S->>S: 重新计算 response
S-->>C: 200 OK
优点:密码不直接传输,更安全 缺点:实现复杂,需要多次往返
Bearer Token(持有者令牌)
✅ 现代 Web 应用的标准选择
# 请求示例
GET /api/user/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcm5hbWUiOiJqb2huIiwiZXhwIjoxNzI4MjQ4MDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# 响应示例
HTTP/1.1 200 OK
Content-Type: application/json
{
"user_id": "123",
"username": "john",
"email": "john@example.com"
}
Refresh Token 机制
Access Token vs Refresh Token
graph LR
A[Access Token] --> B[有效期短<br/>15-30分钟]
A --> C[用于 API 访问]
A --> D[存储在浏览器]
E[Refresh Token] --> F[有效期长<br/>数天至数周]
E --> G[用于获取新 Access Token]
E --> H[存储在 HttpOnly Cookie]
style A fill:#90EE90
style E fill:#ffcc00
Token 刷新流程
sequenceDiagram
participant B as Browser
participant S as Server
Note over B,S: Access Token 过期
B->>S: POST /api/refresh
Note right of B: Body: {refresh_token: "..."}
S->>S: 验证 Refresh Token
S->>S: 生成新的 Access Token
S-->>B: {access_token: "new_jwt..."}
Note over B,S: 使用新 Token 继续请求
B->>S: GET /api/resource
Note right of B: Authorization: Bearer new_jwt...
S-->>B: 200 OK
Refresh Token 优势:
- ✅ Access Token 短期有效,减少泄露风险
- ✅ Refresh Token 可随时撤销
- ✅ 用户无感知自动续期
安全最佳实践
Token 安全传输
| 措施 | 说明 | Go 实现 |
|---|---|---|
| HTTPS 强制 | 防止 Token 被窃取 | RedirectHTTPS 中间件 |
| 短期有效期 | 降低被滥用风险 | Token 15-30 分钟 |
| 签名验证 | 防止 Token 被篡改 | HMAC/RSA 签名 |
| 黑名单机制 | 主动撤销 Token | Redis 存储 revoked_tokens |
浏览器安全设置
// ✅ 推荐:HttpOnly Cookie 存储 Refresh Token
document.cookie = `refresh_token=${refreshToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800`;
// ⚠️ 谨慎:LocalStorage 存储 Access Token
localStorage.setItem('access_token', accessToken);
// 需要 XSS 防护
// ❌ 避免:明文传输 Token
// 必须使用 HTTPS
CORS 和 SameSite
graph TD
A[前端应用<br/>example.com] --> B[后端 API<br/>api.example.com]
B --> C[CORS 策略]
C --> D[Access-Control-Allow-Origin: https://example.com]
C --> E[Access-Control-Allow-Credentials: true]
B --> F[SameSite Cookie]
F --> G[Strict: 严格模式]
F --> H[Lax: 放宽模式]
style D fill:#90EE90
style G fill:#90EE90
Go CORS 配置:
func setupCORS() *cors.Cors {
return cors.New(cors.Options{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 3600,
})
}
相关笔记
- 金山办公作业/Week05/用户认证 - 认证的实现方式
- CS/NET/HTTPS - SSL/TLS 加密传输
- CS/DB/访问控制 - 数据库层面权限