OIDC 服务端
为 Halo 提供 OpenID Connect 身份提供方能力,使用现有 Halo 账户登录第三方应用。管理员注册应用、按 Halo 角色控制应用访问,用户在授权前确认权限,并可以在个人中心撤销授权。
- 插件标识:
oidc-server - 项目/仓库:
halo-plugin-oidc-server - 作者:
ilay
适用场景与插件选择
本插件适合让独立 Web 服务、自建工具或支持标准 OIDC 的第三方应用使用 Halo 账户登录。Halo 作为 OIDC 身份提供方,应用通过 Discovery、授权码、ID Token 和 UserInfo 完成接入。
| 场景 | OIDC 服务端(本插件) | 统一身份认证 |
|---|---|---|
| 主要用途 | 为标准 OIDC 客户端提供身份服务,接入方可以不是 Halo | 多个 Halo 站点组成身份中心和接入站 |
| 接入方工作 | 使用应用自身的 OIDC 配置或客户端库,维护应用会话 | 在接入站安装插件,绑定或创建本地 Halo 用户并建立登录态 |
| 角色处理 | Halo 角色决定能否访问某个应用;不下发角色映射 | 支持中心角色到接入站本地角色的映射 |
| 典型选择 | 外部业务应用需要“使用 Halo 账户登录” | 多个 Halo 站点需要账号绑定、资料同步和角色映射 |
对照依据为「统一身份认证」项目当前 README,具体能力以对应版本文档为准。本插件不为第三方应用创建本地用户、映射本地角色或维护其登录会话。
功能
| 功能 | 说明 |
|---|---|
| 应用管理 | 创建、编辑、启用/停用、删除应用和重置客户端密钥 |
| 角色访问策略 | 允许所有有效登录用户,或要求拥有所选角色之一 |
| 用户确认 | 展示应用和所申请权限,用户同意或拒绝 |
| 授权管理 | 在个人中心查看和撤销自己的应用授权 |
| 授权码登录 | public 客户端必须使用 PKCE S256;confidential 客户端可选,使用时严格校验 |
| OIDC Discovery | 标准发现文档、JWKS、RS256 ID Token;应用列表提供实际端点说明 |
| 用户身份 | UserInfo 按已同意的 Scope 输出个人资料和邮箱 |
| 长期授权 | offline_access、刷新令牌轮换、重放检测与令牌族撤销 |
| 令牌管理 | RFC 7009 撤销和 confidential 客户端 Introspection |
界面预览
以下截图展示管理员应用管理、应用创建配置和用户授权确认页面。截图中的应用名称、回调地址和账号均为本地演示数据,实际内容以你的 Halo 实例为准。
应用管理

创建应用

用户授权确认

