单点登录

单点登录(SSO)让用户用他们在你的身份提供商(IdP,例如 Keycloak、authentik 或 Microsoft Entra ID)已有的账户登录 Specivo。

本页说明它的工作原理,以及每项设置的作用。具体某个提供商的配置步骤,请看对应页面:

如果希望由 IdP 在用户登录之前就创建、更新和停用账户与用户组,请为提供商添加 SCIM 预配。

Specivo 支持什么

  • 仅支持 OpenID Connect。 Specivo 是 OpenID Connect 的依赖方(relying party):它向你的 IdP 询问用户是谁。它本身永远不是 IdP,也不会向其他应用签发身份。 不支持 SAML。如果你的 IdP 只支持 SAML,请在前面再放一个能把 SAML 桥接到 OpenID Connect 的 IdP(Keycloak 和 authentik 都可以)。
  • 可同时配置多个提供商。 每个提供商在登录页面上各有一个按钮。
  • 本地账户继续保留。 除非你隐藏或限制密码登录,否则密码仍然可用,而且你随时都能以管理员身份重新进入。参见 重新获得访问权限。
  • 仅限真人。 服务账户(AI 智能体使用的账户)永远不能通过 SSO 登录。API 密钥不受 SSO 影响。
  • 可选的 SCIM 预配。 不启用时,用户在首次登录时才出现,离开则要经过 人员离职 中的步骤。启用 SCIM 后,IdP 会提前创建用户,并在需要时立即停用。不支持 LDAP。

开始之前

  1. 在公开地址上通过 HTTPS 提供 Specivo。 大多数 IdP 拒绝纯 http:// 的重定向地址,Specivo 也拒绝 http:// 的 IdP,除非你明确允许(见 环境变量设置)。
  2. 把 APP_URL 设为该公开地址,例如在 .env 中写 APP_URL=https://specivo.example.com,然后重建容器(docker compose up -d)。Specivo 交给 IdP 的每个地址都由 APP_URL 构造,绝不会从请求中获取,所以在反向代理后面也无需再改别的。先做这一步:APP_URL 配错是首次登录失败最常见的原因。
  3. 不要让代理的访问日志记录查询字符串。 从 IdP 跳回来的重定向在查询字符串中带有一次性的 code 和 state。自带的 nginx 只记录不含查询字符串的路径。如果前面还有其他代理(Caddy、Traefik、Cloudflare、负载均衡器),也要把它的访问日志配置为不记录查询字符串,至少对 /auth/oidc/ 要这样做。关于此类代理后面按地址限流的问题,参见 代理后面的客户端地址。
  4. 确保你能用密码以管理员身份登录。 测试期间保留这个账户。

配置提供商:总体步骤

所有 IdP 的步骤都一样,只是界面不同。

  1. 在 Specivo 中打开 管理 → 单点登录,点击 + 新建提供商。选一个 标识串(小写字母、数字和 -,2 到 40 个字符),例如 keycloak。连接 卡片会显示由它构造出的两个地址:

    地址 格式
    重定向 URI {APP_URL}/auth/oidc/{slug}/callback/
    后端通道注销 URI {APP_URL}/auth/oidc/{slug}/backchannel-logout/

    例如 https://specivo.example.com/auth/oidc/keycloak/callback/。结尾的斜杠是地址的一部分。如果 Specivo 部署在路径前缀下(STEALTH_PREFIX),在这些地址以及下文的注销后地址中,前缀紧跟在 {APP_URL} 之后。连接 卡片显示的是包含前缀的完整地址,直接从那里复制。

  2. 在 IdP 上为 Specivo 创建一个客户端(即“应用”),并严格按显示的内容注册重定向 URI。不要用通配符。如果 IdP 要求填写注销后的重定向地址,注册 {APP_URL}/login/?logged_out=1。如果要用后端通道注销,也把后端通道注销 URI 注册上。

  3. 把 IdP 上的 颁发者、客户端 ID 和 客户端密钥 复制到 Specivo。
  4. 保存,然后点击 测试连接。Specivo 会获取 IdP 的发现文档和签名密钥,并报告结果(见 测试连接)。
  5. 勾选 已启用(显示在登录页面) 并保存。新建的提供商默认处于禁用状态,这样没人会看到一个配置到一半的按钮。
  6. 打开一个隐私浏览窗口,通过新按钮登录。

更改标识串会改变地址

标识串是重定向 URI 和后端通道注销 URI 的一部分。如果你重命名提供商,必须在 IdP 上同时更新这两个地址,否则通过该提供商的登录会失败。如果该提供商被选为 将登录页面重定向到 的目标,那项设置会自动跟随新的标识串。

提供商设置

提供商表单有五个卡片。下面的表格列出了每一个字段。

连接

