Files
smart-go/.cursor/plans/oauth2_pkce_单体设计_62bcf65b.plan.md
T
2026-04-23 18:58:13 +08:00

18 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
OAuth2 PKCE 单体设计 在单体部署中同时承担 AS 与 RS:逻辑分层 + **opaque access token**(随机串)存 PostgreSQL/RedisRS 中间件按 token 查元数据完成校验;可选暴露 RFC 7662 introspection 供未来独立资源服务。前后端分离下后端不渲染 HTML,仅 JSON 登录与 302OAuth 2.1 + 强制 PKCE,与现有 IAM 对齐。
id content status
schema-oauth 设计并迁移 oauth_client、opaque access_token / refresh_token 元数据表(或 Redis)、authorization code 存储策略 completed
id content status
session-login JSON 登录 API + HttpOnly Cookie 与 Redis 会话;未登录访问 /oauth/authorize 时 302 到配置的前端 URL(携带完整回跳 authorize 链接或 request_id),无后端 HTML completed
id content status
auth-code-pkce 实现 /oauth/authorize 与 /oauth/tokenauthorization_code + S256 PKCE + state completed
id content status
opaque-token-mw 签发 opaque access token(高熵随机串,元数据存库)、Gin Bearer 中间件内联查库/Redis 解析用户与 scope;可选 POST /oauth/introspectRFC 7662 completed
id content status
refresh-scope refresh_token 轮换、scope 与租户/权限映射、限流与审计 completed
false

OAuth2 Authorization Code + PKCE(单体 AS+RS)设计

可行性结论

可以这样做,而且是合理选择。 单体同时作为 AS 与 RS 并不意味着违反 OAuth 模型:OAuth 描述的是角色与端点,不要求物理上分属不同进程。同一服务内:

  • 授权服务器:提供 /oauth/authorize/oauth/token(以及可选的授权服务器元数据、令牌自省、撤销端点)。
  • 资源服务器:现有 /api/v1/... 等业务 API,通过 Authorization: Bearer 校验访问令牌。

部署形态为单体时,AS 签发 opaque 令牌并写入存储,RS 与 AS 同进程:中间件直接 查库/Redis 解析 sub(用户 id)、租户、scopeexp无需 JWT 验签,也不必为 RS 单独 HTTP 调 introspection(除非你愿意统一走自省接口以保持代码路径单一)。若未来 RS 独立部署,再启用 RFC 7662 introspection 或共享 Redis/DB 读副本。

选型说明(已定)Access token 采用 opaque——撤销可即时生效(删行或标 revoked)、不暴露声明给客户端解析;代价是每次 API 请求多一次存储读取(可用 Redis 缓存 + 短 TTL 与 DB 一致)。

与仓库内 docs/oauth-v2.md(OAuth 2.1 草案)对齐的要点:

  • Authorization Code 为主流程;PKCEcode_challenge / code_verifier,推荐 S256)对各类客户端(含机密客户端)可防范授权码注入,草案要求除例外情况外强制使用(见文档中 4.1 与 7.5 节相关段落)。
  • Redirect URI 须严格字符串匹配(2.1 比 2.0 更严)。
  • Implicit 等 grant 在 2.1 中不再规定,不要实现隐式授权。
  • 生产环境 HTTPS短期 access tokenscope/audience 约束为最佳实践。

前后端分离约束(后端不返回 HTML

本方案与「后端只提供 API」兼容OAuth 规范里授权端点本来就是 HTTP 重定向 与浏览器导航,不要求授权服务器返回登录表单 HTML。

  • 登录页、同意页:由前端 SPA 渲染(独立域名或 /login 路由均可)。
  • 后端提供:POST /api/v1/.../login(或 auth/login)等 JSON 接口校验 iam_user 凭据,成功后通过 Set-CookieHttpOnly、Secure、SameSite 按跨站需求选择)建立会话,响应体不写 HTML
  • GET /oauth/authorize:仅返回 302(已登录则带 code 重定向到 client;未登录则重定向到配置的前端登录 URL),或 302 到错误 redirect_uri(OAuth 标准错误响应)。禁止用 HTML 表单作为唯一交互方式。

