第三方认证

第三方认证(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":"..."} 值,从而手动关联已有账户。

Wavelog 手册(中文版) / 第三方认证 0 字 0 行 bg8lng
2026-10-21T12:42:36.053982560Z 2026-10-21T12:43:41.759698637Z