字段 作用
按钮文字 登录按钮上的名称:“使用按钮文字登录”。
标识串 上述地址的一部分。必须唯一。
颁发者 IdP 的颁发者地址,逐字复制。它必须与 IdP 公布的 issuer 以及每个 ID 令牌中的 iss 完全一致。多一个结尾斜杠、http 写成 https 以外的协议、或主机名不同,都会导致失败。
发现 URL 可选。留空表示 {issuer}/.well-known/openid-configuration。只有当 IdP 把配置发布在别处时才需要设置。
客户端 ID 在 IdP 上注册的客户端 ID。
客户端认证 Specivo 在令牌端点上证明自身的方式:client_secret_basic(默认)、client_secret_post,或者 none(公共客户端,仅依赖 PKCE)。除非 IdP 的文档另有说明,否则使用 client_secret_basic。
客户端密钥 见 客户端密钥。选 none 时不使用。
作用域 以空格分隔。默认 openid profile email。openid 是必需的,且始终排在最前面发送。如果 IdP 需要额外的作用域才能释放某个声明(例如 groups),在这里加上。
额外登录参数 可选的 prompt、domain_hint、login_hint、acr_values 和 max_age,会附加到登录请求中。这里不能设置其他参数。设置了 max_age 时,ID 令牌中的 auth_time 距今不得超过 max_age 秒(加上 SSO_CLOCK_SKEW_SECONDS),否则登录会以 sso_token_invalid 被拒绝。无论这里怎么设置,从个人资料页发起的 关联,以及没有密码的用户在修改电子邮件地址前的 重新登录,都始终发送 prompt=login 和 max_age=0。
已启用 在登录页面显示按钮。机密客户端没有密钥时无法启用。关闭它会把所有来自该提供商的会话都退出(见 会话)。
按钮顺序 数字越小越靠前。

Specivo 始终使用带 PKCE(S256)、state 和 nonce 的授权码流程,机密客户端也不例外。你的 IdP 必须允许 S256 的 PKCE。ID 令牌必须用非对称算法签名(RS256、PS256、ES256 等)。用共享密钥签名的令牌(HS256)一律拒绝。

客户端密钥

密钥是只写的。Specivo 只显示它的状态:

状态 含义
已存储(加密) 已保存在数据库中,用由 SECRET_KEY 派生的密钥加密。
从环境变量读取 提供商指向一个已设置的环境变量。
未设置环境变量 提供商指向的环境变量在容器中不存在。
不允许使用此环境变量名 变量名不以 SPECIVO_SSO_SECRET_ 开头。Specivo 绝不会读取它。
更改 SECRET_KEY 后无法读取:请重新输入 见下文。
未设置 什么都没有配置。
公共客户端无需设置 客户端认证为 none。

选择一种来源:

  • 存储在 Specivo 中。 把密钥粘贴到字段里。之后再保存时留空该字段即可保留当前密钥。
  • 从环境变量读取。 输入一个以 SPECIVO_SSO_SECRET_ 开头、后接大写字母、数字和下划线的变量名,例如 SPECIVO_SSO_SECRET_KEYCLOAK。 在 .env.local 中设置它,然后重建容器。每个处理请求的 Specivo 容器都要设置。其他变量名会被拒绝,因此提供商设置永远无法用来读取 SECRET_KEY 或 DATABASE_URL。这样密钥既不进入数据库,也不会出现在数据库备份里。

轮换 SECRET_KEY 会让已存储的密钥无法读取

存储的密钥是用由 SECRET_KEY 派生的密钥加密的。如果你更改了 SECRET_KEY,所有已存储的客户端密钥都会变得无法读取,提供商列表会显示 无法读取,请重新输入。逐个打开提供商,重新粘贴密钥并保存。从环境变量读取的密钥不受影响。

登录策略

字段 作用
首次登录时创建账户 即时(JIT)预配。默认关闭。 见 谁能获得账户。
现有账户 已有 Specivo 账户的用户如何与其 IdP 身份关联:由用户自己关联(默认),或按已验证的邮箱自动关联。见 关联现有账户。
此提供商确实会验证邮箱地址 告诉 Specivo 可以相信 email_verified 声明。自动关联必须开启此项。默认关闭。对 Microsoft Entra ID 绝对不要开启。
允许的邮箱域名 每行一个域名。留空则允许所有域名。见 限制谁可以登录。
最长会话时长(小时) 通过此提供商登录的会话最长持续时间,1 到 168(一周)。默认 24。见 会话。

声明

ID 令牌中的哪些声明(claim)用来填充 Specivo 个人资料。字段留空则使用标准声明。

字段 默认值 用途
用户名声明 preferred_username 新建账户的登录名。
邮箱声明 email 电子邮件地址。
姓名声明 name 显示名称。
语言声明 locale 界面语言。th、th-TH 和 th_TH 都映射为 th。Specivo 没有对应翻译的值会被忽略。
组声明 groups 用于组映射的组或角色。可以是点分路径,例如 groups、realm_access.roles 或 resource_access.specivo.roles。
同时从 userinfo 端点读取声明 关闭 把 IdP 的 userinfo 响应合并进声明中。当 IdP 没有把声明放进 ID 令牌时使用。userinfo 中的 subject 必须与 ID 令牌一致,否则登录会被拒绝。

依赖邮箱验证时,邮箱声明请保持为 email

email_verified 只为标准的 email 声明背书。如果你把 邮箱声明 设为 其他值(upn、自定义属性),Specivo 会始终把该地址视为未验证,即使 此提供商确实会验证邮箱地址 已开启。这时邮箱域名列表会拒绝所有人,用户的 Specivo 邮箱不再跟随 IdP,首次登录时创建的账户邮箱标记为未验证,自动关联也永远不会生效。

组

必需的组、管理员组以及组映射。见 组与角色。

会话检查

字段 作用
续期会话时在提供商处检查 默认开启。 Specivo 续期会话之前(用户活跃时每 15 分钟一次),会向 IdP 询问该用户是否仍可登录。在 IdP 上被禁用、删除或退出的人,会在下一次续期时失去 Specivo 会话。需要 IdP 发放刷新令牌;见 会话检查。
如果无法连接提供商 保留会话(默认):续期照常进行,下一次续期再问。能阻断 Specivo 与 IdP 之间流量的人可以借此保住会话,但不会超过 最长会话时长。结束会话:提供商不可用期间,来自该提供商的所有人都会被退出。本地登录始终可用。

注销

字段 作用
退出 Specivo 时也在提供商处退出 RP 发起的注销,默认开启。见 会话与注销。
接受来自提供商的后端通道注销 默认关闭。见 会话与注销。
当提供商结束会话时 后端通道注销要结束什么:对应的会话(默认)、此人来自该提供商的所有会话,或此人的所有会话及其 API 密钥。见 后端通道注销结束什么。

测试连接

在已保存的提供商上点击 测试连接。Specivo 会立即获取发现文档和签名密钥,不保存任何内容,并显示:

  • 找到的地址(授权、令牌、userinfo 和结束会话端点);
  • IdP 报告的颁发者是否与你输入的一致;
  • 可用的签名算法、IdP 提供的客户端认证方法,以及它发布了多少个签名密钥;
  • IdP 是否宣告支持后端通道注销和刷新令牌授权;
  • 会话是否在提供商处得到检查,以最近一次登录的结果为准:正常、不可用:提供商未发送刷新令牌、不可用:提供商拒绝了刷新令牌,或者在开启检查后还没有人登录过时显示 尚不清楚。 只有真实登录才能拿到刷新令牌,所以测试本身无法尝试;
  • 对那些能工作但多半不是你想要的设置发出警告:IdP 未宣告支持所选的客户端认证方法或 PKCE,开启了在提供商处退出但 IdP 没有结束会话端点,开启了后端通道注销但 IdP 不支持,开启了 userinfo 但 IdP 没有,客户端密钥缺失、无法读取或指向无效的环境变量,或者开启了会话检查但它无法工作(见 会话检查)。

测试不会让任何人登录。它证明 Specivo 能连上 IdP,并且颁发者正确。它无法证明客户端 ID、密钥或重定向 URI 是正确的,只有真实登录才能证明。

用户如何登录

登录页面在密码表单上方为每个已启用的提供商显示一个 使用按钮文字登录 按钮。IdP 确认用户身份后,Specivo 启动自己的会话,和密码登录建立的会话是同一种。IdP 的令牌不会保留,只有 ID 令牌例外(加密后存在会话中,以便退出登录时能告诉 IdP 结束哪个会话)。

Specivo 识别身份的依据是 (颁发者,subject) 这一对:IdP 的地址加上用户在那里的永久 ID。绝不是电子邮件地址。在 IdP 上更改某人的邮箱不会移动其 Specivo 账户,也没有人能通过把邮箱改成相同地址来接管账户。

谁能获得账户

对于没有关联账户的用户,Specivo 按以下顺序决定:

  1. 如果这个(颁发者,subject)已经关联到某个 Specivo 用户,该用户登录。
  2. 否则,如果自动关联的条件满足,该身份关联到匹配的用户。
  3. 否则,如果 首次登录时创建账户 已开启,就创建一个新账户。它没有密码,处于活跃状态,登录名取自用户名声明(清理为小写字母、数字、_ 和 -,重名时追加数字)。缺少邮箱会中止创建(sso_email_missing)。邮箱已被其他账户使用也会中止(sso_email_conflict):这种情况下用户应改用密码登录,再从个人资料页关联账户。新账户绝不会通过邮箱关联到现有账户。
  4. 否则,登录以 sso_account_not_provisioned 被拒绝。

即时创建默认关闭。当 IdP 放行的每个人都应该获得账户时才开启它;如果不是,就配合必需的组或邮箱域名列表一起使用。

即使账户已关联,也可能被拒绝:已停用的账户、被管理员锁定的账户、仍在等待邮箱验证的账户,以及服务账户,都不能通过 SSO 登录(sso_account_disabled)。多次输错密码导致的锁定不会阻止 SSO,因为它只保护密码。

关联现有账户

大多数安装一开始用的是本地账户。把它们与 IdP 关联有两种方式。

手动(默认)。 用户用密码登录,打开 个人资料 → 已关联的账户(在账户菜单中),点击 关联按钮文字。即使用户在 IdP 上已有会话,IdP 也会要求重新登录(Specivo 发送 prompt=login 和 max_age=0),因此一个在 IdP 上处于登录状态的浏览器不能被用来关联别人的账户。在 IdP 登录后,用户回到个人资料页,账户已关联。从此登录页面上的按钮对他们生效。 之后可以点击 断开关联,但前提是保留另一种登录方式(密码或另一个已关联的账户)。管理员可以在用户的管理页面断开任何身份的关联。断开关联会结束通过该身份建立的所有会话。

