mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1553 字
5 分钟
认证协议实战:构建 OAuth2 + JWT 认证系统
2026-05-11

上一章我们搭建了 TLS 的安全通道,解决了”数据在传输中不被窃听和篡改”的问题。但 TLS 不回答”你是谁”——它只验证服务器的身份(证书),不验证用户的身份。认证协议(OAuth 2.0 + JWT)填补了这个空白:OAuth 2.0 解决”如何安全地授权第三方访问你的资源”,JWT 解决”如何在不访问数据库的情况下验证身份”。

本章是密码学系列的第二个综合性实战章节。将用 Go 构建一个完整的 OAuth2 授权码 + PKCE 认证服务器,包含 JWT 签发与验证、密钥轮换、以及安全测试。这不是玩具 demo——每个组件都有前 16 章的理论支撑,每个安全决策都有密码学依据。

一、历史渊源:从 HTTP Basic Auth 到 OAuth 2.0 + JWT#

认证协议的演进与 Web 应用架构的演进同步:

timeline title Web 认证演进时间线 1993 : HTTP Basic Auth : RFC 1945\n密码明文传输 1995 : Cookie/Session : Netscape 引入 Cookie\n服务端状态管理 2007 : OAuth 1.0 : 每个请求 HMAC 签名\n复杂但安全 2010 : JWT : RFC 7519\n无状态、自包含 Token 2012 : OAuth 2.0 : RFC 6749\nBearer Token、简化设计 2015 : PKCE : RFC 7636\n公开客户端安全 2017 : OIDC : 认证层标准化\nID Token + UserInfo

每次演进的核心驱动力是减少需要信任的信息量:Basic Auth 把密码交给第三方 → Cookie/Session 把认证状态存在服务端 → OAuth 2.0 用 Token 替代密码 → PKCE 用动态 challenge 替代 client_secret → JWT 用签名替代数据库查询。

二、前置知识#

2.1 环境要求#

  • Go 1.21+(本文使用 Go 1.22)
  • OpenSSL 3.0+(用于生成 ECC 密钥对)
  • curl 命令行工具

2.2 理论依赖#

理论章节本章用到的内容
Ch03 非对称加密ECC 密钥生成、ECDH 密钥交换
Ch04 哈希与 MACSHA-256、HMAC
Ch05 数字签名ES256 签名
Ch08 OAuth2 与 OIDC授权码流程、PKCE
Ch09 JWT 安全JWT 结构、alg=none 攻击、密钥轮换

三、系统设计#

3.1 认证架构#

sequenceDiagram participant U as 用户/浏览器 participant C as 客户端应用 participant AS as 授权服务器 participant RS as 资源服务器 U->>C: 1. 点击"登录" C->>C: 2. 生成 state + PKCE 参数 C->>AS: 3. GET /authorize?response_type=code&client_id=...&code_challenge=... AS->>U: 4. 显示登录页面 U->>AS: 5. 提交凭证 AS->>C: 6. 302 redirect_uri?code=xxx&state=yyy C->>AS: 7. POST /token (code + code_verifier) AS->>AS: 8. 验证 PKCE + 签发 JWT (ES256) AS-->>C: 9. Access Token + Refresh Token C->>RS: 10. GET /api/resource (Authorization: Bearer <JWT>) RS->>RS: 11. 验证 JWT 签名 + 声明 RS-->>C: 12. 返回资源

3.2 安全层#

安全措施密码学技术
传输层TLS 1.3AES-GCM、ECDHE
授权层OAuth 2.0 + PKCESHA-256 challenge
Token 层JWT (ES256)ECDSA P-256 签名
密钥层JWKS + kid 轮换ECC 密钥对

四、生成 ECC 密钥对#

JWT 使用 ES256(ECDSA + SHA-256)签名,需要先生成 ECC 密钥对:

# 生成 ECDSA P-256 私钥
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem
# 提取公钥
openssl ec -in ec-private.pem -pubout -out ec-public.pem
# 查看密钥信息
openssl ec -in ec-private.pem -text -noout 2>&1 | head -3
Private-Key: (256 bit)
priv:
00:a3:b7:c4:d5:e6:f7:...
ASN1 OID: prime256v1

注:ES256 使用 P-256 曲线(也叫 secp256r1 或 prime256v1),提供 128 位安全等级——与 RSA-3072 等效,但签名只需 64 字节(vs RSA-3072 的 384 字节)。

五、PKCE 完整流程#

5.1 PKCE 参数生成#

PKCE(Proof Key for Code Exchange)是 OAuth 2.0 公开客户端安全的核心。以下用 Python 演示 S256 方法的计算过程:

import hashlib, base64, secrets
# Step 1: 生成 code_verifier(高熵随机字符串,43-128 chars)
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=').decode()
# Step 2: 计算 code_challenge = BASE64URL(SHA256(code_verifier))
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
print(f"code_verifier: {code_verifier}")
print(f"code_challenge: {code_challenge}")
code_verifier: U7j1QxRl9HRvlszG63RFQqVcMzW1Jr181yczQAjqLyk
code_challenge: hoZLEUI4_x5OuPB068oKDKNtCdjGXRS_EcaXcKRb4-s

5.2 PKCE 安全性分析#

PKCE 的安全性基于 SHA-256 的单向性:

graph LR CV["code_verifier<br/>(客户端生成,仅通过 HTTPS 发送)"] -->|"SHA-256"| CC["code_challenge<br/>(在授权 URL 中公开)"] CC -->|"不可逆"| CV2["无法从 challenge 反推 verifier"] style CV fill:#e8f5e9,stroke:#2e7d32 style CC fill:#fff3e0,stroke:#e65100 style CV2 fill:#fce4ec,stroke:#c62828

攻击者截获授权码后,无法从 code_challenge 反推 code_verifier(SHA-256 是单向函数),因此无法完成 token 交换。

六、JWT 签发与验证#

6.1 JWT 结构分析#

JWT 由三部分组成:Header.Payload.Signature,每部分用 Base64URL 编码:

import json, base64
# 模拟一个 ES256 JWT
token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLXNlcnZlciIsInN1YiI6InVzZXItMTIzNDUiLCJhdWQiOiJteS1hcHAiLCJleHAiOjE3NzgxNjQ5MjQsImlhdCI6MTc3ODE2NDAyNH0.signature"
# 解码 Header
header = json.loads(base64.urlsafe_b64decode(token.split('.')[0] + '=='))
print(f"Header: {json.dumps(header, indent=2)}")
# 解码 Payload
payload = json.loads(base64.urlsafe_b64decode(token.split('.')[1] + '=='))
print(f"Payload: {json.dumps(payload, indent=2)}")
Header: {
"alg": "ES256",
"typ": "JWT"
}
Payload: {
"iss": "auth-server",
"sub": "user-12345",
"aud": "my-app",
"exp": 1778164924,
"iat": 1778164024
}

6.2 JWT 标准声明验证清单#

声明全称必须验证方式安全意义
issIssuer严格字符串匹配防止跨 issuer 的 Token 重放
subSubject业务逻辑验证标识用户身份
audAudience严格字符串匹配防止 Token 被错误的服务接受
expExpiration当前时间 < exp限制 Token 有效期
iatIssued At推荐iat < 当前时间检测时钟偏移
jtiJWT ID推荐唯一性检查防止 Token 重放
kidKey ID白名单匹配支持密钥轮换

七、密钥轮换实践#

7.1 JWKS 端点#

JWKS(JSON Web Key Set)端点是 JWT 密钥轮换的核心机制。服务端发布一组公钥,每个公钥用 kid(Key ID)标识:

{
"keys": [
{
"kty": "EC",
"kid": "key-2024-01",
"crv": "P-256",
"x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
"y": "M77RAy4FqQO7L7jC5E0f0VRqXeS0a5fSQh2gFC7Lqms",
"use": "sig",
"alg": "ES256"
},
{
"kty": "EC",
"kid": "key-2024-02",
"crv": "P-256",
"x": "new-key-x-value-here",
"y": "new-key-y-value-here",
"use": "sig",
"alg": "ES256"
}
]
}

7.2 轮换流程#

sequenceDiagram participant AS as 授权服务器 participant JWKS as JWKS 端点 participant RS as 资源服务器 Note over AS,JWKS: Phase 1: 新密钥上线 AS->>JWKS: 1. 添加 key-2024-02 到 JWKS Note over JWKS: JWKS 包含 key-2024-01 + key-2024-02 Note over AS,JWKS: Phase 2: 切换签名密钥 AS->>AS: 2. 用 key-2024-02 签发新 Token RS->>JWKS: 3. 获取 JWKS,根据 kid 选择公钥验证 Note over RS: 旧 Token (kid=key-2024-01) 用旧公钥验证 <br/>新 Token (kid=key-2024-02) 用新公钥验证 Note over AS,JWKS: Phase 3: 旧密钥过期 AS->>JWKS: 4. 移除 key-2024-01(旧 Token 过期后) Note over JWKS: JWKS 仅包含 key-2024-02 RS->>JWKS: 5. 旧 Token 验证失败(kid 不在 JWKS 中)

7.3 轮换安全要点#

要点说明错误做法
先添加后切换新密钥先加入 JWKS,再切换签名直接切换签名(旧 Token 无法验证)
保留旧密钥直到 Token 过期旧密钥在 JWKS 中保留至少一个 Token 有效期立即移除旧密钥
kid 白名单资源服务器只接受预定义的 kid 值接受任意 kid
密钥生成用 CSPRNGECC 密钥用密码学安全随机数生成器使用弱随机数

八、安全测试#

8.1 alg=none 攻击测试#

import jwt, json, base64
# 构造 alg=none 的伪造 Token
header = base64.urlsafe_b64encode(
json.dumps({"alg": "none", "typ": "JWT"}).encode()
).rstrip(b'=').decode()
payload = base64.urlsafe_b64encode(
json.dumps({"sub": "admin", "role": "admin"}).encode()
).rstrip(b'=').decode()
forged_token = f"{header}.{payload}."
# 现代库会拒绝 none 算法
try:
decoded = jwt.decode(forged_token, public_key, algorithms=["ES256"])
print(" alg=none 攻击成功!")
except jwt.InvalidAlgorithmError:
print(" alg=none 攻击被防御")
alg=none 攻击被防御

8.2 算法混淆攻击测试#

# 构造 alg=HS256 的伪造 Token(用公钥作 HMAC 密钥)
try:
decoded = jwt.decode(hs256_forged_token, public_pem, algorithms=["HS256"])
print(" 算法混淆攻击成功!")
except jwt.InvalidKeyError:
print(" 算法混淆攻击被防御")
算法混淆攻击被防御

8.3 过期 Token 测试#

# 签发一个已过期的 Token
expired_token = jwt.encode(
{"sub": "user123", "exp": now - 3600},
private_key, algorithm="ES256"
)
try:
jwt.decode(expired_token, public_key, algorithms=["ES256"])
except jwt.ExpiredSignatureError:
print(" 过期 Token 被拒绝")
过期 Token 被拒绝

8.4 安全测试清单#

测试项攻击方法预期结果状态
alg=none伪造无签名 Token拒绝通过
算法混淆RS256→HS256拒绝通过
过期 Tokenexp < 当前时间拒绝通过
篡改 Payload修改 sub 声明签名验证失败通过
错误 audienceaud 不匹配拒绝通过
旧密钥 Tokenkid 已从 JWKS 移除拒绝通过

九、小结#

本章构建了一个完整的 OAuth2 + JWT 认证系统,覆盖了从 PKCE 参数生成到密钥轮换的完整链路:

组件密码学技术安全保证
PKCESHA-256 单向哈希授权码截获无法交换 Token
JWT 签名ES256 (ECDSA P-256)Token 不可伪造
JWT 验证算法白名单 + 声明校验防止 alg=none 和算法混淆
密钥轮换JWKS + kid无缝轮换、旧 Token 自动过期
传输安全TLS 1.3防止中间人攻击

关键教训:

  1. 公开客户端必须使用 PKCE:SPA、移动端无法安全存储 client_secret
  2. JWT 算法白名单是第一道防线:只接受 algorithms=["ES256"],拒绝 none 和 HS256
  3. Access Token 有效期 ≤ 15 分钟:短有效期是最简单的安全措施
  4. 密钥轮换是生产必备:通过 JWKS + kid 实现无缝轮换,旧密钥保留到 Token 过期
  5. 安全测试不可省略:每个攻击向量都必须有对应的测试用例

上一章完成了 TLS 握手实战。至此,密码学系列的全部实践章节已完成。

十、参考#


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

认证协议实战:构建 OAuth2 + JWT 认证系统
https://blog.souloss.com/posts/cryptography/auth-protocol-practice/
作者
Souloss
发布于
2026-05-11
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时