跳到正文

OAuth2 与认证鉴权 ​

一句话:OAuth2 是授权委托框架,让资源所有者把「有限权限、有限时间」的访问权交给客户端;它回答的是「你能做什么」,不回答「你是谁」——身份层要由 OIDC 的 ID Token 补上。

适用版本:OAuth 2.0(RFC 6749)、Bearer Token(RFC 6750)、PKCE(RFC 7636)、令牌撤销(RFC 7009)、令牌内省(RFC 7662)、OIDC Core 1.0;OAuth 2.1 仍为草案。协议默认值与实现细节以官方文档为准。

为什么需要 ​

第三方应用要读用户数据,最直接的做法是让用户交出密码,但这带来三个问题:密码泄露面扩大、应用拿到全部权限、用户无法单独撤销某一个应用。OAuth2 把「用户凭据」和「受限授权」分开:用户在授权服务器登录,客户端只拿到一个有范围、有期限的 access token,资源服务器只认这个令牌。

但 access token 只证明「持有者被授权访问某个资源」,不证明用户身份。把它当登录态会掉进三个坑:令牌被复制就能冒用、不同客户端的令牌语义不同、资源服务器无法确认「登录的是谁」。OIDC 在 OAuth2 之上增加 ID Token(JWT)和 UserInfo,才构成认证协议。

核心机制 ​

角色与令牌 ​

角色/令牌作用关键约束
Resource Owner授权的人只在授权服务器输入凭据
Client代用户访问资源分公共客户端(无 secret)与机密客户端
Authorization Server认证用户、签发令牌持有签名私钥与客户端注册信息
Resource Server校验令牌、返回资源只信签名与 claims,不信任请求来源
access token访问资源的凭证短生命周期、Bearer 语义、必须走 TLS
refresh token换取新 access token只发授权服务器,可轮换、可撤销
authorization code授权码流程的中间票据一次性、短寿命、绑定 redirect_uri 与 PKCE
ID Token(OIDC)证明用户身份JWT,含 iss/aud/sub/nonce

授权码 + PKCE ​

text
用户 → 客户端 → 授权服务器 /authorize
  client_id, redirect_uri, response_type=code, scope, state,
  code_challenge, code_challenge_method=S256
授权服务器 → 用户:登录 + 同意授权
授权服务器 → 客户端:302 redirect_uri?code=...&state=...
客户端 → 授权服务器 /token
  grant_type=authorization_code, code, redirect_uri, client_id,
  code_verifier(机密客户端再加 client_secret 或 private_key_jwt)
授权服务器 → 客户端:access_token, refresh_token, id_token
客户端 → 资源服务器:Authorization: Bearer <access_token>

四个参数各自防一类问题,缺一个就留下缺口:

  • state:客户端生成随机值,随授权请求发出,回调时原样比对。它把回调绑定到发起请求的浏览器会话,防登录 CSRF 和授权码注入。
  • PKCE:客户端生成 code_verifier,把 BASE64URL(SHA256(verifier)) 作为 code_challenge 发出;换令牌时再出示 verifier。授权码即使被截获,没有 verifier 也换不到令牌。公共客户端没有 client_secret,PKCE 是唯一能把授权码绑定到发起方的手段;OAuth 2.1 草案要求授权码流程使用 PKCE,plain 方法仅为兼容旧实现。
  • redirect_uri 白名单:注册时逐条登记,回调时做完整字符串精确匹配,不允许通配符、路径穿越或开放重定向。否则攻击者能把授权码送到自己的域名。
  • nonce(OIDC):请求时带上,ID Token 必须原样回传;客户端校验它,防 ID Token 重放。state 防的是授权响应被替换,nonce 防的是身份断言被重放,两者不能互相替代。

授权码一次性使用,RFC 6749 建议寿命不超过 10 分钟,工程上通常更短。隐式模式把令牌放进 URL fragment,会留在浏览器历史和 Referer 中,已废弃;密码模式让客户端接触用户密码,OAuth 2.1 已移除。服务间无用户参与时用客户端凭证模式,此时没有 refresh token,客户端本身就是资源所有者;开放平台若不用 OAuth2,常用 AK/SK + HMAC(method+path+timestamp+body)做请求签名,timestamp 窗口 5 分钟、nonce 去重防重放。

令牌生命周期 ​

access token 的寿命没有协议默认值,15 分钟是常见设计值而不是规范要求;原则是「泄露后的暴露窗口」可接受。refresh token 寿命更长,因此推荐轮换(OWASP 建议,不是协议强制):每次刷新都返回新的 refresh token,旧的立即失效;如果旧 token 再次被使用,说明它已泄露,授权服务器应撤销整个 token family 并强制重新登录。用户登出、改密、权限变更时调用撤销接口(RFC 7009),不要只删客户端本地存储。

JWT 与 opaque token 的取舍在于校验位置:

维度JWT(自包含)Opaque token
校验资源服务器本地验签调授权服务器 introspect(RFC 7662)
撤销困难,只能等过期或维护黑名单立即生效
性能无远程调用每次多一次网络请求
适合短寿命、高并发、可接受延迟撤销需要即时封禁、令牌内容不透明

JWT 的 Payload 只是 Base64URL 编码,不是加密,任何人都能读;不要放手机号、身份证号等敏感信息,需要保密就用 JWE。JWT 的三个常见攻击面是:alg=none(服务端必须用配置的算法白名单验签,绝不信任 Header 里的 alg)、HS256 弱密钥离线爆破(密钥至少 256-bit 随机值,高安全场景用 RS256/ES256 私钥签名、公钥验签,具体固定哪一种由授权服务器注册决定)、客户端存储不当(access token 放内存,refresh token 放 HttpOnly; Secure; SameSite Cookie,避免 localStorage 被 XSS 读取)。

