大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
认证与授权: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,然后观察完整跳转链:
① 浏览器 → 授权端点 /authorize?client_id=x&redirect_uri=y&state=zz&scope=openid
② 用户在 Keycloak 登录并同意
③ 302 回调 → /callback?code=abc&state=zz (校验 state 防CSRF)
④ 后端拿 code + client_secret 换 token(POST /token)
⑤ 用 access_token 调 API,过期后用 refresh_token 换新
步骤 5:Spring Security 的 JWT 校验清单
对照检查你平台的过滤器实现:
| 检查项 | 要求 |
|---|---|
| 算法白名单 | 服务端固定,禁止信任 header 的 alg |
| exp 必验 | 拒绝无过期时间的 token |
| iss / aud 校验 | 绑定签发者与受众,防 token 跨系统复用 |
| jti 与黑名单 | 登出/封禁时吊销(Redis 存 jti 黑名单) |
| secret 强度 | ≥256bit 随机,来自 KMS/环境变量,支持轮换 |
| 传输 | 只经 HTTPS,不放 URL 参数 |
| 授权 | token 合法 ≠ 有权限,角色校验在服务端逐接口做 |
讲清楚要点
- JWT 的签名只防篡改,不防阅读:payload 是 base64,不是加密。敏感数据(手机号、身份证)不能直接塞进 payload。
- 认证 ≠ 授权:认证确认身份(登录),授权确认权限(鉴权)。JWT 过期了是认证失败;角色不对是授权失败,两者返回码与处理都不同。
- OAuth2 是授权框架,不是认证协议:授权码模式解决的是「第三方应用在不拿你密码的前提下获得受限访问权」。登录场景要配 OIDC(在 OAuth2 上加 id_token)才完整。
- 无状态 JWT 的代价是「无法主动失效」:好处是服务端免查库;代价是登出、封号、改密后旧 token 依然有效,必须用短期 access token(5-15 分钟)+ Redis 黑名单或 jti 轮换补偿。
- SSO 的本质是共享一个认证中心:多个子系统把「登录」委托给 IdP(OIDC 协议),各系统只保留授权判断。单点登出(SLO)是最容易漏的一环——主站退出了,子系统 session 还活着。
你可能会懵的地方
-
为什么 access token 短期、refresh token 长期? access token 每个请求都带,暴露概率高,有效期越短损失越小;refresh token 只在刷新时出现且可服务端吊销(存库可删),长期但可控。这是「高暴露短寿命、低暴露可吊销」的折中。
-
JWT 放 Cookie 还是 Authorization header? Cookie 自动携带(要配 HttpOnly+Secure+SameSite 防 XSS 窃取与 CSRF);header 需 JS 手动带(XSS 直接读 localStorage 反而危险)。两边都要做的核心是:token 不给 JS 碰(HttpOnly Cookie)+ 接口防 CSRF。
-
登录状态到底存服务端 session 还是 JWT? 单体应用 session + Redis 更可控(随时踢人);分布式/多端用 JWT 减少中心依赖,但要补黑名单。很多生产事故源于「选了 JWT 却没想清楚失效语义」。
-
state 参数真的必要吗? 必要。OAuth 回调若不校验 state,攻击者可把自己的 code 注入你的会话(登录 CSRF),让你不知不觉登录他的账号。state 一次一随机并绑定会话。
-
本站平台怎么做接口防刷配合认证? 登录接口按 IP+用户名双维度限流加验证码;已认证接口按 userId 限流。认证解决「谁」,限流解决「多快」,两者独立配置、独立告警。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 信任 token header 里的 alg | alg:none 直接绕过验签 | 服务端固定算法白名单 |
| JWT secret 弱或入库 | 离线爆破伪造任意身份 | ≥256bit 随机密钥 + KMS 管理 |
| token 永不过期 | 泄露即永久失陷 | 短期 access + 可吊销 refresh |
| 手机号放 payload | base64 一解即见 | 最小化 claim,敏感数据服务端查 |
| 前端判角色隐藏菜单即完事 | 直接调接口照样越权 | 服务端逐接口 @PreAuthorize |
卡点急救
- 验签一直失败:三段间是否被截断空格;base64url 与 base64 混淆;系统间时钟偏差超 leeway。用 jwt.io 或
jjwt逐段比对。 - 登出后 token 还能用:JWT 无状态天然如此。上 Redis 黑名单(jti、exp 为 TTL),或在网关层做版本号比对(用户改密后 tokenVersion+1)。
- OAuth 回调报 redirect_uri mismatch:回调地址必须在 IdP 精确注册(含协议、端口、路径)。本地调试把
localhost:8080/callback加进 client 配置,别用通配符。
面试常问
-
JWT 和 Session 的取舍? Session:服务端可控、随时吊销,但需要集中存储、多实例共享。JWT:无状态、跨服务友好,但失效需额外机制(黑名单/短过期)。答取舍要落到业务:内部单体偏 Session,多端多服务偏 JWT + 补偿机制。
-
OAuth2 四种模式,为什么授权码模式最常用? 授权码、简化(隐式,已不推荐)、密码式、客户端凭证。授权码把 token 交换放在后端(带 client_secret),避免 token 经浏览器暴露,且支持 refresh;隐式模式 token 直接走前端 URL,已被 OIDC+PKCE 取代。
-
如何防止 JWT 被伪造?关键点是什么? 服务端固定验签算法(防 none/算法混淆)、强密钥(防爆破)、校验 exp/iss/aud(防重放与跨系统复用)、HTTPS 传输。核心认识:签名防的是篡改,配置错误才是根因——绝大多数 JWT 事故是误用而非算法被破。
自测三问
- JWT 的三段分别是什么?payload 加密了吗?
- 为什么 access token 要短命?用什么机制补足「主动失效」能力?
- OAuth2 授权码流程中 state 参数防什么攻击?
小结一句话
认证确认身份、授权约束行为:JWT 用签名防篡改但配错即失守,OAuth2 用授权码换安全,SSO 的甜头后面跟着单点登出的坑。
延伸
- RFC 7519: JSON Web Token
- RFC 6749: OAuth 2.0
- 下一课:Web 攻击剖析——SQL 注入、XSS、CSRF、SSRF 与 OWASP Top 10。