上一章我们搭建了 TLS 的安全通道,解决了”数据在传输中不被窃听和篡改”的问题。但 TLS 不回答”你是谁”——它只验证服务器的身份(证书),不验证用户的身份。认证协议(OAuth 2.0 + JWT)填补了这个空白:OAuth 2.0 解决”如何安全地授权第三方访问你的资源”,JWT 解决”如何在不访问数据库的情况下验证身份”。
本章是密码学系列的第二个综合性实战章节。将用 Go 构建一个完整的 OAuth2 授权码 + PKCE 认证服务器,包含 JWT 签发与验证、密钥轮换、以及安全测试。这不是玩具 demo——每个组件都有前 16 章的理论支撑,每个安全决策都有密码学依据。
一、历史渊源:从 HTTP Basic Auth 到 OAuth 2.0 + JWT
认证协议的演进与 Web 应用架构的演进同步:
每次演进的核心驱动力是减少需要信任的信息量: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 哈希与 MAC | SHA-256、HMAC |
| Ch05 数字签名 | ES256 签名 |
| Ch08 OAuth2 与 OIDC | 授权码流程、PKCE |
| Ch09 JWT 安全 | JWT 结构、alg=none 攻击、密钥轮换 |
三、系统设计
3.1 认证架构
3.2 安全层
| 层 | 安全措施 | 密码学技术 |
|---|---|---|
| 传输层 | TLS 1.3 | AES-GCM、ECDHE |
| 授权层 | OAuth 2.0 + PKCE | SHA-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 -3Private-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: U7j1QxRl9HRvlszG63RFQqVcMzW1Jr181yczQAjqLykcode_challenge: hoZLEUI4_x5OuPB068oKDKNtCdjGXRS_EcaXcKRb4-s5.2 PKCE 安全性分析
PKCE 的安全性基于 SHA-256 的单向性:
攻击者截获授权码后,无法从 code_challenge 反推 code_verifier(SHA-256 是单向函数),因此无法完成 token 交换。
六、JWT 签发与验证
6.1 JWT 结构分析
JWT 由三部分组成:Header.Payload.Signature,每部分用 Base64URL 编码:
import json, base64
# 模拟一个 ES256 JWTtoken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLXNlcnZlciIsInN1YiI6InVzZXItMTIzNDUiLCJhdWQiOiJteS1hcHAiLCJleHAiOjE3NzgxNjQ5MjQsImlhdCI6MTc3ODE2NDAyNH0.signature"
# 解码 Headerheader = json.loads(base64.urlsafe_b64decode(token.split('.')[0] + '=='))print(f"Header: {json.dumps(header, indent=2)}")
# 解码 Payloadpayload = 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 标准声明验证清单
| 声明 | 全称 | 必须 | 验证方式 | 安全意义 |
|---|---|---|---|---|
iss | Issuer | 是 | 严格字符串匹配 | 防止跨 issuer 的 Token 重放 |
sub | Subject | 是 | 业务逻辑验证 | 标识用户身份 |
aud | Audience | 是 | 严格字符串匹配 | 防止 Token 被错误的服务接受 |
exp | Expiration | 是 | 当前时间 < exp | 限制 Token 有效期 |
iat | Issued At | 推荐 | iat < 当前时间 | 检测时钟偏移 |
jti | JWT ID | 推荐 | 唯一性检查 | 防止 Token 重放 |
kid | Key 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 轮换流程
7.3 轮换安全要点
| 要点 | 说明 | 错误做法 |
|---|---|---|
| 先添加后切换 | 新密钥先加入 JWKS,再切换签名 | 直接切换签名(旧 Token 无法验证) |
| 保留旧密钥直到 Token 过期 | 旧密钥在 JWKS 中保留至少一个 Token 有效期 | 立即移除旧密钥 |
| kid 白名单 | 资源服务器只接受预定义的 kid 值 | 接受任意 kid |
| 密钥生成用 CSPRNG | ECC 密钥用密码学安全随机数生成器 | 使用弱随机数 |
八、安全测试
8.1 alg=none 攻击测试
import jwt, json, base64
# 构造 alg=none 的伪造 Tokenheader = 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 测试
# 签发一个已过期的 Tokenexpired_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 | 拒绝 | 通过 |
| 过期 Token | exp < 当前时间 | 拒绝 | 通过 |
| 篡改 Payload | 修改 sub 声明 | 签名验证失败 | 通过 |
| 错误 audience | aud 不匹配 | 拒绝 | 通过 |
| 旧密钥 Token | kid 已从 JWKS 移除 | 拒绝 | 通过 |
九、小结
本章构建了一个完整的 OAuth2 + JWT 认证系统,覆盖了从 PKCE 参数生成到密钥轮换的完整链路:
| 组件 | 密码学技术 | 安全保证 |
|---|---|---|
| PKCE | SHA-256 单向哈希 | 授权码截获无法交换 Token |
| JWT 签名 | ES256 (ECDSA P-256) | Token 不可伪造 |
| JWT 验证 | 算法白名单 + 声明校验 | 防止 alg=none 和算法混淆 |
| 密钥轮换 | JWKS + kid | 无缝轮换、旧 Token 自动过期 |
| 传输安全 | TLS 1.3 | 防止中间人攻击 |
关键教训:
- 公开客户端必须使用 PKCE:SPA、移动端无法安全存储 client_secret
- JWT 算法白名单是第一道防线:只接受
algorithms=["ES256"],拒绝 none 和 HS256 - Access Token 有效期 ≤ 15 分钟:短有效期是最简单的安全措施
- 密钥轮换是生产必备:通过 JWKS + kid 实现无缝轮换,旧密钥保留到 Token 过期
- 安全测试不可省略:每个攻击向量都必须有对应的测试用例
上一章完成了 TLS 握手实战。至此,密码学系列的全部实践章节已完成。
十、参考
- RFC 6749: OAuth 2.0
- RFC 7636: PKCE
- RFC 7519: JWT
- RFC 7517: JWKS
- OAuth 2.0 Security Best Current Practice
参考
- RFC 1945 — Hypertext Transfer Protocol — HTTP/1.0
- RFC 6749 — The OAuth 2.0 Authorization Framework
- RFC 7517
- RFC 7519 — JSON Web Token (JWT)
- RFC 7636 — Proof Key for Code Exchange by OAuth Public Clients (PKCE)
- RFC 6749: OAuth 2.0
- RFC 7636: PKCE
- RFC 7519: JWT
- RFC 7517: JWKS
- OAuth 2.0 Security Best Current Practice
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






