第三方认证
第三方认证(Third-Party Authentication)
目录
Wavelog 支持通过第三方身份提供方(IdP)并使用 OpenID Connect(OIDC)协议进行认证。Wavelog 无需自行管理用户名和密码,而是把认证交给位于其前方的可信反向代理。反向代理向 IdP 验证用户身份,并通过 HTTP 请求头把签名后的 JWT 访问令牌转发给 Wavelog。
信息(Wavelog 支持范围)
虽然 Wavelog 支持 OIDC,并在本文档中就 SSO 及其配置给出了一些指导,但我们并不直接为你提供身份提供方方面的支持。因此,如果你不知道如何搭建 Keycloak、如何在 Authentik 中添加双因素认证,或者互联网是如何运作的,很遗憾我们无法在这些方面帮助你。你可以在相关主题的优秀论坛和支持社区中找到帮助。感谢你的理解。
工作原理
sequenceDiagram
participant B as Browser
participant P as Reverse Proxy<br/>(OAuth2 Proxy / mod_auth_oidc)
participant I as Identity Provider<br/>(Keycloak / Authentik / ...)
participant W as Wavelog
B->>P: Request
P->>W: Request Forwarded
W-->>B: Redirect to Login Page
critical Authentication
B->>P: Visit SSO Authentication Endpoint
P-->>B: Redirect to IdP
activate B
B->>I: OIDC Client Request
deactivate B
I-->>I: Auth with Credentials (PW, MFA, etc)
I-->>B: Redirect to OAuth2 Proxy Endpoint
activate B
B->>P: Pass JWT to OAuth2 Proxy Endpoint
deactivate B
end
P->>W: Request + X-Forwarded-Access-Token header
W->>W: Verify JWT, extract claims
W-->>B: Redirect to Dashboard
Wavelog 会从配置的请求头中读取 JWT,对其进行验证(可选地对照 IdP 的 JWKS 端点),再从声明中提取用户身份并完成登录。首次登录时,可以自动创建一个本地 Wavelog 账户。
前提条件
- 一个通过 HTTP 请求头把 JWT 访问令牌转发给 Wavelog 的反向代理(例如 OAuth2 Proxy、Apache
mod_auth_oidc) - 一个支持 OIDC 的身份提供方(例如 Keycloak、Authentik、Zitadel)
- IdP 必须在 JWT 中至少包含 username、email 和一个 callsign 声明
代理范围
警告:重要
只有 SSO 登录端点
/index.php/header_auth/login需要由反向代理保护。Wavelog 的其他所有端点(应用本身、API、登录页面)必须保持可直接访问——否则部分功能、API 集成以及 SSO 跳转回 Wavelog 都会失效。
请把反向代理配置为仅对该单个路径要求认证,其余请求全部以未认证状态透传。其他一切都由 Wavelog 管理。
快速配置
第 1 步: 在 application/config/config.php 中启用 SSO:
<?php
$config['auth_header_enable'] = true;
第 2 步: 把 sso.sample.php 复制到同一目录下的 sso.php,并调整其中的值:
cp application/config/sso.sample.php application/config/sso.php
第 3 步: 配置你的身份提供方和反向代理。请参阅针对具体提供方的指南:
提示:已经在运行 Apache2?
如果 Apache2 是你的 Web 服务器,请使用
mod_auth_openidc而不是 OAuth2 Proxy。它原生处理完整的 OIDC 流程——无需额外的容器或代理。这是 Apache2 环境下的推荐做法。
关于其他方案的说明
通过 Google、GitHub 或类似公共 IdP 登录
把 Google、GitHub 或类似的公共身份提供方直接作为 Wavelog 的 IdP 在技术上可行——oauth2-proxy 对两者都有内置支持——但存在一个根本性限制:这些提供方不支持在其令牌中包含自定义用户属性。Wavelog 要求 JWT 中存在呼号声明,而普通的 Google 或 GitHub 账户没有标准方式来携带这一信息。
推荐的做法是把 Google 或 GitHub 作为 Authentik 或 Keycloak 中的社交登录来源。用户通过 Google/GitHub 完成认证,你的 IdP 用你管理的呼号属性丰富令牌,Wavelog 则收到一个包含全部所需声明的合规 JWT。这样你既能享受社交登录的便利,又不必放弃对令牌内容的控制。
配置参考
所有 SSO 设置都位于 application/config/sso.php。仅当 config.php 中的 auth_header_enable 为 true 时,才会加载该文件。
各选项的详细说明请参见 sso.sample.php 或你复制出的 sso.php 中的内嵌注释。
JWT 签名验证
Wavelog 支持两种 JWT 验证模式:
JWKS 模式(强烈推荐)
配置了 auth_header_jwks_uri 后,Wavelog 会获取 IdP 的公钥,并使用 Firebase JWT 库以密码学方式验证 JWT 签名。该模式还会校验 exp(过期时间)、nbf(生效时间)以及签名算法。
低安全性模式
当 auth_header_jwks_uri 为空时,JWT 载荷会在不验证签名的情况下被解码。Wavelog 仍会检查 exp、nbf、iat、typ,以及算法是否属于常见算法列表(不允许 none)。
警告
低安全性模式无法以密码学方式保证令牌是由你的 IdP 签发的。只有在你完全信任反向代理、且转发的请求头无法被伪造时才可使用(例如代理在转发自己的请求头之前会剥离所有同名的传入请求头)。如果你不理解这句话的含义,请使用 JWKS 模式,并确保反向代理配置正确。
声明映射
必需的声明
auth_headers_claim_config 选项把 Wavelog 用户字段映射到 JWT 声明名称。其键是 users 数据库表中的列名。你基本上可以把任意用户字段映射到 JWT 声明,但以下字段是自动创建账户所必需的:
user_name(对应包含用户名的 JWT 声明)user_email(对应包含电子邮件地址的 JWT 声明)user_callsign(对应包含呼号的 JWT 声明)
不允许覆盖的字段
id(Wavelog 中的用户 ID)external_account(存储 IdP 签发方和主体的 JSON 字段)password(当auth_header_allow_direct_login为true时,SSO 用户仍可保留本地密码用于直接登录,但该字段无法从 JWT 映射)user_type(无法通过 SSO 创建管理员账户,因此 SSO 用户的该字段始终设为 "operator")
<?php
$config['auth_headers_claim_config'] = [
'user_name' => [
'claim' => 'preferred_username',
'override_on_update' => true,
'allow_manual_change' => false,
],
'user_email' => [
'claim' => 'email',
'override_on_update' => true,
'allow_manual_change' => false,
],
'user_callsign' => [
'claim' => 'callsign',
'override_on_update' => true,
'allow_manual_change' => false,
],
// ... additional fields
];
各字段的选项
| 选项 | 说明 |
|---|---|
claim |
从中读取值的 JWT 声明名称 |
override_on_update |
若为 true,每次登录都会用 JWT 更新该字段。若为 false,则仅在创建账户时写入一次值 |
allow_manual_change |
若为 true,用户可以在自己的 Wavelog 资料中编辑该字段。若为 false,该字段为只读,并显示一个 IdP 徽标 |
自定义字段
你可以映射 users 表中的任意其他列。未列出的字段在创建账户时使用其默认值,并在后续登录时不会被更新。
集体台用户组映射
在创建用户时或登录过程中,可以把用户作为集体台成员映射到已有的集体台。这需要 $config['special_callsign'] = true;。
$config['auth_header_clubstation_claim'] 应设置为提供多值用户组属性(RFC7643 4.1.2)的 JWT 声明。按 RFC9068 2.2.3.1,常见的声明是 "groups"。
对于 $config['auth_header_clubstation_direct'],每个键是一个集体台 ID。其嵌套键为:
- group:IdP 中定义的用户组名称
- update_on_login:是否在每次登录时更新用户的成员关系。若为 false,则仅在创建用户时分配。(推荐值为 true)
$config['auth_header_clubstation_direct'] = [
9 => [
'group' => 'wl_society_station_group',
'update_on_login' => true
],
15 => [
'group' => 'wl_special_event_station',
'update_on_login' => true
],
];
对于 Keycloak,groups 声明默认不会提供,必须创建一个自定义客户端作用域。相关说明请参见 Keycloak 服务器管理指南。
该作用域需要一个断言映射,然后在 mappers 下从 Group Membership 添加一个新的映射器。
Authentik 默认会提供 groups 声明。
用户识别
Wavelog 通过由 JWT 的 iss(签发方 URL)和 sub(主体)声明组成的复合键来识别 SSO 用户,该键以 JSON 形式存储在 external_account 数据库列中:
{"iss":"https://auth.example.org/realms/example","sub":"c267892a-2815-4ee7-85ad-c1257ade2b65"}
该组合可以在 IdP 与用户之间唯一确定某个用户,且不受用户名或电子邮件变更的影响。我们决定把该信息以纯 JSON 形式存储在 external_account 列中,以便管理员通过手动修改数据库把 IdP 迁移到新 URL,或把已有账户关联到某个 SSO 账户。详情请见下一节。
IdP URL 变更后的迁移
如果你的身份提供方 URL 发生变化(例如把 Keycloak 迁移到新域名),则所有现有 external_account 条目中的 iss 值都必须更新。sub 值(Keycloak 中的用户 UUID)保持不变。
请在 Wavelog 数据库上运行以下 SQL,就地修改所有现有账户的签发方 URL:
UPDATE users
SET external_account = JSON_SET(
external_account,
'$.iss',
'https://auth.new-domain.org/realms/example'
)
WHERE external_account IS NOT NULL;
如果你有多个 IdP 或 realm,可以在 WHERE 子句中追加一个条件,只更新受影响的账户:
WHERE JSON_VALUE(external_account, '$.iss') = 'https://auth.old-domain.org/realms/example'
注意
在执行
UPDATE之前,请先用SELECT验证替换结果。
用户资料行为
用户资料中密码字段的可见性取决于 SSO 配置:
auth_header_enable |
allow_direct_login |
是否有 SSO 账户 | hide_password_field |
密码字段是否可见? |
|---|---|---|---|---|
false |
— | — | — | 总是 |
true |
false |
— | — | 从不 |
true |
true |
否 | — | 是 |
true |
true |
是 | false |
是 |
true |
true |
是 | true |
否 |
在声明映射中设置为 allow_manual_change: false 的字段,会在用户资料中显示为只读,并带有一个 IdP 徽标,表示它们由身份提供方管理。
故障排查
启用调试日志
在 sso.php 和 config.php 中设置以下内容:
<?php
// sso.php
$config['auth_header_debug_jwt'] = true;
// config.php
$config['log_threshold'] = 2; // debug
下次尝试登录时,原始 JWT 和解码后的声明会写入 application/logs/log-YYYY-MM-DD.php。可用它来确认转发的是正确的声明,并为 auth_headers_claim_config 找到准确的声明名称。
警告
排查完成后请记得关闭
auth_header_debug_jwt。
常见问题
登录页面不显示 SSO 按钮
: 检查 config.php 中 auth_header_enable = true,并确认 application/config/ 下存在 sso.php。
出现「用户未找到」错误
: JWT 中不包含 iss 或 sub 声明,或者令牌验证失败。请启用调试日志以检查令牌。
用户已创建但呼号 / 邮箱有误
: auth_headers_claim_config 中的声明名称与 JWT 中的实际声明不匹配。请启用调试日志以查看所有可用的声明。
令牌验证失败
: 检查 auth_header_jwks_uri 是否指向你 IdP 的正确 JWKS 端点,以及 Wavelog 服务器能否通过网络访问它。
登录时出现邮箱已存在错误
: 已存在使用相同电子邮件地址的本地 Wavelog 账户。请把数据库中该账户的 external_account 列设置为正确的 {"iss":"...","sub":"..."} 值,从而手动关联已有账户。