谁在什么时候校验什么 ​

时机执行方必须校验
/authorize 请求进入授权服务器client_id 已注册;redirect_uri 精确匹配;response_type、scope 合法;把 state(回显)与 PKCE challenge 绑定到本次授权请求
用户登录授权服务器用户凭据与 MFA;这一步是认证,不是 OAuth2 授权
回调到达客户端客户端state 与本地会话一致;无 error 参数;code 存在;不信任回调里的任何用户信息
/token 换令牌授权服务器code 未使用且未过期;redirect_uri 与授权请求一致;code_verifier 与 challenge 匹配;机密客户端完成客户端认证
每次资源请求资源服务器签名(JWKS 按 kid 取公钥)、算法白名单、iss 精确匹配、aud 包含自身、exp/nbf(允许小幅时钟偏移)、scope 足够;opaque token 走 introspect
收到 ID Token客户端签名、iss、aud 包含自己的 client_id、exp、nonce;需要时校验 at_hash
刷新令牌授权服务器refresh token 有效且未被轮换;检测重复使用;校验客户端身份;新 scope 不得超出原授权

关键点:网关验签不等于业务授权。网关只能确认令牌有效,资源服务器仍要按 sub/租户/角色做对象级权限判断,否则一个合法令牌就能越权读别人的数据。

最小上手 ​

bash
# 1. 生成 PKCE verifier 与 S256 challenge(OpenSSL 3.x)
verifier=$(openssl rand -base64 48 | tr -d '\n=+/' | cut -c1-64)
challenge=$(printf %s "$verifier" | openssl dgst -sha256 -binary \
  | openssl base64 -A | tr '+/' '-_' | tr -d '=')
state=$(openssl rand -hex 16)
echo "https://auth.example.com/authorize?response_type=code&client_id=web\
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcb&scope=openid%20profile\
&state=$state&code_challenge=$challenge&code_challenge_method=S256"

# 2. 浏览器完成授权后,用回调里的 code 换令牌(公共客户端不带 secret)
curl -s -X POST https://auth.example.com/token \
  -d grant_type=authorization_code -d code="$CODE" \
  -d redirect_uri=https://app.example.com/cb -d client_id=web \
  -d code_verifier="$verifier"

# 3. 看授权服务器公布了哪些签名密钥
curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys[] | {kid, alg}'

资源服务器侧用库验签,不要手写解析:

python
import jwt
from jwt import PyJWKClient

jwks = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
key = jwks.get_signing_key_from_jwt(token)
claims = jwt.decode(
    token, key.key, algorithms=["RS256"],
    audience="https://api.example.com",
    issuer="https://auth.example.com",
    options={"require": ["exp", "iat", "iss", "aud", "sub"]},
)

algorithms 是白名单,PyJWT 会校验 exp/nbf/aud/iss;kid 找不到时应刷新 JWKS 缓存一次,而不是跳过验签。

生产动作 ​

  • JWKS 与轮换:缓存公钥并设置 TTL;遇到未知 kid 刷新一次;新旧密钥并存至少一个 access token 寿命,避免轮换瞬间全员 401。
  • 时钟偏移:exp/nbf 允许 60 秒左右的偏移是工程值,不是协议默认;服务器统一 NTP,时钟漂移会表现为大面积 401。
  • 令牌不外泄:不把 access token 放 URL、日志、埋点;Bearer token 谁拿到谁就能用,TLS 是底线。
  • 撤销路径:登出、改密、角色变更、检测到 refresh token 重复使用,都要撤销 refresh token 及相关会话;紧急封禁可叠加 jti 黑名单,但别把黑名单当常规撤销方案。
  • 权限收口:scope 按最小权限授予,aud 按 API 区分;资源服务器做对象级校验,网关只做第一道。
  • 监控:401 突增先查密钥轮换与时钟;403 突增先查权限策略变更;refresh 失败率与授权码重复使用要单独告警。

边界 ​

  • OAuth2 不是登录协议。要做「用某平台登录」,用 OIDC;只拿 access token 当身份凭证,等于把「有权限」误当成「是本人」。
  • JWT 不适合需要即时撤销的长寿命会话。无状态是优点也是代价;需要随时踢人就用短 token + refresh 轮换,或改用 opaque token + introspect。
  • PKCE 不保护被攻破的客户端。它能防授权码截获,防不了恶意 App 自己把 verifier 交出去。
  • SSO 不等于 OAuth2。SSO 是「一次认证、多系统复用登录态」,可以用 OIDC、SAML 或 CAS 实现;OAuth2 只是授权委托,不能单独完成 SSO。
  • 各家「第三方登录」有差异。微信、飞书等平台的 scope、用户信息接口和 ID Token 语义并不完全等同 OIDC,接入前以对方文档为准。

讲清楚 ​

OAuth2 解决的是「用户不想把密码交给第三方,但仍要让第三方访问一部分数据」。它把授权拆成授权服务器、客户端、资源服务器三方:用户在授权服务器登录,客户端拿到短期 access token,资源服务器验令牌后放行。授权码模式加 PKCE 是关键,code 一次性、绑定 redirect_uri 和 verifier,state 防回调被替换,OIDC 再用 ID Token 的 nonce 和 aud 补上身份层。access token 要短,refresh token 要轮换并能在泄露时撤销;JWT 校验必须走 JWKS 和算法白名单,并核对 iss、aud、exp。它最大的边界是:OAuth2 只回答「能访问什么」,不回答「是谁」,把它当登录协议是最常见的误用。

把一门技术讲透到既懂也会用