一个身份只能属于一个用户(sso_identity_in_use),一个用户在每个提供商只能关联一个账户(sso_provider_already_linked)。

按已验证的邮箱自动关联。 选择 按已验证的邮箱自动关联(管理员除外),并开启 此提供商确实会验证邮箱地址。用户首次登录时,Specivo 只有在以下条件全部成立时才会把身份关联到本地账户:

  • ID 令牌中的 email_verified 是布尔值 true(文本 "true" 不算);
  • 恰好有一个本地用户使用该邮箱地址(不区分大小写);
  • 该用户自己的邮箱地址在 Specivo 中已标记为已验证;
  • 该用户不是管理员、服务账户或匿名用户;
  • 该用户在此提供商尚未关联任何账户。

其他任何情况都转入谁能获得账户中的步骤。

为什么自动关联默认关闭

按邮箱匹配正是账户被接管的途径。如果 IdP 允许用户设置未经验证的邮箱,或者允许其他租户的用户冒用你的邮箱,那么谁在个人资料里填上 [email protected],谁就能以 Alice 的身份登录。只对你自己运营、并且在发送 email_verified: true 之前确实验证过邮箱的 IdP 开启此项。管理员永远不会被自动关联,所以配置错误的 IdP 无法夺取管理员账户。对 Microsoft Entra ID 绝对不要开启自动关联或“确实会验证邮箱地址”:它的 email 声明未经验证,而且用户可以自行修改。

每次登录都会跟随 IdP 的内容

用户再次登录时,Specivo 会更新:

  • 显示名称;
  • 语言,来自语言声明;
  • 电子邮件地址,仅当提供商受信任(此提供商确实会验证邮箱地址)、令牌中 email_verified: true、并且地址来自标准的 email 声明时才更新。如果新地址已被其他账户使用,则跳过。没有这层信任,用户的 Specivo 邮箱不会跟随 IdP,否则任何能在 IdP 上编辑自己邮箱的人都能在这里改掉它;
  • 管理员标志和用户组成员资格,如果你开启了组同步。

用户从个人资料页关联账户时,不会更新任何内容。

限制谁可以登录

在创建、关联或登录账户之前会先运行两项检查,所以被拒绝的人永远不会被预配。

允许的邮箱域名。 每行一个域名,精确匹配,不区分大小写。子域名不匹配其父域名。域名来自:

  • Google 的 hd(托管域名)声明,仅当颁发者恰好是 https://accounts.google.com 时。Google 根据 Workspace 成员资格设置它。其他 IdP 发来的同名声明一律忽略。
  • 否则来自邮箱地址,且仅当提供商受信任(此提供商确实会验证邮箱地址)、令牌中 email_verified: true、并且 邮箱声明 是标准的 email 时。在用户能自行编辑邮箱的地方,未验证的地址什么也证明不了,所以未验证的地址会被拒绝。

未通过检查的用户收到 sso_domain_not_allowed。这意味着对于邮箱未经验证的 IdP(例如 Entra ID),这个列表起不到任何作用。对那类 IdP 请改用必需的组。

必需的组。 见下文。

组与角色

Specivo 可以从 ID 令牌中读取组或角色声明,并以三种方式使用它。所有匹配都是精确且区分大小写的:Specivo-Admins 和 specivo-admins 是两个不同的组。

把 组声明 设为声明的名称或指向它的点分路径。值必须是字符串列表或单个字符串,其他类型一律视为缺失。

设置 效果
必需的组 只有至少属于其中一个组的人才能登录。留空则允许该提供商的所有人登录。一个组都不属于的人收到 sso_not_in_required_group。完全没有组声明的令牌视为没有任何组,同样会被拒绝。
管理员组 加上 每次登录时根据管理员组设置管理员标志 这些组中的人在 Specivo 中是管理员。不在其中的人会在下次登录时失去管理员标志。如果降级会导致没有其他能登录的活跃管理员,则绝不会执行降级:跳过降级并记录一条警告。
组映射 加上 每次登录时根据下方映射更新 Specivo 用户组成员资格 每一行把一个 IdP 组映射到一个 Specivo 用户组。IdP 组中的人被加入对应的 Specivo 用户组,离开 IdP 组时被移除。项目角色来自 Specivo 用户组自身的项目成员资格,所以一条映射就能在多个项目中授予角色。

保存提供商后即可添加组映射。先在 管理 → 用户组 下创建 Specivo 用户组。

同步只处理它自己创建的内容。管理员手动添加的成员资格,或由其他提供商管理的成员资格,永远不会被移除。

当该提供商的 SCIM 预配开启时,登录时会跳过组映射:此时由 IdP 通过 SCIM 管理成员资格。必需的组和管理员组仍然生效。

声明缺失绝不会移除任何人

如果令牌中缺少组声明(IdP 的映射器出故障时会这样),Specivo 不会更改任何成员资格或管理员标志。只有当声明存在且不再包含该组时,它才会移除成员资格。如果设置了 必需的组,缺失声明仍会导致登录被拒绝。

Microsoft Entra ID 发送的组过多

