跳转至

自定义 OIDC 接入(部署版)


简介

观测云部署版支持通过自定义 OIDC 方式接入第三方身份提供商(IdP),用于处理以下场景:

  • 单个 IdP,但其 OIDC 流程或返回结构与标准实现存在差异;
  • 多个 IdP 并存,需要在登录入口按来源区分;
  • 需要在 code -> token -> userinfo 过程中接管地址生成、账号信息转换或账号归一。

本文基于“多 IDP 情况下的 OIDC 配置说明”整理,同时补充了单 IDP 的使用方式。两种场景共用同一套自定义 OIDC 能力,区别主要体现在前端登录入口和 Func 路由逻辑上。

前提条件

  • 观测云底座版本不低于 1.123.216
  • 已具备部署版 Launcher 配置权限;
  • 已启用内置 Func,并可创建函数 API;
  • 已从目标 IdP 获取 client_idclient_secret、授权地址、Token 地址、用户信息地址等信息;
  • 已明确用户信息字段与 观测云账号字段的映射关系。

实现原理

自定义 OIDC 方案的核心是将 OIDC 的关键流程改由 Func 接管:

  1. OIDCClientSet.wellKnowURL 不再直接指向 IdP 的 .well-known/openid-configuration,而是指向 Func 中的 well_know 函数;
  2. 登录时,观测云 内部通过 authorization_endpoint 调用 Func 的 get_auth_url,由该函数返回最终登录地址;
  3. 回调时,观测云 内部通过 userinfo_endpoint 调用 Func 的 get_userinfo,由该函数内部完成 code -> token -> userinfo
  4. 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_endpointuserinfo_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_tokenturn_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 统一设置为企业邮箱;
  • subexterId 统一刷成邮箱值;
  • 不同 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 返回的 emailsubexterId 是否已统一;
  • 如果回调后登录失败,优先检查 redirect_uri 是否与 IdP 后台登记值完全一致;
  • 如果单 IDP 改造为多 IDP,先保留默认 type,再逐步增加新的登录入口和子脚本。

常见问题

1、为什么 wellKnowURL 不能直接写 IdP 的 .well-known 地址?

因为这套方案的目标不仅是读取标准 OIDC 端点,还要把登录地址生成、回调处理、账号归一这些逻辑交给 Func,因此 wellKnowURL 需要指向自定义 well_know 函数。

2、单 IDP 为什么也推荐使用这套方案?

因为很多单 IDP 场景本质上也是“非标准 OIDC”接入,例如需要改造登录地址、回调参数、用户信息结构或账号主键。使用这套方式可以统一处理这些兼容性问题。

3、IdP 返回的用户信息结构层级很深怎么办?

建议在 Func 中先解析并拍平,再把最终结果整理为第一层结构后返回给 观测云,不要把复杂的嵌套结构直接交给系统账号映射。

文档评价

文档内容是否对您有帮助? ×