未登录时的典型串联(浏览器内):

  1. 用户浏览器访问 AS 的 /oauth/authorize?...(含 PKCE、state)。
  2. AS 发现无会话:302FRONTEND_LOGIN_URL,并在 query 中携带「回到 authorize 所需信息」——常见两种做法(二选一或组合):
    • 回跳链接next / return_to = urlencode(完整 authorize URL),前端登录成功后执行 location.href = decode(next) 再次命中 authorize(此时带 Cookie)。
    • 短期票据login_challenge / request_id 指向 Redis 中暂存的 authorize 查询参数,前端登录成功后调用后端「完成登录并继续授权」或仍用回跳 authorize URL(实现简单优先前者需额外端点)。
  3. 用户在 SPA 输入账号密码 → POST JSON 登录 → Cookie 写入。
  4. 浏览器再次 GET 同一 /oauth/authorize?...(PKCE 参数必须与首次一致,故回跳必须带齐原始 query),AS 有会话则签发 code302 到第三方/首方应用的 redirect_uri

跨域 Cookie:若 API 与 SPA 不同站点,需 SameSite=None; Secure 且 CORS 对登录与 authorize 请求配置 credentials,并在配置中固定 CookieDomain/Path;若同站不同路径则可用 Lax。此项在 pkg/config 中显式配置并写入计划实施阶段。

同意(consent:若需要显式授权,同样 302 到前端同意页(带 consent_challenge 或回跳 authorize),用户提交后由前端再导航回 authorize 或调用完成端点——仍无后端 HTML


目标架构(逻辑视图)

flowchart LR
  subgraph client [OAuth客户端]
    Browser[浏览器/SPA]
    Native[原生应用]
  end
  subgraph monolith [单体后端]
    AuthZ["/oauth/authorize"]
    LoginAPI["POST JSON 登录"]
    Token["/oauth/token"]
    RS["/api/v1 资源API"]
    Intro["可选 /oauth/introspect"]
  end
  subgraph spa [前端应用]
    LoginUI[登录页UI]
  end
  subgraph store [持久化]
    PG[(PostgreSQL)]
    Redis[(Redis可选)]
  end
  Browser --> AuthZ
  AuthZ -->|未登录302| LoginUI
  LoginUI --> LoginAPI
  LoginAPI -->|Set-Cookie| Browser
  Browser --> AuthZ
  AuthZ -->|302 code| Browser
  Browser --> Token
  Token -->|opaque| Browser
  Browser --> RS
  RS -->|查 token 元数据| PG
  RS -.->|独立 RS 时| Intro
  Token --> PG
  LoginAPI --> Redis

核心组件设计

1. OAuth 客户端注册(Client Registry

需要能校验 client_id、允许的 redirect_uri 列表、客户端类型(public / confidential)。

  • 首方应用(自有前端):可用配置 + 种子数据或表 oauth_clientclient_id、哈希后的 client_secret(仅 confidential)、redirect_uris[]grant_typestoken_endpoint_auth_method)。
  • 第三方:同样走表结构,由管理 API 维护(可后续迭代)。

OAuth 2.1 对 public client 不在 token 端点使用 secretconfidential client 使用 client_secret(或 private_key_jwt,可二期)。

2. 授权端点 GET /oauth/authorize

查询参数RFC/2.1):response_type=codeclient_idredirect_uriscopestate强烈建议必填,防 CSRF)、code_challengecode_challenge_method=S256