当用户所属的组多到令牌装不下时,Entra ID 会发送一个标记而不是组列表(即“组溢出”)。Specivo 无法读取真实列表。如果访问权由必需的组或管理员组决定,它会以 sso_groups_overage 拒绝登录。否则它让用户登录,但不更改任何成员资格。解决方法在 IdP 一侧:见 Microsoft Entra ID。

会话与注销

会话时长。 通过提供商登录的会话最长持续 最长会话时长(默认 24 小时,最多 168 小时即一周),即使用户一直在使用 Specivo。到期后需要重新经过 IdP。会话检查正常工作时,在 IdP 上被禁用的人会更快被拦下;没有它,这个数字就是他们还能在 Specivo 中继续工作的时间,除非后端通道注销先结束了会话。

会话跟随它的提供商和身份。 每当访问令牌过期(默认每 15 分钟),浏览器就在后台续期会话。一旦提供商被禁用或身份被断开关联,SSO 会话就不再续期并随之结束。关闭提供商会立即结束所有来自它的会话(管理页面会显示数量;API 在 PATCH 响应的 X-Revoked-Sessions 头中返回计数)。重新开启不会恢复这些会话:用户需要重新登录。密码会话和来自其他提供商的会话不受影响。

会话检查

Specivo 签发自己的会话令牌。没有额外机制的话,它要到会话达到最长时长才会再去问 IdP。开启 续期会话时在提供商处检查(默认开启)后,它会把 IdP 在登录时签发的刷新令牌加密保存下来,并在每次续期会话时出示给 IdP,频率最多每 SSO_IDP_RECHECK_MINUTES 一次(默认为访问令牌的生命周期,15 分钟):

  • IdP 接受:会话续期(如果 IdP 轮换了刷新令牌,则保留新的);
  • IdP 拒绝该用户(invalid_grant、access_denied 或其他针对用户的拒绝):说明他们在那边被禁用、删除或退出了,或者 IdP 会话已过期。Specivo 会话立即结束,他们打开的下一个页面就是登录页。安全审计日志记录 sso_session_rejected;
  • 无法连接 IdP,或者 IdP 拒绝的是 Specivo 本身而不是用户(invalid_client、unauthorized_client、invalid_scope、invalid_request、unsupported_grant_type、server_error、temporarily_unavailable):由 如果无法连接提供商 决定,日志中会有一条 sso.session.idp_check_failed。

这种机制适用于任何签发刷新令牌的 OpenID Connect IdP,不需要后端通道注销。API 密钥不受它影响。

无法工作的情况。 有些 IdP 只有在 Specivo 请求 offline_access 作用域时才签发刷新令牌(authentik、Microsoft Entra ID、Okta 等);对这些 IdP,把它加到 作用域 中。Keycloak 对 openid 就已经签发刷新令牌,而且在 Keycloak 上 offline_access 会创建在 Keycloak 退出登录后仍然存活的离线会话,所以不要给 Keycloak 加这个作用域。Specivo 会在每次登录后立即试用一次刷新令牌。如果 IdP 没有发送,或者拒绝了它(客户端可能不允许使用刷新令牌授权),该用户的会话就按原来的方式续期,测试连接会显示原因。不会有人因为检查无法工作而被退出。

IdP 自身的会话超时现在也会生效。 刷新令牌的寿命通常与用户在 IdP 上的会话一样长。当该会话在 IdP 上因空闲超时或达到最长时长而结束时(Keycloak 中为 SSO Session Idle,默认 30 分钟,以及 SSO Session Max,10 小时),Specivo 的下一次续期会被拒绝,用户需要重新登录。如果这太频繁,请在 IdP 上调高这些超时,或者为该提供商关闭会话检查。

令牌只属于签发它的 IdP。 保存的刷新令牌只会发给它所来自的颁发者、令牌端点和客户端 ID。更改提供商的颁发者、发现 URL 或客户端 ID 会丢弃其所有打开会话中保存的令牌,IdP 发现文档中出现新的令牌端点也一样。这些会话之后不再带检查地续期,直到用户重新登录。

退出 Specivo。 页头中的 退出登录 按钮是一个向 /logout/ 提交 POST 的表单。指向 /logout/ 的普通链接或书签只会打开“退出登录?”确认页面,不会让任何人退出。如果会话来自 SSO 提供商、退出 Specivo 时也在提供商处退出 已开启、并且 IdP 有结束会话端点,浏览器会被送到 IdP 去结束那边的会话,然后返回 {APP_URL}/login/?logged_out=1。这个地址是固定的,请把它注册为 IdP 允许的注销后重定向地址。如果 IdP 不可用或该设置关闭,用户只是退出 Specivo 而已。

后端通道注销 默认关闭。开启后,IdP 可以直接(服务器对服务器)告诉 Specivo 某人的会话已结束,Specivo 随即结束对应的会话。在 IdP 上注册 后端通道注销 URI,勾选 接受来自提供商的后端通道注销,并确保 IdP 能通过该地址访问到 Specivo。Microsoft Entra ID 不支持此功能。测试连接会显示 IdP 是否宣告支持它。

