OAuth2/OIDC 与资源授权:三个真实进程
U30 / #67。教材入口:OAuth2与OIDC、资源授权。本实验使用真实 Spring Security 认证器、签名器、Servlet、网络端点与 OAuth2/OIDC 客户端;没有用 mock Authentication 代替协议。
运行与输入
需要 JDK21、Maven3.9、Python3.9+。macOS 可运行:
JAVA_HOME=$(/usr/libexec/java_home -v21) python3 labs/oauth2-authorization/run.py
# 只复现某组,产物放自己的唯一 cache 目录,不覆盖完整验收证据:
python3 labs/oauth2-authorization/run.py --case oidc --label local-oidcLinux 等环境显式设置 JAVA_HOME 到 JDK21。首次 Maven 下载依赖需要网络,不需要外部 OAuth 账号,也不需要 MySQL/Redis/Mongo/ES。脚本过滤继承的 Spring、server、lab、JVM agent 和 Maven 配置环境变量,使用 POM 中 Boot 4.0.9 BOM 管理的 Framework 7.0.10、Security 7.0.8;打包后逐项核对实际 jar。源码与运行证据分别标版本,不用旧 Security6.3.3 局部实验结果说明新协议。
公开源文件:POM、应用入口、AS完整配置与故障控制、Client完整配置、资源服务器完整配置、应用配置、runner、实际证据。
runner 构建一个可执行 jar,用 as、rs、client profile 启动三个独立 JVM。它先预留再释放三个不同随机 loopback 端口,若被其他程序抢占则显式失败,不会终止其他程序。AS/Client 分别使用 AS_SESSION/CLIENT_SESSION,防止同域不同端口的 session cookie 冲突。issuer、回调和 RS 地址均来自本次端口;不信任任意转发头。
仅合成用户 alice、内存客户端/授权数据、进程内随机 RSA A/B/foreign;lab-client 是有 secret 且显式要求 S256 的交互客户端,reference-client 是独立 secret 的机器客户端。源码内演示 secret/密码只用于本地合成数据,不能部署为生产身份服务。AS 保留 form CSRF;RS 只接受显式 Authorization Bearer header,不启用浏览器自动携带的 Basic、登录 session 或 cookie 认证,因此前缀 API 显式关闭 CSRF。这一前提不适用于自动凭据 API,详见 U29。
场景与判断
--case | 输入、独立预期与恢复 |
|---|---|
reference | 真实 client_credentials 颁发无 JWT 点分结构的 reference token;远程 active=true 访问200、本地 JWT 链401;AS revoke 后远程401。机器主体不是最终用户 |
roles | AS 登录含 CSRF;缺 challenge/plain=>invalid_request;错 verifier=>invalid_grant;正常授权码一次换 token;重放=>invalid_grant。单独新授权的 access token 可访问,ID Token 在JWT及opaque资源链401;后者对真实AS active的ID Token另做audience约束。错误兑换/重放后重新授权,避免沿用已失效 grant |
claims | 缺/畸形 token;真实私钥签 RS512、错签名、错 issuer、错 audience、过期、缺 exp、未来 nbf、未知 kid、缺 tenant 均401;每个坏输入后有效 RS256 token200。允许算法固定RS256、mandatory exp、时间偏差显式0秒、aud=orders-api、issuer固定本次AS;不代表所有框架默认都如此 |
authorization | 签名正确但 scope 缺失403;请求管理能力403;只有 authenticated 的入口调用被代理方法,无 write403/有 write200;同租户他人对象、跨租户对象、伪造tenant参数均403;服务端可信t1本人和可信t2本人分别200 |
lifecycle | 活跃token远程200;AS introspection 故障503,显式失败处理返回503且不产生身份;恢复200。实际撤销后远程401,同一个 JWT 本地仍200。未知token revoke也200。refresh禁复用,旧值400;撤销新refresh后不能再换token |
oidc | 真实 Spring Client 发 state、nonce、S256;AS回调后 Client 实际换 token、验 ID Token、保存 OIDC session,再由 OAuth2 RestClient 给 RS 发 access token。改state401;AS真实签名但错误nonce的ID Token401;失败后不能读身份;恢复后完整登录成功 |
rotation | A暖缓存;发布A+B并用B签名,未知B kid触发刷新后B200、A仍200;直连JWKS实际503期间已缓存B仍200、未知kid取钥失败503;直连恢复200,恢复B200;仅发布B后缓存A短时仍200;显式未知kid触发刷新后A401、B200。这个结果不证明撤公钥会即时通知其他节点缓存 |
/lab/mint 只生成反例/恢复用的真实签名合成 token;它不是标准颁发端点,也不经 AS 授权服务登记,所以不把它拿去证明 introspection/revocation。生命周期与reference场景的令牌全部来自真正 /oauth2/token。故障控制和mint需要每次runner生成的随机 X-Lab-Control,只绑定loopback;没有公开读取私钥的端点。
证据、错误与清理
每次运行输出独有的 ~/.cache/software-engineering-study/java-depth-impl/u30/run-*/result.json,包含环境、命令退出状态、打包依赖、源文件与jar SHA-256、脱敏HTTP预期/实测、进程退出/回收。完整入口成功才更新仓库内的验收JSON,单组或失败不覆盖它。HTTP记录不保存完整 token、code/verifier/state/nonce、Authorization/Cookie、密码或私钥;仅保留路径、参数名、状态和合成场景标签。原始构建/服务日志留在本次cache,不上传真实账号或数据。
runner 在 finally 中只 TERM 自己创建的进程组,wait 回收;必要时 KILL 自己进程。JVM 因 TERM 通常退出143,入口所有断言通过后退出0,两者含义不同。RSA材料只在AS内存,停止进程即释放;不改系统服务、数据库或浏览器数据。target 和自己的cache用于排错,不冒称已删除。
开发中的真实失败包括旧AS DSL不适用7.0.8、reference客户端未注册返回401、内存仓库禁止重复client secret、introspection异常被错误分派掩盖、JWKS故障filter晚于端点导致旧“宕机”断言未证明注入;增加直连JWKS503/200的负控后移早filter,两资源链显式将基础设施故障映射503,逐项修正后以完整入口复验。行为失败与编译/启动失败分开记录在开发验证记录,不把编译失败算作协议负例。
证明边界
这是单节点、合成数据、loopback HTTP 的真实协议实验;没有生产TLS、企业IdP、数据库耐久、跨节点session/撤销传播或性能压测。reference有效期由真实AS active判定;本票JWT过期/nbf使用真实签名的时间反例,不虚称已经等待完整生产TTL。网络故障注入是端点503,未声明真实TCP超时/慢响应实验。JWK缓存是本次已观测路径,未声称完整5分钟TTL及所有缓存实现已测。
ACL JDBC、Run-As、WebSocket消息规则、Spring Data查询、DPoP、多issuer动态路由、PAR/device、token exchange、OIDC RP/back-channel注销按固定源码/配置讲解,其运行边界见正文;不把本入口核心EX3的成功当成这些扩展已实跑。安全头/真正TLS与CSRF等机制复用U29独立证据。