18 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| OAuth2 PKCE 单体设计 | 在单体部署中同时承担 AS 与 RS:逻辑分层 + **opaque access token**(随机串)存 PostgreSQL/Redis,RS 中间件按 token 查元数据完成校验;可选暴露 RFC 7662 introspection 供未来独立资源服务。前后端分离下后端不渲染 HTML,仅 JSON 登录与 302;OAuth 2.1 + 强制 PKCE,与现有 IAM 对齐。 |
|
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)、租户、scope、exp,无需 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 为主流程;PKCE(
code_challenge/code_verifier,推荐 S256)对各类客户端(含机密客户端)可防范授权码注入,草案要求除例外情况外强制使用(见文档中 4.1 与 7.5 节相关段落)。 - Redirect URI 须严格字符串匹配(2.1 比 2.0 更严)。
- Implicit 等 grant 在 2.1 中不再规定,不要实现隐式授权。
- 生产环境 HTTPS、短期 access token、scope/audience 约束为最佳实践。
前后端分离约束(后端不返回 HTML)
本方案与「后端只提供 API」兼容:OAuth 规范里授权端点本来就是 HTTP 重定向 与浏览器导航,不要求授权服务器返回登录表单 HTML。
- 登录页、同意页:由前端 SPA 渲染(独立域名或
/login路由均可)。 - 后端提供:
POST /api/v1/.../login(或auth/login)等 JSON 接口校验iam_user凭据,成功后通过Set-Cookie(HttpOnly、Secure、SameSite按跨站需求选择)建立会话,响应体不写 HTML。 GET /oauth/authorize:仅返回 302(已登录则带code重定向到 client;未登录则重定向到配置的前端登录 URL),或 302 到错误 redirect_uri(OAuth 标准错误响应)。禁止用 HTML 表单作为唯一交互方式。
未登录时的典型串联(浏览器内):
- 用户浏览器访问 AS 的
/oauth/authorize?...(含 PKCE、state)。 - AS 发现无会话:302 到
FRONTEND_LOGIN_URL,并在 query 中携带「回到 authorize 所需信息」——常见两种做法(二选一或组合):- 回跳链接:
next/return_to=urlencode(完整 authorize URL),前端登录成功后执行location.href = decode(next)再次命中 authorize(此时带 Cookie)。 - 短期票据:
login_challenge/request_id指向 Redis 中暂存的 authorize 查询参数,前端登录成功后调用后端「完成登录并继续授权」或仍用回跳 authorize URL(实现简单优先前者需额外端点)。
- 回跳链接:
- 用户在 SPA 输入账号密码 →
POSTJSON 登录 → Cookie 写入。 - 浏览器再次
GET同一/oauth/authorize?...(PKCE 参数必须与首次一致,故回跳必须带齐原始 query),AS 有会话则签发code并 302 到第三方/首方应用的redirect_uri。
跨域 Cookie:若 API 与 SPA 不同站点,需 SameSite=None; Secure 且 CORS 对登录与 authorize 请求配置 credentials,并在配置中固定 Cookie 的 Domain/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_client(client_id、哈希后的client_secret(仅 confidential)、redirect_uris[]、grant_types、token_endpoint_auth_method)。 - 第三方:同样走表结构,由管理 API 维护(可后续迭代)。
OAuth 2.1 对 public client 不在 token 端点使用 secret;confidential client 使用 client_secret(或 private_key_jwt,可二期)。
2. 授权端点 GET /oauth/authorize
查询参数(RFC/2.1):response_type=code、client_id、redirect_uri、scope、state(强烈建议必填,防 CSRF)、code_challenge、code_challenge_method=S256。
服务端行为:
- 校验 client、redirect_uri、PKCE 参数完整且 method 支持。
- 若用户未登录:302 到配置项
FRONTEND_LOGIN_URL(或等价路径),并携带完整回跳到本端点所需的参数(见上文「前后端分离约束」:next/return_to或request_id)。不返回 HTML。登录成功依赖后续POSTJSON 登录 + HttpOnly Cookie + Redis 会话(与internal/server/http.go中间件顺序一致)。 - 已登录用户:若需同意且未静默授权:302 到
FRONTEND_CONSENT_URL(同样仅重定向,无 HTML);首方可配置静默同意跳过。 - 生成 authorization code(一次性、短 TTL,如 60–120s),绑定:
client_id、redirect_uri、code_challenge/method、资源所有者 id(即iam_user主键)、租户(tenant_id)、请求的scope。存 PostgreSQL 或 Redis(Redis 更适合极短 TTL)。 - 302 到
redirect_uri?code=...&state=...。
3. 令牌端点 POST /oauth/token
支持:
grant_type=authorization_code:code、redirect_uri、client_id、code_verifier(PKCE);confidential 客户端另加client_secret或 HTTP Basic。grant_type=refresh_token:refresh token 轮换(发新废旧,旧 token 入库标记撤销)推荐。
校验:
- 校验 code 未使用、未过期;
code_verifier与当时存的code_challenge按 S256 比对(与 docs/oauth-v2.md 一致)。 - 颁发 access_token(opaque,高熵随机串)与 refresh_token(opaque);存库字段建议:
token或仅存 哈希(防 DB 泄露即泄露明文)、user_id、tenant_id、client_id、scope、expires_at、revoked_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_id、tenant_id、scope、exp。 - 校验未过期、未撤销;按需校验
scope。 - 将
user_id、tenant_id、scope写入gin.Context,替代当前仅靠X-User-ID/X-Tenant-ID的占位方式;对内网遗留 header 仅过渡且需防伪造。
可选 POST /oauth/introspect(RFC 7662):与上述查表语义一致,供未来独立 RS 或统一审计;单体同进程内可直接复用同一套存储访问逻辑,不必强制每次 API 都走 HTTP 自省。
与 JWT 对比:本方案不暴露 /.well-known/jwks.json(除非日后为 id_token 引入 JWT);若将来要支持纯离线验签再另议。
5. Scope 与现有 IAM 的映射
- 最小集:
openid(若上 OIDC)、profile、email(按需,OIDC 层再扩展)。 - API 资源:
api或细分user.read、admin等,与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_client、oauth_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 |
authorize、token、introspect、可选 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/middleware 或 internal/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.go 于 auth 根下),与 internal/auth/oauth2 并列;密码校验复用 internal/iam/service + pkg/utils/codec。
配置:pkg/config 增加 Auth 段(内含 OAuth2、Session、CORS 等子段)。
迁移:migrations/postgres 新文件如 005_oauth.sql(编号按仓库顺延)。
入口组装:internal/server/http.go 的 NewHttpRouteRegistrars 增加 authRoutes;全局 Bearer 中间件在 NewHttpEngine 或路由组上按需挂载。
纯工具:PKCE SHA256、高熵随机串可放 pkg/security/token(或 pkg/authutil),避免 internal/auth 根包臃肿。
可选扩展(非首期必选)
- OpenID Connect:在 OAuth2 之上增加
openidscope、/oauth/userinfo;id_token一般为 JWT(与 opaque access_token 并存不冲突)。 - RFC 7662:独立资源服务时 RS 调 introspection;单体已用查表则可选暴露端点以保持对外契约一致。
- RP-Initiated Logout:前后端分离时的统一登出。
实施阶段建议
- 数据模型 + 配置:客户端表、opaque token 表/索引(按 token 哈希查找)、TTL、前端回跳 URL、迁移脚本。
- JSON 登录 + 会话:
POST登录接口、HttpOnly Cookie + Redis,供authorize识别用户;与前端约定next/return_to回跳完整 authorize URL。 - authorize + token + PKCE:浏览器串联「authorize → 302 前端登录 → 再 authorize」;端到端打通 Code 换 opaque Token。
- Bearer 中间件(查库/缓存):保护
/api/v1,上下文注入user_id/tenant_id;可选introspect端点。 - Refresh 轮换与撤销、scope 与 IAM 权限对齐、集成测试(含跨域 Cookie 场景)。