对于有效的注销请求,无论是否匹配到会话,Specivo 都返回 200。令牌缺失、无效或重放时返回 400;提供商未知、已禁用或关闭了后端通道注销时返回 404;当前无法校验令牌时返回 503 以便 IdP 重试:Redis 或 IdP 的密钥不可达,或者令牌用 Specivo 没见过的密钥签名而它在不到一分钟前刚重新读取过 IdP 的密钥(密钥刚轮换时会这样)。同一提供商、同一发送方地址(IPv6 按 /64 网段计)每分钟超过 60 个请求时返回 429。

后端通道注销结束什么

由 当提供商结束会话时 决定:

选项 结束什么
结束对应的 Specivo 会话(默认) 注销请求所指明的 IdP 会话(其 sid)对应的 Specivo 会话。只指明用户(sub)而未指明会话的注销,会结束此人来自该提供商的所有会话。
结束此人来自该提供商的所有会话 来自该身份的所有会话,不论属于哪个 IdP 会话。
结束其所有会话并关闭其 API 密钥 同上;而且如果该账户只通过 SSO 登录(没有密码),它的所有会话(包括来自其他提供商的)以及全部 API 密钥(含 MCP 密钥)都会被关闭。安全审计日志记录 user_deprovisioned,原因为 sso_backchannel。

最后一个选项绝不会停用账户,也绝不会触碰有密码的账户、服务账户或管理员(这些账户按第二种行为处理;对管理员,Specivo 记录 sso.deprovision.skipped ... reason=administrator)。管理员的离职请在 Specivo 中处理:移除管理员权限或停用账户。如果此人之后再次通过 IdP 登录,是可以的,但其 API 密钥会一直保持关闭,直到本人或管理员重新开启。IdP 对用户主动退出和被禁用发送的是同一种注销,所以只有在你能接受“在 IdP 退出登录也会关闭该用户的 API 密钥”时才选它。对同一会话重复发送注销不会有任何变化。

人员离职

最快的方式是 SCIM 预配:在 IdP 上禁用或删除此人会立即停用其 Specivo 账户(会话和 API 密钥)。没有 SCIM 时,Specivo 通过以下途径得知有人离开。IdP 支持多少就用多少。各提供商页面都有一份简短的操作手册: Keycloak、authentik。

  1. 在 IdP 上禁用此人。 他们无法再登录。会话检查正常工作时,其 Specivo 会话在下次续期时结束,通常在一个访问令牌生命周期(15 分钟)内,最迟不超过 SSO_IDP_RECHECK_MINUTES 加上该生命周期。没有会话检查时,会话会一直有效,直到你结束它或它达到最长会话时长(最多一周)。
  2. 后端通道注销,在 IdP 支持的情况下,会在 IdP 结束其会话时立即结束 Specivo 会话。哪些 IdP 操作会触发注销因 IdP 而异:Keycloak 在管理员把用户退出时发送(禁用时不发送),authentik 在用户被禁用、删除或退出时发送。配合 结束其所有会话并关闭其 API 密钥,它还会关闭此人的 API 密钥(见 后端通道注销结束什么)。
  3. 登录被拒绝,原因可能是失去了必需的组、邮箱域名不再符合要求,或者组溢出遮住了其所属的组(sso_groups_overage),这会结束来自该身份的剩余会话。
  4. 在 Specivo 中停用该用户。 在 管理 → 用户 中打开该用户的页面,点击 停用账户(或调用 POST /api/v1/admin/users/{id}/deactivate/)。这对所有账户都有效,不论有没有 SSO,服务账户也包括在内。它一步完成结束所有会话(浏览器的下一次请求被拒绝)并关闭其全部 API 密钥。密钥被保留但处于关闭状态,这样历史记录仍可查阅。重新启用账户 让此人可以再次登录,但 API 密钥保持关闭:他们可以在自己的 API 密钥页面重新开启,或者由本人或管理员创建新的。你不能停用自己的账户,也不能停用最后一个活跃的管理员。
  5. 断开其身份的关联,在用户的管理页面操作,以结束来自该身份的会话。

每个提供商还有一个选项 当登录因必需的组或允许的邮箱域名被拒绝时停用 Specivo 账户(deactivate_on_access_loss,默认关闭)。开启后,被拒绝的登录(第 3 步,包括组溢出)会像第 4 步那样停用账户,但只针对已经关联到该身份的账户。Specivo 绝不会以这种方式停用服务账户或最后一个活跃的管理员(只记录一条警告)。

API 密钥。 通过 SSO 登录的用户可以创建 API 密钥(默认开启)。密钥不经过 IdP,所以会话检查和普通的后端通道注销都拦不住它:IdP 禁用此人之后它仍然有效,直到密钥过期、你停用该用户,或者一次带有 结束其所有会话并关闭其 API 密钥 的后端通道注销把它关闭。这就是为 SSO 用户保留密钥的取舍:智能体和脚本在 IdP 故障或某个组被遗忘时仍能继续工作,但离职处理需要上述步骤之一。如果你不想这样,在 管理 → 设置 → 身份验证 下关闭 通过 SSO 登录的用户可以使用 API 密钥。 它作用于没有密码且有已关联身份的账户:这类账户不能创建密钥,而且在该设置关闭期间,它们现有的密钥都会被拒绝。无论该设置如何,这类账户都不能通过 忘记密码 获得密码;管理员仍然可以有意为其设置一个。有密码的用户和服务账户不受影响。