安装与配置
需要 Halo 2.26.0 或更高版本,Halo 使用 Java 21。通过 Halo Console 的「插件」上传构建后的 JAR,启用后刷新 Console 和个人中心。
在 Halo 中配置正确的外部访问地址,例如 https://halo.example.com。OIDC issuer 为该地址加 /oidc:https://halo.example.com/oidc。issuer 使用管理员配置的固定地址,不使用请求中的 Host 推导。生产环境使用 HTTPS;HTTP 仅支持本机环回地址用于开发。
打开插件详情 →「设置」→「服务开关」,开启「启用 OIDC 服务」后协议端点才会接受请求;同一页面还可以配置令牌有效期。启用协议前,确认 Halo 外部地址与第三方应用配置一致。
管理员在「OIDC 服务端」页面创建应用。超级管理员具有默认权限;其他管理员需在 Halo 角色中授予「OIDC 应用管理」权限。
创建应用
- 填写应用名称和说明,选择客户端类型。默认 confidential,适合能安全保管密钥的服务器应用;SPA、移动端等无法保管密钥的应用选择 public。客户端类型创建后不可修改。
- 填写完整的回调 URI,每行一个。回调必须逐字匹配,包括协议、主机、端口、路径和查询字符串;不支持通配符和 fragment。
- 选择应用允许请求的权限。应用登录必须包含
openid;可选profile、email和offline_access。 - 明确选择访问策略。指定角色时至少选择一个角色,用户拥有其中任意一个即可;管理员身份也需满足策略。
- 创建成功后自动展示接入信息,并提供 Client ID 和 Client Secret 的复制按钮。confidential 应用的 Client Secret 仅在创建或重置后显示一次,及时保存至应用的秘密配置;public 应用使用 Client ID 与 PKCE,不生成密钥。
应用列表展示客户端类型、访问策略、状态、Client ID 和回调地址,可复制 Client ID 或点击「接入信息」查看配置。启用/停用、重置密钥和删除在每行的更多操作中。再次查看 confidential 应用时不会返回原密钥;密钥丢失时使用「重置密钥」生成新的凭据。
角色控制的是应用访问资格。满足角色条件的用户仍需同意授权;同意授权不会赋予用户新的 Halo 角色,也不会向第三方应用授予 Halo 管理 API 权限。
停用应用后旧授权码和令牌失效,重新启用不会恢复这些令牌。删除应用或重置密钥后,第三方需要重新进行授权。
用户授权与撤销
第三方应用将浏览器跳转到授权端点。未登录时使用 Halo 原生登录,兼容已有的 Passkey、两步验证等登录方式;插件不接收或存储用户密码。
授权确认页说明应用、当前账户、回调主机及请求的身份数据。点击「同意并继续」后跳转回应用并返回授权码;点击「拒绝」也会返回应用,携带 error=access_denied 和原始 state。以后请求已经同意的相同权限时可以复用授权,prompt=consent 强制再次确认。
用户进入个人中心的「个人资料 → 应用授权」,可以撤销任意应用授权。撤销后相关授权码、访问令牌和刷新令牌无法继续在插件中使用。应用访问和令牌刷新会重新检查账户状态、应用策略与当前角色,移除角色后不能继续获取身份或刷新令牌。
**撤销范围:**第三方已经保存的 ID Token 可在到期前离线验证,第三方已有会话由该应用管理。撤销授权不会自动退出第三方应用,首版不提供 back-channel logout 或 RP-Initiated Logout。
用户删除与同名账户
授权记录按用户名及绑定账户创建时间的 subject 校验归属。删除后重新创建的同名用户拥有新的身份,不能查看、撤销或复用旧账户的授权,旧的授权确认页面也不能继续提交。
插件运行时,用户删除流程通过 Finalizer 清理其授权记录(包括已撤销记录)、授权码、访问令牌、刷新令牌与令牌族。确认这些记录已被 Halo 实际回收后才解除删除保护,随后用户名才能重新用于创建账户。清理失败会重试,期间旧账户已不能继续访问 OIDC。
停用或卸载插件时释放本插件的用户 Finalizer,避免停止的控制器阻塞 Halo 用户删除;应用配置和授权数据按 Halo 插件数据机制保留。再次启用时会清理无主、已删除账户及身份不匹配的历史授权。即使后台清理尚未完成,新账户也无法读取或修改旧授权;既有账户的合法授权可继续使用。
OIDC 接入
推荐第三方使用成熟 OIDC 客户端库。设置 issuer https://halo.example.com/oidc,通过 Discovery 读取真实端点,不自行拼接认证请求或忽略 ID Token 校验。
| 端点 | 方法 | 用途 |
|---|---|---|
Issuer URL:https://halo.example.com/oidc |
— | OIDC 签发者标识,第三方客户端配置此地址 |
/oidc/.well-known/openid-configuration |
GET | OIDC Discovery |
/.well-known/openid-configuration |
GET | 同一 issuer 的站点根发现入口 |
/oidc/jwks |
GET | 签名公钥 |
/oidc/authorize |
GET / POST | 授权与同意确认 |
/oidc/token |
POST | 授权码换取令牌、刷新令牌 |
/oidc/userinfo |
GET / POST | 访问令牌获取身份 |
/oidc/revoke |
POST | 撤销令牌 |
/oidc/introspect |
POST | confidential 应用查询自身令牌状态 |
Scope
| Scope | 身份数据或能力 |
|---|---|
openid |
必需,稳定的 sub 身份标识和 ID Token |
profile |
显示名、用户名、可用头像 |
email |
邮箱与邮箱验证状态 |
offline_access |
显式同意后获得刷新令牌 |
授权码与 PKCE 示例
以下以服务器应用为例,回调 URI https://app.example.com/callback 已在 Halo 登记。每次登录生成新的随机 state、nonce 和 code_verifier,在应用会话中保存。
import { randomBytes, createHash } from 'node:crypto'
const verifier = randomBytes(32).toString('base64url')
const challenge = createHash('sha256').update(verifier).digest('base64url')
const state = randomBytes(32).toString('base64url')
const nonce = randomBytes(32).toString('base64url')
const authorizationUrl = new URL('https://halo.example.com/oidc/authorize')
authorizationUrl.search = new URLSearchParams({
client_id: process.env.OIDC_CLIENT_ID,
response_type: 'code',
redirect_uri: 'https://app.example.com/callback',
scope: 'openid profile email',
state,
nonce,
code_challenge: challenge,
code_challenge_method: 'S256',
}).toString()
// 将 state、nonce、verifier 保存至用户会话,随后跳转 authorizationUrl。
回调收到 code 前先检查 state 与会话值一致,拒绝错误响应或不匹配值。令牌请求使用 application/x-www-form-urlencoded:
const tokenResponse = await fetch('https://halo.example.com/oidc/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.OIDC_CLIENT_ID,
client_secret: process.env.OIDC_CLIENT_SECRET,
code: callbackCode,
redirect_uri: 'https://app.example.com/callback',
code_verifier: verifier,
}),
})
if (!tokenResponse.ok) throw new Error('OIDC 令牌兑换失败')
const tokens = await tokenResponse.json()
public 客户端省略 client_secret,仍必须使用 PKCE S256。confidential 客户端可以省略 PKCE,也可以使用 PKCE;如果授权请求带有 PKCE,换令牌时必须提交匹配的 code_verifier,不能降级为无 PKCE。confidential 客户端可改用 client_secret_basic;一次请求只使用一种客户端认证方式。
通过 OIDC 库和 JWKS 验证 ID Token 签名、iss、aud、exp、nonce 和 at_hash。随后使用访问令牌获取身份:
const response = await fetch('https://halo.example.com/oidc/userinfo', {
headers: { Authorization: `Bearer ${tokens.access_token}` },
})
if (!response.ok) throw new Error('OIDC 身份查询失败')
const identity = await response.json()
// identity.sub 必须与已验证的 ID Token 的 sub 相同。
需要刷新令牌时,应用登记并申请 offline_access,用户明确同意。刷新请求使用 grant_type=refresh_token、最新 refresh_token 和客户端认证信息。每次成功刷新后立即替换旧刷新令牌;旧值重放会撤销同一令牌族,不适合并行刷新。
支持范围
支持 response_type=code、response_mode=query、authorization_code / refresh_token、public 客户端必需的 PKCE S256、confidential 客户端可选的 PKCE S256、prompt=consent / none。不支持隐式、密码、客户端凭证、设备授权、动态客户端注册和跨应用退出。
应用仅使用 opaque 访问令牌调用插件 UserInfo / Introspection;它不能用于认证 Halo API。协议声明以 Discovery 为准,不声明未实现的认证能力。
完整示例:独立 Node.js 应用
仓库提供可运行的 OIDC 联调应用,它是独立 HTTP 服务,通过标准 OIDC 端点与 Halo 交互,不依赖 Halo 插件 API 或前端组件。需要 Node.js 22.12+,无需安装 npm 依赖。
- 在本机 Halo 中启用插件和「启用 OIDC 服务」,外部访问地址设为
http://127.0.0.1:8092。 - 创建一个机密客户端,回调地址填写
http://127.0.0.1:8765/callback,允许openid、profile、email、offline_access,选择测试账户可访问的角色策略。保存创建后显示的 Client ID 和 Client Secret。 - 在插件仓库根目录执行以下命令,将配置放到已被 Git 忽略的
workplace目录:
New-Item -ItemType Directory -Force "workplace" | Out-Null
Copy-Item "scripts/oidc-demo.example.json" "workplace/oidc-demo.json"
macOS / Linux 可使用 mkdir -p workplace 和 cp scripts/oidc-demo.example.json workplace/oidc-demo.json。编辑 workplace/oidc-demo.json,把 clientId、clientSecret 替换为刚创建的凭据,issuer 使用应用列表端点说明中的完整 Issuer URL(此例为 http://127.0.0.1:8092/oidc)。不要提交这个含密钥的本地文件。
node "scripts/oidc-demo.mjs" "workplace/oidc-demo.json"
- 打开
http://127.0.0.1:8765,点击「通过 Halo 登录」,登录并同意授权。浏览器回到示例应用后,应显示身份信息和校验成功提示。 - 点击「查询用户身份」验证 UserInfo;点击「刷新令牌」验证轮换。然后在 Halo 个人中心撤销该应用授权,再次查询身份或刷新都应被拒绝。
- 重新授权后,先刷新一次,再点击「重放旧刷新令牌」,应收到拒绝响应,随后该令牌族的 UserInfo 和刷新请求也应失败。
示例依次完成 Discovery 校验、state / nonce / PKCE 生成、授权跳转、回调和令牌交换、JWKS / RS256 签名验证、iss / aud / 时效 / nonce / at_hash 校验,以及 UserInfo 的 sub 一致性验证。机密凭据和令牌保存在服务端,不展示在页面中。公开客户端可省略 clientSecret,仍使用 PKCE。
该脚本仅用于本机演示和回归,内存会话在进程退出时丢失,只允许环回地址的 Halo。生产接入应使用成熟 OIDC 客户端库、HTTPS 和持久化会话管理;第三方本地账号关联请使用 issuer + sub,不要使用用户名或邮箱作为唯一身份。
开发与构建
目录结构
后端按职责划分 Java 包,根包仅保留插件入口;测试按对应职责组织,共享测试数据和存储模拟放在 support 包。
src/main/java/top/ilay/oidcserver/
├── OidcServerPlugin.java # 插件生命周期、扩展模型与索引注册
├── config/ # Spring 配置与插件设置读取
├── extension/ # Halo 持久化扩展模型与授权绑定
├── dto/ # 管理 API 请求、响应与分页模型
├── service/ # 应用、授权、身份、签名与令牌服务
├── web/ # 管理和用户 API、授权控制器与页面
├── protocol/ # OAuth/OIDC 参数校验、错误与协议过滤器
├── reconciler/ # 用户删除、无主授权和过期令牌状态清理
└── util/ # 加密与扩展资源状态工具
src/test/java/top/ilay/oidcserver/
├── OidcServerPluginTest.java
├── service/ # 业务服务与安全回归测试
├── web/ # 授权页面、跳转与权限提示测试
├── protocol/ # 协议过滤器测试
├── reconciler/ # 用户删除、同名重建和清理生命周期回归
└── support/ # 共享测试环境与存储模拟
src/main/resources/ # 插件清单、设置、角色模板与 Logo
ui/src/ # Console 和个人中心界面
docs/screenshots/ # README 界面截图
| 工具 | 要求 |
|---|---|
| Halo | ≥2.26.0 |
| JDK | 21,包含 javac;JRE 只能运行,不能编译 |
| Node.js | ≥22.12.0 |
| pnpm | 10.33.0,Gradle 可在工程内下载,无需全局安装 |
Windows 使用完整 JDK:
./build-local.ps1 -JdkPath "D:/jdk/jdk-21.0.12.15" -GradleArguments @("build", "--console=plain")
macOS / Linux:
./gradlew build
产物位于 build/libs。构建包含后端测试、UI 类型/格式/lint 检查及完整 UI 资源打包。
本地开发可使用已有 Halo 实例;自动启动开发容器使用 ./gradlew haloServer。只有需要独立实例时执行该命令,避免与已有端口冲突。
前端 API 来自后端的 Springdoc/OpenAPI,运行实例提供对应分组后执行:
./build-local.ps1 -GradleArguments @("generateApiClient", "-x", "build", "--console=plain")
生成目录 ui/src/api/generated 为构建产物,不手工修改。普通配置使用 Halo Setting,私钥与客户端秘密验证值使用 Halo Secret,访问/刷新令牌只保存摘要。
反馈与许可证
在 Issues 反馈问题,提供 Halo/插件版本、复现流程和不含秘密的错误信息。不要附带 Client Secret、令牌、私钥或登录密码。
GPL-3.0 © ilay