服务端行为

  1. 校验 client、redirect_uri、PKCE 参数完整且 method 支持。
  2. 若用户未登录302 到配置项 FRONTEND_LOGIN_URL(或等价路径),并携带完整回跳到本端点所需的参数(见上文「前后端分离约束」:next/return_torequest_id)。不返回 HTML。登录成功依赖后续 POST JSON 登录 + HttpOnly Cookie + Redis 会话(与 internal/server/http.go 中间件顺序一致)。
  3. 已登录用户:若需同意且未静默授权:302FRONTEND_CONSENT_URL(同样仅重定向,无 HTML);首方可配置静默同意跳过。
  4. 生成 authorization code(一次性、短 TTL,如 60–120s),绑定client_idredirect_uricode_challenge/method资源所有者 id(即 iam_user 主键)、租户tenant_id)、请求的 scope。存 PostgreSQL 或 RedisRedis 更适合极短 TTL)。
  5. 302 到 redirect_uri?code=...&state=...

3. 令牌端点 POST /oauth/token

支持:

  • grant_type=authorization_codecoderedirect_uriclient_idcode_verifierPKCE);confidential 客户端另加 client_secret 或 HTTP Basic。
  • grant_type=refresh_tokenrefresh token 轮换(发新废旧,旧 token 入库标记撤销)推荐。

校验

  • 校验 code 未使用、未过期;code_verifier 与当时存的 code_challenge 按 S256 比对(与 docs/oauth-v2.md 一致)。
  • 颁发 access_tokenopaque,高熵随机串)与 refresh_tokenopaque);存库字段建议token 或仅存 哈希(防 DB 泄露即泄露明文)、user_idtenant_idclient_idscopeexpires_atrevoked_at 等。

错误响应遵循 OAuth 2.1 的 application/json 错误体约定。

4. Opaque Access Token 与资源服务器校验(默认方案)

Access token 为不可解析的随机串(opaque),使用 JWT 自包含声明。

存储与撤销:表(或 Redis)保存令牌元数据;撤销时删除或标记 revoked立即生效

RS 中间件(挂在 apiGroup 上需保护的子树):

  • Authorization: Bearer 取出 token查存储(优先 Redis 缓存命中,miss 回源 PG)得到 user_idtenant_idscopeexp
  • 校验未过期、未撤销;按需校验 scope
  • user_idtenant_idscope 写入 gin.Context,替代当前仅靠 X-User-ID / X-Tenant-ID 的占位方式;对内网遗留 header 仅过渡且需防伪造。

可选 POST /oauth/introspectRFC 7662):与上述查表语义一致,供未来独立 RS 或统一审计;单体同进程内可直接复用同一套存储访问逻辑,不必强制每次 API 都走 HTTP 自省。

与 JWT 对比:本方案不暴露 /.well-known/jwks.json(除非日后为 id_token 引入 JWT);若将来要支持纯离线验签再另议。

5. Scope 与现有 IAM 的映射

  • 最小集:openid(若上 OIDC)、profileemail(按需,OIDC 层再扩展)。
  • API 资源:api 或细分 user.readadmin 等,与 iam_menu.perms / 角色体系映射:opaque 行内可存 scope 字符串;细粒度权限可在中间件加载后按 user_id+tenant_id 再查角色/菜单(与「快照进 token」二选一,避免存储行过大)。

6. 安全与运维要点

  • HTTPS 强制(开发可用本地证书或反向代理)。
  • Redirect URI 精确匹配;禁止 open redirect。
  • Authorization code 单次使用;建议绑定 PKCE(已实现则满足 2.1 核心要求)。
  • Refresh token:哈希存储、轮换、可撤销。
  • 限流/oauth/token、登录接口按 IP/client 限流。
  • 审计:登录、发 token、撤销记日志。

与现有代码库的衔接点

