Skip to content

认证提供商

配置认证提供商以接入外部 OAuth 2.0 / OpenID Connect 身份源或使用 APL 编写自定义单点登录插件。

概述

💡 通过创建认证提供商,管理员可以将平台与基于 OAuth 2.0 或 OpenID Connect 协议的第三方身份提供商(IdP)打通。这允许员工使用其已有的外部账号一键登录平台,实现身份统一管理。针对非标准化的第三方协议,平台支持通过编写 APL 插件代码来实现对接。

开始之前

IMPORTANT

  • 角色权限:您必须拥有超级管理员(System Administrator)权限。
  • 信息准备:配置前需先在第三方身份认证平台(如 Okta、企业微信)创建应用,并获取其发行的 Client ID(Consumer Key)和 Client Secret(Consumer Secret)。

方式一:配置标准 OpenID Connect 认证提供商

  1. 前往 系统管理 > 企业设置 > 安全认证 > 认证提供商

  2. 点击 新建,在“提供商类型”中选择 OpenID Connect

  3. 配置以下基础与接口参数:

    • 名称:输入认证提供商的名称(最长 50 个字符)。
    • URL后缀:设置唯一的后缀(最长 50 个字符,仅能包含字母、数字和下划线,必须以字母开头,不包含空格,不能以下划线结尾且不能有连续的两个下划线)。该后缀将用于在系统底部生成唯一的单点登录入口地址。
    • Consumer Key:填写第三方身份验证提供方发行的 Client ID。
    • Consumer Secret:填写第三方身份验证提供方发行的 Client Secret。
    • Token接口地址:获取第三方令牌的 API 请求地址。
    • 用户信息接口地址:第三方返回用户基础属性的 API 请求地址。
    • 授权范围:允许获取的用户信息权限范围描述(不填则默认为 openid)。
  4. 配置用户映射验证关系:

    • 外部用户身份认证字段:从第三方返回的用户 JSON 属性中,选择一个唯一属性(如 emailemployee_number)作为外部标识。
    • 纷享用户身份认证字段:从平台系统的人员对象中,选择一个字段的 API 属性名(API Name,如 employee_number)作为匹配键。

    NOTE

    当员工使用单点登录时,平台自动调用接口获取外部用户的认证值,并在本地人员表中检索是否存在“纷享用户身份认证字段”与其完全相等的员工账号。如果匹配成功,则代表验证通过并成功登录。

    • 自定义登出地址:指定用户登出时需要跳转的第三方注销 API 地址。
  5. 点击 保存。系统会在页面下方自动生成三条只读地址,请复制并填回您的第三方 IdP 控制台中:

    • 单点登录地址:员工登录系统的专用入口。
    • 回调地址:第三方登录成功后,安全重定向回平台的地址。
    • 登出地址:退出登录时的注销地址。

方式二:使用 APL 编写自定义认证提供商插件

针对非标准协议或包含特殊加密要求的身份认证,管理员可以使用 APL(Groovy 语言)自定义逻辑:

  1. 前往 系统管理 > 企业设置 > 安全认证 > 认证提供商
  2. 点击 新建,类型选择 自定义插件
  3. 在代码编辑器中编写实现类。该类必须继承 AuthProviderPlugin 接口并实现其核心方法。
  4. APL 插件代码结构示例如下:
groovy
/**
 * @type classes
 * @returntype
 * @namespace custom_oauth_protocol
 */
class AAA implements AuthProviderPlugin {

    /**
     * 重定向跳转至第三方 IdP 的登录地址
     * redirect_uri 为系统生成的回调地址
     * client_id 为第三方发行的 client id
     */
    @Override
    String authorizeUrl() {
        String authorizeUrl = "https://example.com/oauth2/auth"
        final Map<String, String> param = [
                "response_type": "code",
                "response_mode": "query",
                "scope"        : "openid",
                "redirect_uri" : "https://example.my.fxiaoke.com/oauth2/sp/callback/AAA",
                "client_id"    : "client-id-placeholder"
        ]

        return appendUrl(authorizeUrl, param)
    }

    /**
     * 第三方回调处理
     * @param params 登录成功回调时携带的参数(如授权码 code)
     * @return 返回匹配成功的纷享内部人员主键 ID
     */
    @Override
    Integer callback(Map<String, List<String>> params) {
        String token = getAccessToken(params)
        log.info(token)

        Integer empId = getEmployeeId(token)
        log.info(empId)

        return empId;
    }

    /**
     * 单点登出重定向地址
     * @return 返回 null 时,系统默认跳转回登录页
     */
    @Override
    String logoffUrl() {
        return "https://example.com/oauth2/logoff"
    }

    private static String appendUrl(String url, Map<String, String> data) {
        String paramStr = ''
        data.each { String key, value -> paramStr += key + "=" + value + "&" }
        paramStr = paramStr.substring(0, paramStr.length() - 1)
        return url.contains("?") ? (url + "&" + paramStr) : (url + "?" + paramStr);
    }

    private String getAccessToken(Map<String, List<String>> parameterMap) {
        FormBody body = FormBody.builder()
                .field("client_id", "client-id-placeholder")
                .field("scope", "openid")
                .field("redirect_uri", "https://example.my.fxiaoke.com/oauth2/sp/callback/AAA")
                .field("client_secret", "client-secret-placeholder")
                .field("code", parameterMap["code"].get(0))
                .build()

        Request request = Request.builder()
                .method("POST")
                .url("https://example.com/oauth2/token")
                .timeout(7000)
                .retryCount(0)
                .body(body)
                .build()

        HttpResult o = (HttpResult) http.execute(request).getData()
        log.info(o)
        return o.content["access_token"].toString()
    }

    Integer getEmployeeId(String accessToken) {
        HttpResult o = (HttpResult) http.get("https://example.com/oauth2/userinfo", ["Authorization": accessToken]).getData();
        String user = o.content["sub"].toString();
        log.info(user)
        QueryResult data = (QueryResult) object.find("PersonnelObj",
                [["employee_number": user]],
                2,
                0
        ).getData()
        List<Map> dataList = data.dataList

        if (dataList.size() <= 0) {
            message.throwErrorMessage("找不到对应的用户, user:" + user)
        }

        if (dataList.size() > 1) {
            message.throwErrorMessage("找到多个对应的用户, user:" + user)
        }

        return dataList[0]["user_id"].toString() as Integer
    }

    // debug 测试入口
    static void main(String[] args) {

    }
}
  1. 保存并编译代码,发布后将其与自定义认证提供商绑定生效。

相关主题