管理员的 API 密钥可以通过管理 API(/api/v1/admin/sso/...)读取单点登录配置,但不能修改。创建、编辑或删除提供商及其组映射,以及保存身份验证设置,都需要管理员登录 Specivo 操作;API 密钥会得到 403 api_key_not_allowed。因此泄露的密钥无法添加一个把自己的用户映射为管理员的身份提供商。所有会交出登录途径的管理操作(例如重置密码)也都如此;见 管理员的密钥不能做什么。

身份验证设置

管理 → 设置 → 身份验证 作用于整个实例:

设置 作用
在登录页面隐藏密码表单 把表单替换为一个指向 /login/?local=1 的链接 使用本地账户登录。没有任何已启用的提供商时,表单始终显示。
仅管理员可使用密码登录 其他人必须使用 SSO。上面的链接仍会显示表单,但非管理员的密码登录会被拒绝。此时每次被拒绝的密码登录都得到同样的“登录名或密码不正确”答复,被锁定、已停用或未验证的管理员账户也一样,这样表单不会暴露谁是管理员。原因记录在 specivo.sso 日志中(sso.local_login.refused_admin)。输错密码仍会让管理员被锁定一段时间;期间他们仍可通过 SSO 登录。
通过 SSO 登录的用户可以使用 API 密钥 默认开启。
将登录页面重定向到 把访问者直接送到某一个提供商。/login/?local=1 可以绕过它,已登录的访问者、退出登录、密码重置和任何 SSO 错误也都会绕过,所以出故障的提供商不会造成循环。

如果你把自己锁在这些设置之外了,参见 重新获得访问权限。

环境变量设置

这些设置和其他配置一样写在 .env 中,对所有提供商生效。

变量 默认值 用途
APP_URL http://localhost:8030 Specivo 的公开地址。重定向和注销地址由它构造。配置提供商之前先设置它。
SSO_FORCE_LOCAL_LOGIN false 应急开关:始终显示密码表单,从不重定向,忽略隐藏表单和仅管理员的设置。
SSO_ALLOW_INSECURE_HTTP false 允许 http:// 的 IdP 地址。仅用于一次性的测试 IdP。绝不要在正式 IdP 上使用:令牌和密钥会以明文传输。
SSO_HTTP_TIMEOUT_SECONDS 5 每个发往 IdP 的请求最长允许的时间。
SSO_IDP_RECHECK_MINUTES 访问令牌生命周期(15) 每个会话续期时最多多久向 IdP 询问一次。见 会话检查。0 表示每次续期都询问。
SSO_CLOCK_SKEW_SECONDS 60 校验令牌时间时,Specivo 与 IdP 的时钟允许相差多少。超过 300 的值会被降到 300。请校准时钟(使用 NTP),而不是调大这个值。
SPECIVO_SSO_SECRET_<NAME> 无 供提供商指向的客户端密钥。

升级到带 SSO 的版本

  • 设置 APP_URL,然后再配置第一个提供商。
  • /logout/ 现在需要 POST。 页头的链接现在是按钮。指向 /logout/ 的书签会显示确认页面。
  • SECRET_KEY:如果之后轮换了它,需要重新输入已存储的客户端密钥(见 客户端密钥)。
  • 现有会话 继续有效。
  • 会话检查 在升级后对所有提供商开启,包括已有的提供商。没有人会因此被退出:升级时已打开的会话不带 IdP 刷新令牌,按原来的方式续期;之后开始的会话,只要 IdP 签发刷新令牌就会被检查(IdP 需要时请把 offline_access 加到 作用域 中)。首次登录后点击 测试连接,查看它是否正常工作。

故障排查

登录失败时,用户会回到登录页面并看到一条消息。如果是在自己的账户页面操作失败(关联 失败),则回到 个人资料 页。消息从不复述 IdP 说了什么,所以要看错误代码和日志。代码以 ?sso_error=<code> 的形式出现在地址中。原因记录在 Specivo 日志中。api 容器每行写一个 JSON 对象(DEBUG=true 时为纯文本)。SSO 日志来自 specivo.sso 记录器,消息以事件名开头,例如 sso.id_token.rejected ... reason=wrong_issuer 或 sso.login.refused_policy。查看方式:docker compose logs api | grep specivo.sso。日志中绝不包含令牌或密钥。

