自定义 OIDC 接入(部署版)¶
简介¶
观测云部署版支持通过自定义 OIDC 方式接入第三方身份提供商(IdP),用于处理以下场景:
- 单个 IdP,但其 OIDC 流程或返回结构与标准实现存在差异;
- 多个 IdP 并存,需要在登录入口按来源区分;
- 需要在
code -> token -> userinfo过程中接管地址生成、账号信息转换或账号归一。
本文基于“多 IDP 情况下的 OIDC 配置说明”整理,同时补充了单 IDP 的使用方式。两种场景共用同一套自定义 OIDC 能力,区别主要体现在前端登录入口和 Func 路由逻辑上。
前提条件¶
- 观测云底座版本不低于
1.123.216; - 已具备部署版 Launcher 配置权限;
- 已启用内置 Func,并可创建函数 API;
- 已从目标 IdP 获取
client_id、client_secret、授权地址、Token 地址、用户信息地址等信息; - 已明确用户信息字段与 观测云账号字段的映射关系。
实现原理¶
自定义 OIDC 方案的核心是将 OIDC 的关键流程改由 Func 接管:
OIDCClientSet.wellKnowURL不再直接指向 IdP 的.well-known/openid-configuration,而是指向 Func 中的well_know函数;- 登录时,观测云 内部通过
authorization_endpoint调用 Func 的get_auth_url,由该函数返回最终登录地址; - 回调时,观测云 内部通过
userinfo_endpoint调用 Func 的get_userinfo,由该函数内部完成code -> token -> userinfo; - Func 对不同 IdP 的用户信息进行归一化后,直接返回给 观测云,再由系统完成账号匹配与登录。
单 IDP 与多 IDP 的区别¶
| 场景 | 前端登录入口 | Func 路由方式 | 回调地址 |
|---|---|---|---|
| 单 IDP | 仅一个登录按钮 | 未传 type 时直接走默认 IdP |
可不带 type,也可固定带一个值 |
| 多 IDP | 多个登录按钮,每个按钮携带不同 type |
main_oidc 根据 type 分发到不同 IdP 子脚本 |
必须与对应 IdP 的回调地址保持一致,通常带 type=xxx |
如果是单 IDP,推荐直接在主脚本中将默认值写死,例如:xtype = args.get("type") or "keycloak"。这样后续若扩展为多 IDP,只需补充前端入口和分发逻辑即可。
操作步骤¶
1、配置 forethought-core/core 中的 OIDCClientSet¶
在 Launcher 中进入 命名空间:forethought-core > core,为 config.yaml 增加或修改如下配置:
OIDCClientSet:
# 开启自定义 OIDC
enableCustomOIDC: true
# 指向 Func 中 well_know 函数的 API 地址
wellKnowURL: "<Func well_know API 地址>"
mapping:
username: preferred_username
mobile: mobile
email: email
exterId: sub
说明:
enableCustomOIDC: true是启用自定义 OIDC 的关键开关;wellKnowURL必须指向 Func 中的well_know函数,而不是直接写 IdP 的服务发现地址;mapping中的字段名需要与 Func 最终返回的用户信息结构保持一致。
2、配置前端登录入口¶
在 Launcher 中进入 命名空间:forethought-webclient > front-web-config,修改 config.js。
单 IDP 示例¶
window.DEPLOYCONFIG = {
...
paasCustomLoginInfo: [
{
label: "OIDC 登录",
url: "https://<部署版 Web 域名>/oidc/login",
desc: "自定义 OIDC 登录"
}
]
}
多 IDP 示例¶
window.DEPLOYCONFIG = {
...
paasCustomLoginInfo: [
{
iconUrl: "https://<图标地址>",
label: "Keycloak 登录",
url: "https://<部署版 Web 域名>/oidc/login?type=keycloak",
desc: "OIDC Keycloak 登录"
},
{
iconUrl: "https://<图标地址>",
label: "Authing 登录",
url: "https://<部署版 Web 域名>/oidc/login?type=authing",
desc: "OIDC Authing 登录"
}
]
}
说明:
- 多 IDP 场景下,推荐通过
type参数标识来源; - 该
type会透传到 Func,供主脚本区分不同 IdP; - 如果 IdP 回调地址中也依赖
type,则前端登录入口、IdP 配置、Func 子脚本中的redirect_uri三处必须保持一致。
3、配置 Web Nginx 转发规则¶
在 命名空间:forethought-webclient > front-web-config 中修改 nginx.conf,将 OIDC 登录和回调请求转发到 inner 服务:
location /oidc/login {
proxy_connect_timeout 5;
proxy_send_timeout 5;
proxy_read_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "keep-alive";
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/login;
}
location /oidc/callback {
proxy_connect_timeout 5;
proxy_send_timeout 5;
proxy_read_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "keep-alive";
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/callback;
}
这一步的目的是让浏览器访问的 /oidc/login 和 /oidc/callback 最终进入部署版内部的 OIDC 处理逻辑。
4、在 Func 中编写主脚本¶
主脚本负责对外提供统一入口,并按 type 分发到不同 IdP 子脚本。建议至少包含以下三个函数:
well_know:返回给OIDCClientSet.wellKnowURL使用的服务发现信息;get_auth_url:生成并返回最终跳转到 IdP 的认证地址;get_userinfo:接收回调参数,在内部完成code -> token -> userinfo。
示例:
import __keycloak as keycloak_client
import __authing as authing_client
@DFF.API('OIDC 服务发现接口')
def well_know():
return {
"authorization_endpoint": "<Func get_auth_url API 地址>",
"token_endpoint": "",
"userinfo_endpoint": "<Func get_userinfo API 地址>"
}
@DFF.API('获取登录地址信息')
def get_auth_url(**kwargs):
args = kwargs.get("args", {})
xtype = args.get("type") or "keycloak"
if xtype == "keycloak":
return keycloak_client.get_auth_url(**kwargs)
elif xtype == "authing":
return authing_client.get_auth_url(**kwargs)
else:
raise Exception(f"非法值 type=`{xtype}`")
@DFF.API('用户信息获取接口')
def get_userinfo(**kwargs):
args = kwargs.get("args", {})
xtype = args.get("type") or "keycloak"
if xtype == "keycloak":
return keycloak_client.get_userinfo(**kwargs)
elif xtype == "authing":
return authing_client.get_userinfo(**kwargs)
else:
raise Exception(f"非法值 type=`{xtype}`")
说明:
well_know返回的authorization_endpoint和userinfo_endpoint实际上都是 Func API 地址;token_endpoint在这套方案中通常可以留空,因为code -> token的过程已经在get_userinfo内部完成;- 单 IDP 场景也推荐保留这层主脚本,后续扩展更方便。
5、为每个 IdP 编写子脚本¶
每个 IdP 子脚本建议独立维护,例如 __keycloak.py、__authing.py。子脚本至少需要实现以下内容:
OIDC_SET:当前 IdP 的客户端配置;well_know:当前 IdP 的真实 OIDC 端点;get_auth_url:生成认证地址;turn_token:根据回调code换取 Token;turn_userinfo:根据 Token 获取用户信息;get_userinfo:对外统一入口,内部依次调用turn_token与turn_userinfo。
示例结构:
OIDC_SET = {
"client_id": "<OIDC 客户端 ID>",
"client_secret": "<OIDC 客户端密钥>",
"scope": "openid profile email address",
"redirect_uri": "https://<部署版 Web 域名>/oidc/callback?type=authing",
"grant_type": "authorization_code"
}
def well_know():
return {
"authorization_endpoint": "https://<IdP 授权地址>",
"token_endpoint": "https://<IdP Token 地址>",
"userinfo_endpoint": "https://<IdP 用户信息地址>",
"end_session_endpoint": "https://<IdP 登出地址>",
"jwks_uri": "https://<IdP JWKS 地址>"
}
!!! warning
上述 `OIDC_SET` 仅用于说明配置结构,不建议在 Func 脚本中直接硬编码 `client_secret`、Token 或其他敏感凭据。
如果需要管理密钥信息,优先通过 Func 侧的密码类型环境变量进行注入和读取;如果具体接入场景中仍需使用 `OIDCClientSet.clientSecret`,也建议通过同样的方式进行安全管理,避免明文写入脚本或配置示例。
关键注意事项:
- 多 IDP 场景下,
redirect_uri必须带上对应的type=xxx参数; - IdP 后台登记的回调地址必须与
OIDC_SET.redirect_uri完全一致; get_auth_url的返回值固定为{"url": "..."};get_userinfo的返回值必须是账号信息 JSON,且推荐将用户属性直接放在第一层结构中。
6、统一用户信息字段¶
Func 最终返回给 观测云 的用户信息,需要与 OIDCClientSet.mapping 一一对应。建议至少保证以下字段存在:
| 观测云账号字段 | IdP 字段示例 |
|---|---|
username |
preferred_username |
email |
email |
mobile |
mobile |
exterId |
sub |
如果多个 IdP 中同一个人使用的是同一邮箱,建议在 Func 中统一处理为相同账号标识,例如:
- 将
email统一设置为企业邮箱; - 将
sub或exterId统一刷成邮箱值; - 不同 IdP 返回结构不一致时,先在 Func 内转换成统一 JSON 后再返回。
这一步是避免多 IdP 登录时生成重复账号的关键。
7、必要时清理历史 OIDC 账号¶
如果环境中已经存在旧的 OIDC 账号,且不同 IdP 导致同一邮箱生成了多个账号,可按以下思路处理:
-- 查询 OIDC 账号中是否存在相同邮箱的重复账号
select email, count(id) as num
from `main_account`
where status = 0 and `exterId` <> ""
group by email
having num > 1;
-- 清理多余账号后,可统一将 exterId 刷为 email
update `main_account`
set exterId = email
where status = 0 and `exterId` <> "";
执行前请先完成备份,并结合实际账号状态谨慎处理。
调试建议¶
- 在联调初期,可在
get_userinfo的最后打印账号信息,并临时抛出异常,先确认字段是否符合预期,再放开登录; - 如果出现重复账号,优先检查不同 IdP 返回的
email、sub、exterId是否已统一; - 如果回调后登录失败,优先检查
redirect_uri是否与 IdP 后台登记值完全一致; - 如果单 IDP 改造为多 IDP,先保留默认
type,再逐步增加新的登录入口和子脚本。
常见问题¶
1、为什么 wellKnowURL 不能直接写 IdP 的 .well-known 地址?¶
因为这套方案的目标不仅是读取标准 OIDC 端点,还要把登录地址生成、回调处理、账号归一这些逻辑交给 Func,因此 wellKnowURL 需要指向自定义 well_know 函数。
2、单 IDP 为什么也推荐使用这套方案?¶
因为很多单 IDP 场景本质上也是“非标准 OIDC”接入,例如需要改造登录地址、回调参数、用户信息结构或账号主键。使用这套方式可以统一处理这些兼容性问题。
3、IdP 返回的用户信息结构层级很深怎么办?¶
建议在 Func 中先解析并拍平,再把最终结果整理为第一层结构后返回给 观测云,不要把复杂的嵌套结构直接交给系统账号映射。