OIDC / OAuth2.0 登录对接部署指南¶
本指南说明如何将 Shortener 对接任意标准 OIDC / OAuth2.0 身份提供方(IdP), 实现单点登录(SSO)。配套配置项见 配置指南 · OIDC 配置, 设计决策见 ADR 0001。
概述¶
Shortener 支持两条并存的登录通道:
- OIDC 通道:对接外部 IdP(Keycloak、Authelia、Okta、Entra ID、Google 等),走标准授权码流。
- 密码通道:配置文件中的单管理员账号(Argon2id 哈希口令)。
两条通道均签发无状态 JWT(HS256),前端统一以 Bearer 令牌使用,互不影响。
OIDC 登录采用单身份 + 白名单模型:
- 本服务没有账号系统 / 用户表,用户身份完全来自 IdP,不落库。
- 仅
allow_emails/allow_subjects白名单内的 IdP 用户可登录(任一命中即放行)。 - 启用 OIDC(
enabled = true)时,两项白名单至少配置一项,不能都为空(启动校验会拒绝)。
前置条件¶
- 一个可用的 OIDC IdP,且你能在其上创建 OAuth2 / OIDC 客户端。
- 已部署 Shortener 服务(服务端 v0.2.0+,包含 OIDC 与 JWT 支持)。
- 用于签发 JWT 的
JWT_SECRET(所有实例必须一致)。
步骤一:准备 JWT 签名密钥¶
两条登录通道签发的 JWT 都用 JWT_SECRET 签名。请在所有实例使用同一个值,否则多实例间令牌互不相认。
也可以把密钥写入文件、再用 JWT_SECRET_FILE 指向它,便于对接 systemd LoadCredential、Docker/K8s Secret 等以文件形式挂载密钥的机制:
openssl rand -base64 48 > /etc/shortener/jwt_secret
chmod 600 /etc/shortener/jwt_secret
export JWT_SECRET_FILE=/etc/shortener/jwt_secret
两者同时设置时以 JWT_SECRET 为准;文件内容末尾的换行会被自动去掉。
建议通过环境变量 / Secret 管理注入,不要硬编码进配置文件。
步骤二:在 IdP 侧创建客户端¶
以通用 OIDC IdP 为例:
- 新建一个 OAuth2 / OIDC 客户端(应用类型选「Web / 公开或机密均可」)。
- 授权方式选择 Authorization Code(授权码流),
response_type=code。 - 配置 Redirect URI / Callback URL,填写本服务的回调地址:
- 申请 Scope:
openid、profile、email。 - 若 IdP 要求客户端密钥(confidential client),记下
client_secret。
如果使用 Keycloak:在 Realm 下创建 Client,
Valid Redirect URIs填上面的回调地址,Web Origins填前端域名;在 Clients 的 Credentials 标签页获取 Secret。
步骤三:配置 Shortener¶
编辑配置文件(默认 config.toml),新增 [oidc] 段:
[oidc]
# OIDC 登录总开关(false 时登录不可用)
enabled = true
# IdP 的 issuer。discovery 文档位于 <issuer>/.well-known/openid-configuration
# enabled 时必填
issuer = "https://keycloak.example.com/realms/shortener"
# 步骤二中创建的客户端 ID
client_id = "shortener-app"
# 客户端密钥。强烈建议改用环境变量 OIDC__CLIENT_SECRET 注入,勿写入文件
client_secret = ""
# 回调地址无需配置:服务根据请求的 Host 头自动推导为 https://<域名>/api/oidc/callback。
# 只需确保 IdP 中登记的 Redirect URI 与该地址完全一致
# 白名单:email 或 sub 任一命中即放行(至少配置一项,不能都为空)
allow_emails = ["admin@example.com"]
allow_subjects = []
环境变量覆盖(敏感项优先):
enabled 为 OIDC 登录的总开关:默认 false(不启用);设为 true 并配置 issuer / client_id / 至少一项白名单后启用 OIDC 登录。
步骤四:准备密码通道(可选但推荐保留)¶
即使启用了 OIDC,也建议保留配置中的管理员账号,用于脚本 / 无 IdP 环境的运维登录。 口令必须以 Argon2id 哈希存储,生成方式:
# 服务端自带子命令(推荐)
shortener-server hash-password --password "your-secure-password"
# 或使用 CLI
shortener-cli hash-password --password "your-secure-password"
# 交互式(不在 shell 历史留痕)
shortener-server hash-password
将输出的整行($argon2id$...)填入配置:
步骤五:启动并验证¶
启动服务(确保已注入 JWT_SECRET):
手动验证 OIDC 流程:
# 1) 触发登录,应 303 重定向到 IdP 授权页
curl -s -i "https://shortener.example.com/api/oidc/login" | grep -i "^location:"
# 2) 用浏览器打开上面的 location 完成 IdP 登录,
# IdP 会回跳到 /api/oidc/callback?code=...&state=...
# 回调成功后会 302 到前端并附带 ?token=<jwt>
前端登录页已内置「使用 OIDC 登录」按钮,点击即发起上述流程;
回调后页面自动从 URL 提取 token 并写入 localStorage。
email / name 来自 id_token 或 userinfo:服务优先读取 id_token 中的 claims; 若 id_token 不含
sub, 其余 claims 需通过 userinfo 端点获取),服务会自动调用 userinfo 端点补充获取name后再进行白名单匹配。userinfo 失败时降级回退 id_token claims。
白名单行为说明¶
| 场景 | 结果 |
|---|---|
用户 email 在 allow_emails 中 |
放行,签发 JWT |
用户 sub 在 allow_subjects 中 |
放行,签发 JWT |
| 均不在白名单 | 403 Forbidden:User is not in the OIDC allowlist |
allow_emails 与 allow_subjects 均为空 |
配置校验失败,服务无法启动(enabled 时必须至少配置一项) |
增删白名单用户需修改配置并重启服务。
多实例 / 容器部署¶
- JWT 为无状态 HS256,只要所有实例共享同一
JWT_SECRET,即可横向扩展、共享校验。 client_secret与JWT_SECRET建议通过容器 Secret / 环境变量注入,而非写进镜像或配置文件。 以文件形式挂载的 Secret(Docker / K8s)可用JWT_SECRET_FILE指向挂载路径。- 配合反向代理(Nginx / Caddy)时,需将
https://<域名>/api/oidc/callback正确转发到本服务。
排错¶
- 未启用 OIDC(
enabled未设或为false):访问/api/oidc/login会返回 404("OIDC login is not enabled")。 issuer配置错误 / 网络不通:enabled已设为true但首次访问/api/oidc/login仍返回 500, 检查issuer是否可从本服务访问、.well-known/openid-configuration是否可达。- 回调 404 / 不匹配:确认 IdP 登记的 Redirect URI 与本服务自动推导的回调地址(
https://<域名>/api/oidc/callback)完全一致(含协议、域名、路径)。 - 登录后 403:说明用户不在白名单,检查
allow_emails/allow_subjects。 - 白名单匹配用的是 userinfo 补充后的
email(若 id_token 缺失):先在 IdP 侧确认该用户的 email 地址, 再与allow_emails中的条目逐一比对(大小写不敏感)。 - Authelia 等 IdP 默认不会把
email放进 id_token,因此即使你在 Authelia 里配置了邮箱, 也必须确保 IdP 的 userinfo 端点能返回email(通常由 IdP 管理端控制,无需额外配置)。 - JWT 校验失败(多实例):确认各实例
JWT_SECRET一致。 - 启动报
JWT_SECRET or JWT_SECRET_FILE environment variable is not set:JWT_SECRET与JWT_SECRET_FILE均未注入,请设置其一后重试。 - 启动报
Failed to read JWT secret file ...:JWT_SECRET_FILE指向的文件不存在或当前用户无读取权限,检查路径与权限。
安全建议¶
- 生产环境必须设置
allow_emails/allow_subjects(至少一项),服务在enabled且两项均为空时会拒绝启动。 client_secret与JWT_SECRET通过环境变量 / Secret 管理,避免入库与进版本控制。- 配置中的
admin.password_hash与oidc.client_secret均属于敏感信息,勿提交到代码仓库。