This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/CS/NET/Authorization.md
2026-04-20 22:47:51 +08:00

554 lines
14 KiB
Markdown
Raw Permalink 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: [CS, NET, security, authorization, web, authentication-flow]
create time: 2026-04-17
---
# Web Application Authorization Flow
## 概述
**Authorization(授权)** 在 Web 应用中是 HTTP 层面的认证令牌验证机制。本文聚焦浏览器、服务器、Token 的交互流程和加密验证过程。
## HTTP Authorization 流程
### 完整授权流程
```mermaid
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 验证过程
```mermaid
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
```mermaid
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)
```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 签名流程
```mermaid
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 实现签名与验证
```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))
}
```
## 服务器验证流程
### 详细的验证步骤
```go
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 中间件
```go
// 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(基础认证)
```mermaid
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(摘要认证)
```mermaid
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 应用的标准选择**
```http
# 请求示例
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
```mermaid
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 刷新流程
```mermaid
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 |
### 浏览器安全设置
```javascript
// ✅ 推荐: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
```mermaid
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 配置**:
```go
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/访问控制]] - 数据库层面权限