基于 Func 自定义 OIDC 对接观测云最佳实践

    banner.png

    概述

    在企业可观测性平台的建设过程中,身份认证的统一管理是保障平台安全和提升管理效率的关键环节。观测云支持基于 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_knowturn_tokenturn_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 作为参数传递给该地址。

    阶段二:用户登录跳转

    1. 用户访问观测云 SSO 登录页,选择对应 IdP 入口
    2. 观测云根据 well_know 返回的 authorization_endpoint,将用户重定向至 Func 的 turn_token 函数(携带回调参数)
    3. 观测云实际登录流程会自动完成 IdP 认证页面的跳转和 code 的回传

    阶段三:Token 换取与用户信息获取

    1. 用户完成 IdP 侧认证后,携带授权码 code 回调至观测云
    2. 观测云调用 Func 的 turn_token 函数,传入 code 参数
    3. turn_token 函数内部使用 code 向 IdP 的 Token 端点换取 Access Token,并返回给观测云
    4. 观测云获取到 Access Token 后,调用 Func 的 turn_userinfo 函数,传入 Access Token
    5. turn_userinfo 函数使用 Access Token 向 IdP 的用户信息端点获取用户详情
    6. 对用户信息进行字段映射和归一化处理后,返回给观测云
    7. 观测云根据返回的用户信息创建或更新用户会话

    配置步骤

    前置准备

    在开始配置前,请确认以下事项:

    准备项 说明
    观测云部署版 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,获取到接口访问方式:

    配置观测云 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_knowturn_tokenturn_userinfo 三个核心函数的协同工作,该方案能够适配各类标准或非标准的身份提供商,完美解决账号归一化等复杂场景。

    联系我们

    加入社区

    微信扫码
    加入官方交流群

    立即体验

    在线开通,按量计费,真正的云服务!

    立即开始

    选择观测云版本

    代码托管平台