主题
认证与鉴权
一句话:认证回答「你是谁」,鉴权回答「你能做什么」;OAuth2 解决授权委托,OIDC 在它上面补身份层,JWT 只是一种令牌格式。
适用:OAuth 2.1 草案、OpenID Connect 1.0、RFC 7636(PKCE)、RFC 7517(JWKS);JWT 校验以所用身份提供方文档为准。
为什么需要
单体应用可以用 Session 在服务端保存登录态。服务拆多、端变多之后,每个服务都去共享 Session 会把认证中心变成强依赖,跨域和移动端也不方便。
另一种做法是把身份信息放进签名令牌,让每个服务本地验签。它换来无状态和横向扩展,代价是令牌签发后难以撤销、密钥和声明校验一旦写错就会全线失守。
核心机制
先分清四个概念
- 认证(Authentication):确认主体身份,例如登录。
- 鉴权(Authorization):判断主体能否执行某个操作。
- OAuth2:授权框架,让第三方在用户同意下获得有限权限,不规定「用户是谁」。
- OIDC:构建在 OAuth2 上的身份层,用 ID Token 告诉客户端「用户是谁」。
把 OAuth2 当登录协议是常见错误:拿到 access token 只说明客户端被授权,不等于完成了用户认证。需要登录态时用 OIDC。
JWT 的结构与校验清单
JWT 是 Header.Payload.Signature 三段 Base64URL。Header 里有 alg 和 kid,Payload 是声明,Signature 保护前两段。
Payload 只是编码,不是加密,任何人都能解出内容。不要放密码、身份证号、完整手机号等敏感信息。
资源服务校验一个 JWT,至少要按顺序做:
- 解析 Header,算法必须在服务端白名单内(如 RS256/ES256),拒绝
none。 - 用
kid从可信 JWKS 取公钥;不要信任 token 里的jku/x5u指向的任意 URL。 - 验签。
- 校验
iss等于预期签发者。 - 校验
aud包含当前服务自己的 audience;不能只验签不验受众,否则给 A 服务的令牌可以拿去调 B 服务。 - 校验
exp、nbf、iat,允许少量时钟偏差(常见 60 秒)。 - 需要时校验
scope、azp、nonce(OIDC ID Token)。
OAuth2 的授权码模式与 PKCE
授权码模式:客户端把用户导向授权服务器,用户登录并同意后,授权服务器回调一个一次性 code;客户端后端用 code + client_secret 换 token。
公共客户端(SPA、移动 App)没有能力安全保存 client_secret,必须使用 PKCE:
text
1. 客户端生成随机 code_verifier(43–128 字符)
2. code_challenge = BASE64URL(SHA256(code_verifier))
3. 授权请求带 code_challenge 和 code_challenge_method=S256
4. 换 token 时带上原始 code_verifier
5. 授权服务器重新计算并比对PKCE 防的是授权码被截获后直接兑换。OAuth 2.1 已移除隐式模式和密码模式,授权码流程要求 PKCE;公共客户端没有 PKCE 不应上线。
客户端凭证模式用于服务间调用,没有用户参与;不要把它当成用户登录。
OIDC:身份层补在哪里
OIDC 在 OAuth2 上增加:
scope=openid时返回 ID Token(JWT),包含sub、iss、aud、exp、iat、nonce等。/.well-known/openid-configuration发现文档,给出authorization_endpoint、token_endpoint、jwks_uri。jwks_uri提供公钥集合,按kid轮换。- UserInfo 端点返回用户资料。
ID Token 是给客户端确认登录身份的,不是用来调用资源服务的;调用 API 用 access token。两者受众不同,校验规则也不同。
SSO:CAS 与 OIDC 的差别
CAS 的流程是:用户访问应用 A,未登录被重定向到 SSO;SSO 登录后发 TGT Cookie 和一次性 ST;应用 A 后端拿 ST 去 SSO 校验,通过后建立自己的局部会话。再访问应用 B 时,SSO 已有 TGT,直接发新 ST,不用重新登录。
OIDC SSO 则是统一 IdP 签发 ID Token/access token,各应用本地验签或调 UserInfo。前者需要后端回源校验,后者可以无状态,但撤销仍然要靠短 TTL 或中心化检查。
AK/SK 与 RBAC
AK/SK 用于服务间或开放平台:请求方用 Secret Key 对「方法 + 路径 + 时间戳 + 请求体摘要」做 HMAC-SHA256,服务端用同一套规则重算并比对。时间戳窗口(常见 ±5 分钟)防重放,nonce 缓存防同一请求重复使用。
RBAC 是用户 → 角色 → 权限。权限判断应该放在服务端,不能只靠网关;网关能挡住大部分入口,但内部调用和异步任务也要校验。
令牌安全
- Access Token 短 TTL(如 15 分钟),Refresh Token 长 TTL(如 7 天),只放 HttpOnly + Secure + SameSite Cookie。
- Refresh Token Rotation:每次续期都换新 RT,旧 RT 立即失效;同一家族旧 RT 被重复使用,判定为泄露,整个家族失效。
- 注销:短 TTL + Redis 黑名单存
jti;或者用不透明 token + 内省接口,撤销更直接。 - 常见攻击:
alg=none、算法混淆(把 RS256 公钥当 HS256 密钥)、弱 HS256 密钥、伪造kid/jku。防御核心是算法白名单和固定密钥来源。
最小上手
发现 OIDC 配置并查看 JWKS:
bash
curl -s https://issuer.example.com/.well-known/openid-configuration \
| jq '{issuer, authorization_endpoint, token_endpoint, jwks_uri}'
curl -s https://issuer.example.com/.well-known/jwks.json \
| jq '.keys[] | {kid, kty, alg, use}'资源服务校验伪代码(省略错误处理):
text
h = decode_header(token)
assert h.alg in ["RS256", "ES256"]
key = jwks.get(h.kid) # 未知 kid 时刷新一次 JWKS,限频
assert verify_signature(token, key)
c = decode_claims(token)
assert c.iss == EXPECTED_ISSUER
assert MY_AUDIENCE in c.aud
assert c.exp > now - 60
assert c.nbf <= now + 60生产动作
- JWKS 缓存:缓存 5–15 分钟;遇到未知
kid才刷新,刷新要限频,避免被伪造 token 打成放大攻击。 - 受众隔离:每个服务有自己的
aud;不要所有服务共用同一个 audience。 - 时钟:允许 60 秒偏差;服务器开启 NTP,否则
exp校验会随机失败。 - 撤销:高安全场景用不透明 token + introspection,或者短 TTL +
jti黑名单;不要指望纯 JWT 立即失效。 - Cookie:
HttpOnly; Secure; SameSite=Lax;状态变更接口加 CSRF Token 或校验Origin。 - AK/SK:Secret 不落日志、不放 URL;时间戳窗口 + nonce 防重放;密钥支持轮换和双密钥过渡。
- 排障:401 先看
iss/aud/exp和 JWKS 的kid是否匹配;403 看角色和 scope,不要把所有鉴权失败都归因于 token。
边界
- JWT 不是加密容器,Payload 对持有者可见;敏感数据不要放进去。
- OAuth2 不是认证协议;只接 OAuth2 不接 OIDC,拿不到可信的用户身份。
- 本地验签无法立即撤销;强撤销需求要用中心化校验,代价是每次请求多一次依赖。
- 不要把
client_secret放进 SPA 或移动端;公共客户端用 PKCE,不用 secret。 - SSO 只解决登录,不解决细粒度授权;RBAC/ABAC 仍要在业务侧做。
讲清楚
认证和鉴权要分开。用户登录用 OIDC,底层走 OAuth2 授权码 + PKCE;OAuth2 本身只解决授权委托,不告诉你用户是谁。JWT 只是令牌格式,资源服务验它要按顺序做:算法白名单、按 kid 从可信 JWKS 取公钥、验签、校验 iss 和 aud、校验 exp/nbf。公共客户端没有 secret,必须用 PKCE。JWT 签发后难以撤销,所以 access token 要短、refresh token 要轮换,紧急封禁靠 jti 黑名单或中心化内省。服务间调用用 AK/SK 签名加时间戳防重放,权限判断放在服务端而不是只放网关。