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
T
2026-04-20 22:47:51 +08:00

14 KiB
Raw Blame History

tags, create time
tags create time
CS
NET
security
authorization
web
authentication-flow
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,
    })
}

相关笔记