OAuth 2.0 与 OpenID Connect:授权码、身份断言与重放边界
拆解授权码加 PKCE、state、nonce、redirect URI、Access Token、ID Token、刷新与发送者约束。
RFC 9700 是 OAuth 2.0 安全最佳当前实践,要求精确 redirect URI 匹配并淘汰不安全流程;OpenID Connect Core 1.0 在 OAuth 之上定义 ID Token 与身份认证。
OAuth 解决委托,不直接证明登录身份
资源所有者让客户端获得有限 API 权限,Authorization Server 发令牌,Resource Server 按 audience 与 scope 判断访问。OpenID Connect 增加 ID Token、UserInfo 和认证语义,让客户端验证谁完成了认证。把 Access Token 当登录票据会混淆受众、主体与用途。
Authorization Code 流让浏览器只携带短期一次性 code,客户端在后端换 token。PKCE 用 code_verifier 将 code 绑定到发起客户端;state 将回调绑定到浏览器会话并防止登录 CSRF;OIDC nonce 将 ID Token 绑定到认证请求。三者解决不同关联,不可互相替代。
回调端点是协议状态机
redirect URI 使用预注册精确匹配,code 一次性且短时,换取时校验 client、redirect URI 与 PKCE。客户端收到响应后先匹配 state,再验证 token endpoint 的响应;OIDC 还校验 issuer、audience、签名、nonce 与时间。任何一步失败都终止流程并清理临时状态。
前端通道可能被历史记录、扩展、日志和开放重定向观察。令牌不放 URL,授权服务器和客户端都不提供任意跳转。多个 issuer 共用回调时必须防止 mix-up,用预期 issuer 与发现元数据约束端点。
Token 权限要比用户权限更窄
scope、resource/audience、生命周期与客户端类型共同限制 token。Refresh Token rotation 在每次使用后签发新值并检测旧值重放;公共客户端不能保守客户端秘密。高价值 API 可用 mTLS 或 DPoP 将 access token 绑定发送者,降低 bearer token 泄露后的可用性。
撤销、登出与本地 Session 是不同状态。上游 IdP 会话结束不保证所有 access token 立即失效,本地退出也不一定注销 IdP。系统必须分别定义前台会话、刷新链、访问令牌和账户禁用的收敛时间。
浏览器、客户端和资源服务各守一段边界
浏览器保存 state/nonce 关联但不持有 confidential client secret;客户端负责回调、code 兑换和本地 Session;资源服务只接受面向自己的 access token,不信任客户端口头传来的用户信息。Authorization Server 的登录成功也不能让客户端跳过 issuer、audience 与 nonce 校验。
多租户 SaaS 若按邮箱域自动选择 issuer,必须把发起时 issuer 写入关联状态,回调只能返回同一 issuer。动态发现元数据需要固定允许的发行者和 TLS 来源,不能用回调参数拼 discovery URL。JWKS 刷新对未知 kid 有频率上限,避免攻击者制造无限远程拉取。
结果未知和刷新并发必须建模
客户端向 token endpoint 发送 code 后超时,不知道 code 是否已使用;重试相同 code 可能得到 invalid_grant,不能转而重启一个不关联原会话的登录并覆盖状态。刷新令牌轮换遇到两个并发请求时,服务端应识别同一 token family,允许受控短窗口或让客户端串行刷新,不能把正常竞态误判后永久锁死。
协议测试覆盖 state 交换、nonce 复用、错误 issuer、多个 audience、过期 code、重复 code、redirect 细微差异、旧 refresh 重放和 access token 错用为 ID Token。断言不仅是 HTTP 状态,还要确认没有创建本地会话和没有留下可继续兑换的临时状态。
同意页面与 scope 设计决定真实授权边界
scope 不能只是后端接口名的集合,应表达用户和管理员能理解的能力,并让资源服务进一步做对象级授权。一次授予 read:orders 不代表可读所有租户订单;offline_access 也不应默认出现。增量授权在真正需要高权限时再请求,减少长期 refresh token 的爆炸半径。
同意记录关联用户、客户端、资源、scope、策略版本和时间,撤销后停止刷新并让资源服务在可接受窗口内收敛。客户端显示名、Logo 与 redirect 域是钓鱼界面的一部分,动态注册不能让未审查客户端伪装成受信应用。
BFF、SPA 与原生客户端采用不同凭证边界
BFF 可把 token 留在服务端,只给浏览器受保护 Session Cookie,降低脚本直接读取 token 的风险,但重新引入 CSRF 与 Session 运维。SPA 是公共客户端,不能保守 client secret,使用授权码 + PKCE 并尽量缩短 token 驻留;原生应用使用系统浏览器和注册的回调机制,不能嵌入 WebView 偷取凭证。
客户端类型影响 token endpoint 认证、refresh token 策略和重定向方式,但不改变资源服务器的 audience 与对象授权义务。架构评审应画出 token 在浏览器存储、进程内存、日志、代理和下游之间的全部路径,并证明没有把 bearer token 复制到不需要的组件。
故障处置区分授权服务器不可用、JWKS 暂不可达和令牌内省超时。已有离线可验证 token 可在缓存密钥有效期内继续,但未知 key 或高风险撤销查询失败时默认拒绝;不能把身份系统故障变成全站匿名写入,也不能让每个请求同步依赖远程 JWKS。
协议观测要保留关联而不泄露令牌
客户端记录随机 flowId、预期 issuer、clientId、redirect route、state 摘要、PKCE 方法、授权结果类别和本地 Session id 摘要;不记录 code、verifier、access token、refresh token 或完整 ID Token。资源服务记录 issuer、audience、subject 的受控标识、scope 集合摘要和拒绝原因,便于区分配置漂移、重放与权限不足。
关键指标包括 flow 启动/完成比、state/nonce mismatch、code exchange invalid_grant、JWKS unknown kid、refresh reuse、token audience reject 和 issuer 延迟。大量 invalid_grant 可能是并发刷新、回调重试或攻击,需结合 token family 和客户端版本判断,不能自动把所有用户登出形成二次事故。
迁移 IdP 时采用双发行者读取、单发行者写入:资源服务先信任受控的新旧 issuer 且规则隔离,客户端逐批切换授权端点,观察旧 Session/refresh 到期,最后撤销旧信任。禁止在迁移期关闭 issuer 校验或把两个 issuer 的同名 subject 当同一用户,账户关联必须有独立稳定映射。
安全状态机必须让失败停在安全位置
一次安全决策沿“解析—认证—授权—执行—审计”推进。任何阶段超时或证据不足都不能伪装成允许;同时要区分未认证、无权限、输入拒绝、依赖不可用和结果未知,避免客户端盲目重试。
反向验证要证明旁路也被关闭
测试不止提交非法字符串,还要更换租户、对象、HTTP 方法、内容类型、转发头、异步线程、重定向目标和旧版本凭证;在认证服务、审计存储、DNS、密钥源和策略缓存超时时观察默认行为。拒绝必须无业务副作用,错误响应不泄露资源存在性或内部实现。
容量同样属于安全边界。密码派生、签名验证、文件扫描、令牌内省和审计写入都可能被放大;为主体、租户、IP、操作和下游分别设置预算,并记录拒绝原因。限流不能只按可伪造 Header,也不能让攻击流量占满合法用户恢复通道。
生产证据与治理
上线门禁要求:威胁场景有 owner;允许和拒绝路径都有测试;默认拒绝与应急旁路已演练;秘密可轮换;审计可查询且防越权;依赖与构件来源可证明;旧版本兼容窗口和撤销路径明确。安全控制只有在故障、升级和应急状态仍成立时才算完成。
用两个 Java 17 模型固定安全不变量
模型不尝试实现密码算法或攻击载荷,只把控制输入、判定顺序和确定输出固化,真实系统再替换为框架、密钥服务与持久化策略。
javac --release 17 -Xlint:all -Werror examples/backend-development/security/oauth-oidc/PkceStateDemo.java examples/backend-development/security/oauth-oidc/OidcValidationDemo.java
java -cp examples/backend-development/security/oauth-oidc PkceStateDemo
java -cp examples/backend-development/security/oauth-oidc OidcValidationDemostateMatches=true pkceVerified=true codeUsedOnce=true redirectExact=true tokenIssued=true
issuerValid=true audienceValid=true nonceValid=true accessTokenAsIdToken=false identityAccepted=true输出变化必须对应一条明确策略或迁移决定;禁止为了让测试通过而放宽 issuer、audience、tenant、路径、算法或来源校验。
把控制成本写进容量与事故恢复
“每个回调、代码和令牌都必须绑定客户端、会话、受众、发送者与一次性上下文”不是免费承诺。认证与密码派生消耗 CPU,远程策略和 Session 占用连接,签名与哈希增加计算,扫描和隔离消耗磁盘与队列,审计增加写放大。容量模型按峰值合法流量叠加恶意放大:每请求验证次数、下游往返、最大输入、并发隔离任务、失败重试、日志体积和安全状态保留期。平均延迟正常而拒绝队列持续增长,仍然意味着控制即将失效。
事故恢复按“阻止继续扩大—保存证据—撤销资格—修复权威状态—重建派生状态—验证不变量”推进。只修代码却不撤销 token、secret 或错误策略缓存,攻击窗口仍然存在;只轮换凭证却不检查已发生业务写,也无法证明资产恢复。对 PkceStateDemo 与 OidcValidationDemo 对应的决策输出建立线上指标或审计查询,才能把模型与生产证据连接起来。
升级时先部署严格兼容的读取端,再切换签发者或写入端,观察旧版本使用归零后删除兼容分支。任何临时兼容都记录 owner、截止、拒绝指标和回滚条件;为了兼容而跳过验证不属于迁移策略,而是新增旁路。
