LDAP 自定义认证与 Func 属性加工(部署版)¶
部署版可以将 LDAP 认证交给客户维护的 Func 函数,也可以保留内置 LDAP 认证,仅由 Func 加工认证成功后的用户属性。两种方式均复用现有 LDAP 登录入口和 SSO 账号、工作空间及角色映射规则。
使用前,请确认 Studio 后端版本已支持 LDAPClientSet.authMode、LDAPClientSet.external 以及原生 Func 函数接口的响应格式。启用 secret 加密还需配套支持无品牌 v2 协议的 Studio 与 Func core;仅更新下载脚本不能使旧 Studio 支持 v2。现有部署保持 authMode: builtin(或不配置),且不启用非空 server.hosts 时,继续使用原单地址链路;无需为升级配置 Func 或 secret。
选择模式¶
authMode |
密码由谁验证 | Func 收到什么 | Func 成功时返回什么 |
|---|---|---|---|
builtin(默认) |
Studio 内置 LDAP | 不调用 Func | 不适用 |
builtin_transform |
Studio 内置 LDAP;失败时不调用 Func | 用户名、已通过认证的 userinfo,不含本次登录密码 |
用户属性的顶层补丁 |
external |
客户 Func | 用户名、密码 | 完整 userinfo |
builtin_transform 共用现有 LDAP 连接、TLS、搜索和映射配置,也可同时配置 多域控。它使用新认证执行链路;即使未配置 hosts,也应按多域控文档中的新链路要求检查 SIMPLE 用户密码验证、SAFE_SYNC 和启动环境。
external 不再连接 Studio 配置中的 LDAP 服务器,客户 Func 负责实际认证。Studio 仍使用 LDAPClientSet.mapping 解释返回属性;不要由函数返回 Studio token、账号 UUID 或角色 UUID 来代替用户信息。
两种外部模式按需选择,每次登录只调用 external.url 指定的函数。external 指向 authenticate 时不会自动再调用 transform;如需由客户函数同时完成认证和属性加工,可在认证成功后加工完整 userinfo 再返回。若保留 Studio 内置 LDAP 认证、仅自定义属性,请使用 builtin_transform 并将 URL 指向 transform。
准备 Func 脚本¶
以下示例包含标准 LDAP 密码验证和属性加工两个入口,可作为客户脚本的起点:
- 下载 core.py:LDAP 连接、协议编解码和属性校验。
- 下载 main.py:Func 函数入口,包含
authenticate(body="")和transform(body="")。 -
下载来源与校验信息。两份脚本与 Studio 实现同源;升级协议时应配套更新。
-
在客户可管理的 Func 环境中创建脚本集,例如
ldap_client_demo。 - 创建子脚本,ID 为
core,粘贴core.py;再创建 ID 为main的子脚本,粘贴main.py。main中的import __core as core引用同脚本集的core,不要改成从互联网下载代码。 - 确认 Func Python 环境已安装兼容版本的
ldap3、cryptography、requests。选择与 Func Python 版本兼容的依赖,不要覆盖运行环境中其他脚本的依赖;私有 CA 文件需挂载到 Func 执行容器。 - 配置下述 Func 环境变量,先发布
core,再发布main。Func 调用依赖已发布脚本,不能只保存草稿。 - 为所需函数创建同步函数 API:外部认证使用
ldap_client_demo__main.authenticate,属性加工使用ldap_client_demo__main.transform。函数参数名为body,类型为字符串。复制同步调用 URL,勿使用异步任务接口。
有关环境变量、脚本发布和函数 API,参见 Func 基本概念 与 编写和调用函数。
Func 环境变量¶
| 名称 | 说明 |
|---|---|
LDAP_CLIENT_DEMO_SECRET |
可不配置,默认空字符串。需要加密请求体时,与 Studio external.secret 配置相同的非空值;建议使用安全生成的随机共享值,不要将它写入脚本、URL 或日志。 |
LDAP_CLIENT_DEMO_CONFIG |
JSON 对象或 JSON 字符串,内容为 LDAPClientSet 内部的 LDAP 配置,不再套一层 LDAPClientSet。外部认证需要目录配置;仅属性加工也应配置与 Studio 一致的 mapping。 |
使用标准 LDAP 认证函数时,LDAP_CLIENT_DEMO_CONFIG 示例:
{
"server": {
"hosts": ["ldaps://ldap-a.example.com:636", "ldaps://ldap-b.example.com:636"],
"connect_timeout": 6,
"use_ssl": true,
"get_info": "SCHEMA",
"tls": {"validate": "CERT_REQUIRED", "ca_certs_file": "/config/ldap/ca.pem"}
},
"connection": {"authentication": "SIMPLE", "client_strategy": "SAFE_SYNC", "receive_timeout": 5},
"startTls": false,
"authTimeoutSeconds": 20,
"baseDN": "dc=example,dc=com",
"bindDN": "cn=studio-reader,ou=service,dc=example,dc=com",
"bindPassword": "<服务账号密码>",
"searchAttribute": "uid",
"personObjectClass": "inetOrgPerson",
"mapping": {"exterId": "uid", "username": "uid", "email": "mail", "name": "displayName"}
}
将示例替换为实际目录配置,保持身份字段与原部署一致。配置中包含 bindPassword 时,将整段 JSON 字符串以 Func 环境变量的 password 类型保存;脚本会解析该字符串,避免在普通 JSON 变量列表中回显服务密码。LDAP_CLIENT_DEMO_SECRET 也使用 password 类型。此配置使用与 Studio 多域控相同的有界切换规则,用户密码提交后不会自动换节点重试。仅属性加工时,可以只配置:
两个模式都使用 main.py 中的 _settings(),因此属性加工也必须提供有效 JSON 对象。不要从请求参数接收服务账号、LDAP 地址或 secret。
函数 API 的访问控制¶
示例的两个 @DFF.API 入口均设置 require_api_auth=False,创建函数接口时可以不绑定 Func API 认证。保留同步执行、20 秒超时和 cache_result=0;不要缓存密码认证结果。
Studio 固定发送下文的 kwargs.body JSON 请求,不附加自定义认证 Header,可直连上述未绑定额外 API 认证的 Func 同步入口:
仅在受控内网开放演示接口,限制来源和请求速率,关闭认证参数与请求体采集;传送真实密码时使用受信任 HTTPS。取消强制 Func API 认证不会跳过函数内部的 LDAP 密码验证,也不会关闭 Func 全局认证。secret 只控制请求体的可选加密,不替代 LDAP 验证、TLS 或网络访问控制。
如果已有函数接口绑定了 API 认证,更新脚本不会自动解除绑定;需要由接口管理员按实际接入策略处理。若仍保留要求额外 Header 的接口认证,应由客户部署的受控入口适配;这不是示例直连模式的必填步骤。有关可选配置,参见 Func API 认证。
无需增加 headersFile、keyFile 或其他 Studio 认证字段,也不要把 Func 的 MCP 编程地址或开发 Access-Token 当作登录业务入口。
在 Studio 中配置¶
按 应用配置说明 修改 Launcher forethought-core 的 Core 配置,并重启相关服务。
外部认证¶
LDAPClientSet:
enable: true
authMode: external
authTimeoutSeconds: 20
# 解释 Func 返回属性;必须与既有用户及 Func 配置保持一致。
mapping:
exterId: uid
username: uid
email: mail
name: displayName
external:
# authenticate 的完整同步 HTTP(S) 函数接口地址,必须可从 Studio 服务访问。
# 不是 LDAP 地址、Func 控制台页面或 MCP 编程地址;不接受重定向。
url: "https://func.example.com/ldap/authenticate"
# 可不配置;若启用,与 Func 环境变量中的值相同,不随请求传送。
secret: ""
# 可选,HTTPS 私有 CA 在 Studio 容器内的路径;空值使用系统信任。
caCertsFile: ""
external 模式下 Studio 不使用 server、connection、bindDN 等本地 LDAP 参数;可以保留原配置用于回退。实际目录配置由 Func 环境变量维护。
内置认证后加工属性¶
保留原有 server、connection、startTls、目录搜索与身份映射配置,仅将模式和外部入口改为:
# 以下为 LDAPClientSet 下需要合并的配置片段,不要用它覆盖整段 LDAP 配置。
LDAPClientSet:
authMode: builtin_transform
authTimeoutSeconds: 20
external:
# transform 函数的同步 HTTP(S) 接口地址。
url: "https://func.example.com/ldap/transform"
secret: ""
caCertsFile: ""
external.url 在两种外部模式下均为必填。旧 builtin 模式忽略该配置,保持默认空 URL 不影响现有部署。URL 不包含账号密码或片段;建议使用受信任 HTTPS。HTTP 不提供传输层保护,不应用于传送真实登录密码。
authTimeoutSeconds 为新链路认证及属性准备的总时限,整数 1~60 秒,默认 20 秒;不是每个环节分别计时,也不覆盖后续账号事务。Func 示例的执行超时为 20 秒,应结合 LDAP、网关和 Studio 的总时限配置,避免无界等待。Func 失败、超时或协议错误时本次登录失败,不降级到其他认证模式。
body 请求与返回协议¶
Func 入口的定义始终是 def authenticate(body="") 或 def transform(body="")。Studio 以 POST 发送 JSON,外层固定为:
外部认证的逻辑请求对象为:
双方都未配置 secret 时,body 是该对象序列化一次得到的 JSON 字符串,实际 HTTP 请求示例:
双方配置相同非空 secret 时,body 是形如 v2:... 的加密字符串。下载的 core.py 负责加解密,无需手工实现;只配置一侧、两侧不一致或密文校验失败均拒绝请求,不回退到明文。空格也属于 secret 内容,双方必须完全一致。
协议使用 HKDF-SHA256 派生 32 字节密钥,AES-256-GCM 加密;HKDF 的 salt 为空,密钥派生上下文为 ldap-forward/v2,AAD 为 ldap-forward/v2/request。传输格式为 v2: 加标准 Base64,编码内容依次为 12 字节随机 nonce、密文及 16 字节认证标签。仅加密请求中的 body,返回值仍是 JSON;该协议不提供请求过期或重放去重能力,仍需 HTTPS 和入口访问控制。
这里的 AES-GCM 认证标签用于校验加密请求体的完整性,不是额外的数字签名。Studio 不增加 sign、signature、签名 Header 或 AK/SK;客户无需准备签名密钥文件。双方未配置 secret 时,只透传 JSON 字符串,不附加签名或加密认证标签。
品牌无关协议: 加密只使用双方约定的 secret 和上述无品牌协议参数,不读取产品名称、品牌标识或品牌域名。页面文案和帮助地址按品牌替换,不参与密钥派生或密文校验。
属性加工的逻辑请求对象为:
{
"username": "user1",
"userinfo": {"uid": ["user1"], "mail": ["user1@example.com"], "memberOf": ["cn=readers,dc=example,dc=com"]}
}
其中不发送本次登录密码。函数返回业务对象即可,外部认证成功示例:
return {"status": "success", "userinfo": {
"uid": ["user1"],
"mail": ["user1@example.com"],
"displayName": ["示例用户"]
}}
密码错误或不允许登录返回 {"status": "denied"};目录、配置或处理异常返回 {"status": "error"}。不要附带密码、异常堆栈或内部配置。原生 Func 同步 API 直接返回该业务对象,函数本身无需再套 ok、data。Studio 也兼容旧入口使用以下包装:
{"ok": true, "data": {"result": {"status": "success", "userinfo": {"uid": ["user1"], "mail": ["user1@example.com"]}}}}
Studio 要求 HTTP 200,并验证原生业务对象或上述包装中的业务结果;只要外层出现 ok,就严格校验包装,外层失败或畸形时不回退原生成功。异步任务 ID、重定向、HTML 页面或任意文本都不是有效认证结果。解密后的请求对象、用户属性对象以及 Func 业务结果各最多 256 KiB,HTTP 响应体最多 1 MiB,不要返回大批无关属性。
修改认证或属性加工逻辑¶
自定义认证¶
main.py 的 authenticate 默认调用同源 LDAP 执行器验证真实用户名和密码。若对接其他身份服务,应在 _run() 的认证分支替换实际验证步骤:先调用客户可信身份服务并确认密码或认证凭证有效,再构造 userinfo;认证拒绝返回 denied,服务异常返回 error。不要仅凭用户名存在就返回成功。
userinfo 的字段名由 mapping 决定。例如 mapping.exterId: uid 时必须返回非空 uid,还必须至少有可映射的用户名或邮箱。身份字段可以是字符串或单元素字符串数组,不能返回多个候选身份。其他授权所需组属性应沿用现有结构。
接入多个独立身份源时,由客户函数确定身份源并验证身份;必须保证 exterId 在接入范围内全局稳定且唯一。同名或同邮箱不代表同一人,不会自动合并现有 LDAP/OIDC 账号。迁移现有用户前应核对身份来源和已有绑定,避免创建重复账号或身份冲突。
属性加工¶
先为 ldap_client_demo__main.transform 创建同步 HTTP 接口,在 Studio 配置 authMode: builtin_transform,将 external.url 指向该接口;共用原 LDAP 连接、搜索、TLS 和 mapping。如启用请求体加密,继续使用与 Func 相同的 secret。
下载示例的 patch = {} 默认不修改任何属性。客户在 main.py 的 transform_only 分支编写实际加工规则,修改后重新发布 main,例如:
# 示例:实际值应由可信业务规则或属性服务计算,不接收客户端任意指定。
patch = {"displayName": ["业务显示名"]}
core.merge_transform(original, patch, config["mapping"])
return {"status": "success", "userinfo": patch}
同时在 Studio 与 Func 的 mapping 中配置 name: displayName,其余原映射保持一致。空补丁 {} 表示保留原属性;补丁按顶层字段合并,不是递归合并。未返回的字段保留,显式返回空值或空数组可能清空字段并改变角色映射结果。
属性加工不得改变 mapping.exterId 对应的身份值;修改其他属性也应先验证工作空间与角色映射。对嵌套对象更新时,应返回该字段所需的完整内容。不要返回密码、token、私钥或共享 secret 等凭证字段。
安全验证与回退¶
旧加密协议仅用于测试环境。切换至 v2 时,先暂停相关测试认证并等待在途请求及 Func 队列排空,先发布新版 core,再更新 Studio,验证后恢复。双端协议版本必须一致;不自动接受旧密文,也不回退明文。未配置 secret 的 JSON 透传不受影响。
上线前先使用合成数据和专用测试账号验证:
- 两侧 secret 都为空时,Func 收到 JSON 字符串;都配置相同 secret 时收到密文且认证成功;两侧不一致时失败。
- 正确密码成功,错误密码拒绝;Func 停止、超时、响应格式错误时拒绝,不切回旧认证。
builtin_transform的错误密码请求不进入 Func;空补丁保留身份及组属性,修改身份值的补丁被拒绝。- 现有用户仍进入原账号,账号资料、工作空间和角色符合预期;仍按现有策略完成后续登录与 MFA。
- 检查 Studio、Func、网关、任务记录和 APM 的采集策略,确保认证请求参数、密码、secret 不进入日志或监控。示例入口不打印这些内容,但不自动关闭平台的参数与任务采集;加密也不代替该项检查。普通用户属性也应按最小必要范围返回。
排查失败时,先核对 Studio 到网关/Func 的 DNS、网络和 CA,再核对 Func API 的同步方式、访问认证、两侧 secret、映射与返回结构。查看错误类别和 trace 信息,勿通过打印完整请求体或配置排查。
需要回退时,恢复升级前的 authMode: builtin、原单地址 server.host 与原 LDAP 配置,清空或删除 server.hosts,重启相关服务。不必删除 Func 脚本。回退不自动恢复加工过的属性或合并已产生的账号,须单独核对已有账号与授权状态。