区域 作用
internal/server/http.go 注册 /oauth/*、可选 /.well-known/oauth-authorization-server/oauth/introspect,并为 /api/v1 挂 Bearer 中间件
internal/iam 复用用户校验(密码可用 pkg/utils/codec)、租户与用户查询
migrations/postgres oauth_clientoauth_access_token(或统一 oauth_token)、oauth_refresh_token、authorization code 表或 Redis
pkg/config issuer URL、access/refresh TTL、FRONTEND_LOGIN_URL / FRONTEND_CONSENT_URL、跨域 Cookie 与 CORS

依赖:OAuth 协议可自研或选用 Fosite / go-oauth2(需核对 opaque + 2.1 PKCE);JWT 库仅在 日后 OIDC id_token 时再引入。


建议代码落点(包结构)

与现有 internal/iam(租户/用户 CRUD并列,新增独立边界 internal/auth:作为各类认证方式的统一挂载点OAuth2、日后 SAML、LDAP、API Key 等),避免与 IAM 领域模型混包。首期 OAuth2 Code + PKCE 放在 internal/auth/oauth2/ 子树(Go 子包 oauth2),与「账号密码会话」等可并列拆分,互不污染。

路径 内容
internal/auth/oauth2/handler authorizetokenintrospect、可选 revocation
internal/auth/oauth2/service 授权码、PKCE、opaque 签发/校验、refresh 轮换
internal/auth/oauth2/repository oauth_client、token、authorization code 持久化
internal/auth/oauth2/entity OAuth 表对应模型(关联 iam_user,用户主数据仍在 IAM
internal/auth/session/(可选子包) JSON 登录、Cookie/Redis 会话(供 authorize 识别用户;命名避免与 net/http 概念混淆时可叫 websession
internal/auth/middlewareinternal/server Bearer 查库中间件(协议无关:只要是 opaque 就查存储);或放 internal/server/auth.go
internal/auth/http_register.go 聚合注册:实现 HttpRoutes,内部委托 oauth2 等子路由(与 internal/iam/http_register.go 同级注入)
internal/auth/wire_provider.go ProviderSet,在 cmd/server/wire.go 注入

命名说明:顶层用 auth 而非 oauth,便于扩展;HTTP 路径仍可保持标准 /oauth/authorize/oauth/token(协议规定,与包名无关)。

JSON 登录:建议 internal/auth/session(或 handler/login.goauth 根下),与 internal/auth/oauth2 并列;密码校验复用 internal/iam/service + pkg/utils/codec

配置pkg/config 增加 Auth 段(内含 OAuth2SessionCORS 等子段)。

迁移migrations/postgres 新文件如 005_oauth.sql(编号按仓库顺延)。

入口组装internal/server/http.goNewHttpRouteRegistrars 增加 authRoutes;全局 Bearer 中间件在 NewHttpEngine 或路由组上按需挂载。

纯工具PKCE SHA256、高熵随机串可放 pkg/security/token(或 pkg/authutil),避免 internal/auth 根包臃肿。


可选扩展(非首期必选)

  • OpenID Connect:在 OAuth2 之上增加 openid scope、/oauth/userinfoid_token 一般为 JWT(与 opaque access_token 并存不冲突)。
  • RFC 7662:独立资源服务时 RS 调 introspection;单体已用查表则可选暴露端点以保持对外契约一致。
  • RP-Initiated Logout:前后端分离时的统一登出。

实施阶段建议

  1. 数据模型 + 配置:客户端表、opaque token 表/索引(按 token 哈希查找)、TTL、前端回跳 URL、迁移脚本。
  2. JSON 登录 + 会话POST 登录接口、HttpOnly Cookie + Redis,供 authorize 识别用户;与前端约定 next/return_to 回跳完整 authorize URL。
  3. authorize + token + PKCE:浏览器串联「authorize → 302 前端登录 → 再 authorize」;端到端打通 Code 换 opaque Token。
  4. Bearer 中间件(查库/缓存):保护 /api/v1,上下文注入 user_id/tenant_id;可选 introspect 端点。
  5. Refresh 轮换与撤销、scope 与 IAM 权限对齐、集成测试(含跨域 Cookie 场景)。