代码 用户看到的消息 检查什么
sso_state_invalid “你的登录已过期,或是在另一个浏览器中发起的。” 登录耗时超过 10 分钟,用户点了后退按钮,在第二个浏览器或标签页组中打开了链接,或者禁用了 Cookie。该 Cookie 的作用范围是 /auth/oidc/。在代理后面时,检查代理是否剥掉了 Cookie。重试即可。
sso_idp_error “身份提供商拒绝了登录。” IdP 返回了错误,例如因为用户未被分配到该应用而返回 access_denied,或者它拒绝了授权码。令牌请求失败也显示同一条消息。检查客户端密钥、重定向 URI 以及用户在 IdP 上的访问权限。日志中有 IdP 的错误代码。
sso_idp_unreachable “身份提供商不可用。” Specivo 在 SSO_HTTP_TIMEOUT_SECONDS 内无法连接 IdP 的发现、密钥或令牌端点,或者收到了服务器错误。当令牌指向一个 Specivo 不认识的签名密钥、而它刚刚重新读取过 IdP 的密钥时(IdP 可能刚轮换了密钥),这条消息也会显示一分钟;一分钟后重试即可成功。从 Specivo 容器内检查 DNS、防火墙和证书。期间管理员可以使用 使用本地账户登录 链接。
sso_discovery_invalid “单点登录配置不正确。” 发现文档或密钥不可用:文档中的颁发者与你输入的不一致,某个端点是 http://,或者没有可用的签名密钥。点击 测试连接。
sso_provider_misconfigured “单点登录配置不正确。” 客户端密钥无法读取或缺失。重新输入,或检查环境变量名以及该变量是否已在容器中设置。
sso_token_invalid “单点登录失败。请重试。” ID 令牌未通过某项校验。原因见日志:颁发者或受众不正确(颁发者或客户端 ID 不匹配)、签名无效、令牌已过期(服务器之间时钟漂移)、缺少 nonce 或 sub、令牌使用了 Specivo 不接受的算法签名(例如 HS256),或者在发送了 max_age 时 auth_time 缺失或过旧(关联 时始终如此:IdP 没有让用户重新登录)。
sso_unavailable “单点登录暂时不可用。” Specivo 无法连接 Redis,而它需要 Redis 来存储登录状态。本地登录仍然可用。检查 redis 容器。
sso_account_not_provisioned “这里没有账户关联到你的身份提供商账户。” 没有关联的账户,自动关联不适用,并且 JIT 创建已关闭。开启 首次登录时创建账户,从用户的个人资料页关联账户,或检查关联条件。
sso_account_disabled “此账户已禁用。” 关联的用户已停用、被管理员锁定、尚未验证邮箱地址,或者是服务账户。检查 管理 → 用户。
sso_email_missing “身份提供商没有发送电子邮件地址。” JIT 需要邮箱。在 IdP 上添加 email 作用域和声明,或者把 邮箱声明 设为正确的名称。对 Entra ID,添加 email 可选声明。
sso_email_conflict “使用此电子邮件地址的账户已存在。” JIT 将会创建第二个使用已有邮箱的账户。用户应改用密码登录,再从个人资料页关联账户。
sso_identity_in_use “此身份提供商账户已关联到另一个账户。” 关联时出现。该 IdP 身份属于另一个 Specivo 用户。先在那边断开关联。
sso_provider_already_linked “此身份提供商的一个账户已关联到你的个人资料。” 关联时出现。该用户在此提供商已经关联了一个账户。
sso_reauth_mismatch “请使用已关联到你个人资料的身份提供商账户重新登录,而不是其他账户。” 没有密码的用户在修改电子邮件地址前重新登录时出现。他们在 IdP 上登录的账户没有关联到其个人资料,或者在该提供商没有关联任何账户。
sso_domain_not_allowed “你的邮箱域名不允许在此登录。” 邮箱域名不在列表中,或邮箱未验证,或提供商未设为信任邮箱验证,或 IdP 没有发送邮箱。见 限制谁可以登录。
sso_not_in_required_group “你的身份提供商账户不在允许登录的组中。” 用户不属于任何必需的组,或者令牌没有组声明。检查 组声明 路径、IdP 的组映射器、作用域,以及组名是否完全一致(包括大小写)。
sso_groups_overage “你的身份提供商发送的组过多,无法读取。” Entra ID 的组溢出。见 Microsoft Entra ID。
sso_last_sign_in_method “这是你唯一的登录方式。” 断开关联时出现。用户没有密码,也没有其他已关联的账户。先设置密码或关联另一个账户。
sso_access_denied “单点登录失败。请重试。” 上面三个策略代码的通用形式。正常情况下你应该看到的是某个具体代码。
sso_token_exchange_failed 显示为 sso_idp_error。 IdP 的令牌端点拒绝了授权码。检查 IdP 上的客户端密钥、重定向 URI 和 PKCE 设置。

管理员还会遇到的其他情况:

  • IdP 提示“invalid redirect URI”。 它必须与 连接 卡片上的地址逐字一致,包括协议(https)、主机、端口和结尾斜杠。检查 APP_URL。
  • 提供商列表显示“无法读取,请重新输入”。 SECRET_KEY 变了。重新输入密钥。
  • 用户点击提供商按钮后看到 404 页面。 该提供商已禁用,或者标识串变了而旧链接被缓存。
  • 用户的显示名称或语言变了,但邮箱没变。 这是预期行为,除非提供商被设为信任邮箱验证。
  • 用户在 24 小时后被退出 Specivo。 这是 最长会话时长 在起作用。
  • 用户在离开半小时左右后被退出。 他们在 IdP 上的会话因空闲而超时,会话检查在下一次续期时发现了这一点。调高 IdP 的空闲超时,或者为该提供商关闭会话检查。见 会话检查。
  • 测试连接提示提供商拒绝了刷新令牌。 该客户端在 IdP 上可能没有被允许使用刷新令牌授权。允许它(authentik:Grant Types → Refresh token)。