身份与访问控制
MFA、零信任、认证 vs 授权。
认证与授权:JWT、OAuth2 与 SSO
这一课你将能做到
- 手工拆解并验证一个 JWT,说清三段结构与签名的防篡改边界
- 走通 OAuth2 授权码流程,区分认证(Authentication)与授权(Authorization)
- 按校验清单加固本站 Spring Boot 平台的 JWT 实现,识别 none 算法与弱密钥攻击
为什么要学
认证回答「你是谁」,授权回答「你能干什么」。企业里几乎所有的越权事故都出在这两件事的交接缝上:token 没验签、算法被换成 none、secret 太弱被爆破、刷新令牌长期有效、SSO 登出不同步。你日常写的 @PreAuthorize、网关上的 JWT Filter,正是这条战线的前沿。
人话:JWT 就像一张「防伪工牌」——工牌上的信息(payload)人人可读,但上面的防伪章(签名)只有发证部门能盖;伪造信息会导致章对不上。
先建立图像
JWT = Header.Payload.Signature
eyJhbGciOi... . eyJzdWIiOi... . SflKxwRJSme...
│ │ │
│ │ └─ 签名:HMAC(secret, H.P) 或 RSA 签名
│ └─ Payload:sub、exp、iss、aud...(base64 可读,非加密!)
└─ Header:{"alg":"HS256","typ":"JWT"}
验签流程:服务端用 secret 重算 HMAC,与第三段比对
payload 改一个字节 → 签名必不匹配 → 拒绝
跟着做
步骤 1:手工拆解一个 JWT
# 用 Python 生成一个 HS256 token(观察结构,勿用于生产)
python3 - <<'EOF'
import hmac, hashlib, base64, json
def b64(d): return base64.urlsafe_b64encode(d).rstrip(b'=')
h = b64(json.dumps({"alg":"HS256","typ":"JWT"}).encode())
p = b64(json.dumps({"sub":"1001","role":"user","exp":1799999999}).encode())
secret = b"my-super-secret-key-change-me"
s = b64(hmac.new(secret, h+b"."+p, hashlib.sha256).digest())
print(f"{h.decode()}.{p.decode()}.{s.decode()}")
EOF
# 把三段分别 base64url 解码看内容(echo x | base64 -d)
步骤 2:篡改 payload 验证签名保护
把 payload 里的 "role":"user" 改成 "role":"admin" 后发请求,服务端验签必然失败——这是签名唯一的保证:防篡改,不是保密。
步骤 3:复现经典误配置(仅自建演示代码)
// ❌ 危险写法:信任 header 里的 alg 字段
Algorithm alg = "none".equals(header.get("alg"))
? Algorithm.none() // 等于不验签!
: Algorithm.HMAC256(secret);
// ✅ 正确:算法由服务端配置固定,绝不读取 token 自带的 alg
Algorithm alg = Algorithm.HMAC256(secret); // 白名单固定
DecodedJWT jwt = JWT.require(alg)
.withIssuer("java-nav").withAudience("web")
.acceptLeeway(30) // 时钟偏移容忍
.build().verify(token);
再演示弱密钥问题:secret 设成 123456,用 hashcat 或 jwt_tool 几秒内即可爆破出原值——这就是为什么 secret 必须 ≥256bit 且来自密钥管理。
步骤 4:走通 OAuth2 授权码流程
用本地 Keycloak(docker run -p 8081:8080 quay.io/keycloak/keycloak start-dev)建一个 client,然后观察完整跳转链: