基于 Func 自定义 OIDC 对接观测云最佳实践
概述
在企业可观测性平台的建设过程中,身份认证的统一管理是保障平台安全和提升管理效率的关键环节。观测云支持基于 OIDC/OAuth 2.0 协议的单点登录(SSO),但对于部分非标准 OIDC 场景——如身份提供商(IdP)的服务发现接口不规范、用户信息返回结构特殊等情况,标准配置方式难以满足需求。
观测云部署版提供了一套基于 Func(DataFlux Func) 的自定义 OIDC 接入方案,通过将 OIDC 的关键流程交由 Func 接管,实现对各类非标准 IdP 的灵活适配。本文旨在提供一套经过验证的实践方案,帮助企业高效、安全地完成基于 Func 的自定义 OIDC 对接。
适用场景
本方案适用于以下场景:
| 场景类型 | 具体说明 |
|---|---|
| 非标准 OIDC IdP | IdP 的 OIDC 流程或返回结构与标准实现存在差异 |
| 定制化流程改造 | 需要在 token 换取或用户信息获取过程中进行地址改写、参数转换或账号归一化 |
| 回调地址改写 | 因网络架构或域名替换需求,需要动态处理 redirect_uri |
方案优势
- 统一管理:在 Func 中集中管理 IdP 配置和账号映射逻辑
- 灵活适配:可适配任意 OAuth 2.0/OIDC 规范的 IdP,包括各类国产协同平台
- 三函数协同:通过
well_know、turn_token、turn_userinfo三个函数实现全流程接管 - 账号归一:解决同一用户通过不同 IdP 登录时产生重复账号的问题
- 安全可控:敏感信息(client_secret 等)通过 Func 密码环境变量管理,避免硬编码
架构设计
整体架构
基于 Func 的自定义 OIDC 方案,核心思路是将 OIDC 的关键流程交由 Func 接管,使观测云不再直接与 IdP 交互,而是通过 Func 作为中间层完成协议适配和账号归一化。
观测云 OIDC Client 会依次调用 Func 提供的三个核心函数:
| 函数名 | 作用 | 调用时机 |
|---|---|---|
| well_know | 返回 OIDC 服务发现信息,告知观测云 authorization_endpoint 和 userinfo_endpoint 的地址 | 观测云启动时、周期性刷新配置 |
| turn_token | 根据授权码 code 向 IdP 换取 Access Token | 用户完成 IdP 认证后,观测云收到回调时 |
| turn_userinfo | 根据 Access Token 获取用户信息并进行字段映射 | 获取 Token 成功后 |

核心流程详解
阶段一:服务发现
观测云的 OIDCClientSet 在初始化时会调用 Func 的 well_know 函数,获取以下关键信息:
{
"authorization_endpoint": "https://func.example.com/api/v1/turn_token",
"userinfo_endpoint": "https://func.example.com/api/v1/turn_userinfo"
}
关键设计:authorization_endpoint 实际指向的是 Func 的 turn_token 函数地址,而非直接跳转到 IdP。观测云会在用户登录时自动将授权码 code 作为参数传递给该地址。
阶段二:用户登录跳转
- 用户访问观测云 SSO 登录页,选择对应 IdP 入口
- 观测云根据
well_know返回的authorization_endpoint,将用户重定向至 Func 的turn_token函数(携带回调参数) - 观测云实际登录流程会自动完成 IdP 认证页面的跳转和 code 的回传
阶段三:Token 换取与用户信息获取
- 用户完成 IdP 侧认证后,携带授权码
code回调至观测云 - 观测云调用 Func 的
turn_token函数,传入code参数 turn_token函数内部使用code向 IdP 的 Token 端点换取 Access Token,并返回给观测云- 观测云获取到 Access Token 后,调用 Func 的
turn_userinfo函数,传入 Access Token turn_userinfo函数使用 Access Token 向 IdP 的用户信息端点获取用户详情- 对用户信息进行字段映射和归一化处理后,返回给观测云
- 观测云根据返回的用户信息创建或更新用户会话
配置步骤
前置准备
在开始配置前,请确认以下事项:
| 准备项 | 说明 |
|---|---|
| 观测云部署版 | https://private.guance.com |
| Launcher 配置权限 | 可修改 forethought-core/core 命名空间配置 |
| Func 环境 | 已启用 Func,并可创建函数 API https://private.func.com |
| 用户中心 | https://passport.user.com |
编写 Func
新建 oidc_func 脚本集,包含3个脚本。
oidc_func__well_know
import json
import requests
@DFF.API('OIDC服务发现接口')
def well_know():
result = {
# 这个地址提供原始的 获取登录认证 code 地址
"authorization_endpoint":"https://passport.user.com/oauth/authorize",
# code 换 token 接口,此地址需要指向 func 侧对应的 turn_token 函数地址
"token_endpoint":"https://private.func.com/api/v1/sync/sapi-gYaprJo5WPBq/s",
# 获取用户信息接口,此地址需要指向 func 侧对应的 turn_userinfo 函数地址
"userinfo_endpoint":"https://private.func.com/api/v1/sync/sapi-cldP8lHqnwc/s",
}
return result
oidc_func__turn_token
import json
import requests
@DFF.API('获取用户token接口')
def turn_token(**kwargs):
'''
获取访问 token 信息
'''
print("--------turn_token--------------")
headers = _DFF_HTTP_REQUEST.get("headers", {})
print(json.dumps(_DFF_HTTP_REQUEST))
print(json.dumps(kwargs))
new_headers = {
"content-type": headers.get("content-type"),
"authorization": headers.get("authorization")
}
url = "https://passport.user.com/oauth/token"
resp = requests.post(url, data=kwargs, headers=new_headers)
print("==========resp==========")
print(resp.text)
if resp.status_code >= 400:
raise Exception("获取 token 失败")
result = resp.json()
print("==========result==========")
print(json.dumps(result))
return result
oidc_func__turn_userinfo
import json
import requests
@DFF.API('用户信息获取接口')
def turn_userinfo(**kwargs):
'''
Test hello world function
'''
url = "https://passport.user.com/api/bff/v1.2/oauth2/userinfo"
print("--------turn_userinfo--------------")
headers = _DFF_HTTP_REQUEST.get("headers", {})
print(json.dumps(_DFF_HTTP_REQUEST))
print(json.dumps(kwargs))
new_headers = {
"content-type": headers.get("content-type"),
"authorization": headers.get("authorization")
}
resp = requests.get(url, headers=new_headers)
print("==========resp==========")
print(resp.text)
if resp.status_code >= 400:
raise Exception("获取 用户信息 失败")
result = resp.json()
print("==========result==========")
print(json.dumps(result))
# 针对吉利, 需要提取返回结果信息
result = result.get("data", {})
if not result.get("phone_number"):
result["phone_number"] = ""
name = result['email'].split('@')
ou_name = result['username']
if name:
ou_name = name[0]
result['ou_name'] = ou_name.lower()
result['username'] = result['ou_name']
print(result['ou_name'])
return result
在“管理”->“函数 API” 中创建 API,获取到接口访问方式:
- wellknow:https://private.func.com/api/v1/sync/sapi-z3QXUrBtNECW/s
- turn_token:https://private.func.com/api/v1/sync/sapi-gYaprJo5WPBq/s
- turn_userinfo:https://private.func.com/api/v1/sync/sapi-cldP8lHqnwc/s
配置观测云 Core 服务
在 Launcher 中进入命名空间 forethought-core > core,增加 IDCClientSet 配置。
------------oidc ----------------------
OIDCClientSet:
# OIDC Endpoints 配置地址,即完整的 `https://xxx.xxx.com/xx/.well-known/openid-configuration` 地址.
wellKnowURL: 'https://private.func.com/api/v1/sync/sapi-z3QXUrBtNECW/s'
# 由认证服务提供的 客户端ID
clientId: xxxxxxxx
# 客户端的 Secret key
clientSecret: xxxxxxx
# 认证方式,目前只支持 authorization_code
grantType: authorization_code
verify: false
# 获取 token 接口的认证方式 basic: 位于请求头中的 Authorization 中; post_body: 位于请求body中; 默认值为 basic.
fetchTokenVerifyMethod: basic
# 数据访问范围
#scope: "openid profile email address"
scope: "read"
# 认证服务器认证成功之后的回调地址
innerUrl: "{}://{}/oidc/callback"
# 认证服务认证成功并回调 DF 系统之后,DF系统拿到用户信息后跳转到前端中专页面的地址
frontUrl: "{}://{}/tomiddlepage?uuid={}"
# 从认证服务中获取到的账号信息 与 DF 系统账号的映射配置, 其中必填项为: username, email, exterId
mapping:
# 认证服务中,登录账号的用户名,必填,如果值不存在,则取 email
username: preferred_username
# 认证服务中,登录账号的邮箱,必填
email: email
# 认证服务中,登录账号的唯一标识, 必填; 此值应对应第三方认证服务中账号的唯一标识ID。
exterId: sub
# http请求配置设置,根据认证服务接口动态调整适配(目前只支持 userinfo 信息的获取)
requestSet:
userinfo:
# 用户信息数据来源, 可选值(accessToken: 表示从 access_token 的声明中获取; origin: 表示从第三方认证服务获取)
sourceMethod: origin
# 是否合并访问令牌声明中的数据, 当 sourceMethod=origin 时生效
mergeAccessTokenDeclaration: false
method: GET
# 认证方式, 可选值(bearer: HTTP Bearer认证; basic: HTTP基本认证), 默认为 bearer 方式
authMethod: bearer
# 可指定请求头, 未指定时默认
headers:
# (可选)额外指定的 url 请求参数, 覆盖更新
params:
# (可选)额外指定的 body 请求参数, 覆盖更新
body:
# 系统将根据 mapping 的配置将 url / body 中的参数名替换成目标接口期望的参数名(左侧为系统内置参数名,右侧为新的目标参数名)
mapping:
client_id: client_id
client_secret: client_secret
access_token: access_token
# 响应结果中 账号信息的主体路径; 以点号分割的字符串路径
responseInfoPath:
在 Launcher 中进入命名空间 forethought-webclient > frontNginx增加 OIDC 配置。
# =========OIDC协议 跳转相关配置开始=========
# 请求直接跳转至 Inner API 的接口 =========开始=========
# 这个地址是用于 第三方登录时的访问地址;可视情况自行变更,但 proxy_pass 对应的路由地址不可改
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;
}
# 这个地址是用于 第三方服务通过 OIDC 协议认证通过之后,回调本服务的当前地址;该地址与 【3.2.1】配置中 OIDCClientSet 配置项下的 innerUrl 配置直接关联;该地址变更时应与 innerUrl 同步变更; proxy_pass 对应值不可改
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协议 跳转相关配置结束=========
在 Launcher 中进入命名空间 forethought-webclient > frontWeb增加几个 url。
paasCustomSiteList: [
{"url": "https://private.guance.com/oidc/login", "label": "私有观测云"}
],
paasCustomLoginUrl: "https://passport.user.com/public/sp/slo/appid?redirect_url=https://private.guance.com/oidc/login",
paasCustomLoginInfo: [{ "iconUrl":"", "label": "OAuth2", "url": "https://passport.user.com/public/sp/slo/appid?redirect_url=https://private.guance.com/oidc/login" ,desc:"OAuth2"}]
总结
基于 Func 的自定义 OIDC 对接方案,为观测云部署版用户提供了一套灵活、可扩展的身份认证集成框架。通过 well_know、turn_token、turn_userinfo 三个核心函数的协同工作,该方案能够适配各类标准或非标准的身份提供商,完美解决账号归一化等复杂场景。