554 lines
14 KiB
Markdown
554 lines
14 KiB
Markdown
---
|
||
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/访问控制]] - 数据库层面权限
|