认证源插件

最后更新:2021-12-02

1 背景


政企通常希望有自己的个性登录方式,比如对接到自己的4A,开发者可以按IDP的规范和文档, 开发出对应的jar包, 上传到IDP后台后, 形成插件, 扩展对应的MFA认证能力。

对于一个IT部门或者运营部门,购买IDaaS后想将原有身份认证能力整合到新平台中来,或者在业务扩展时想要接入新的认证方式,需要有一种快速集成和上线的流程;对于管理员来说,认证集成后最好无需他人介入也能将能力挂载到系统中直接使用;对于开发者来说,除了对接认证侧,在IDaaS平台中有一套认证交互的对接标准,能将认证能力集成接入的工作效率提高很多。
image.png
IDaaS的认证源插件化集成可以将不同角色串联起来形成一个闭环,衔接了客户在从一个需求,产出,转化为使用的整个过程,是完成用户想要一个认证能力到用户最后使用它的一次高效的需求转化能力。

2 功能清单

插件依赖与IDaas主项目,能使用以下功能:
spring ioc, spring mvc ,hibernate
能够动态上传插件,上传后不需要重启,立即生效

3 资源下载

DEMO: 点击下载

4 准备工作

确定插件名称,插件ID

  • 插件名称:不能超过10字符,简单明了,如:ldap认证源

  • 插件ID:只能由字母,数字与下画线(_)构成,唯一,且只能以字母开头,如:ldap001 或 ldap_001

  • 插件ID不能重复

这里以实现对接一个Ldap认证源插件为例,插件名称为LDAP,插件ID为: plugin_ldap

5 快速实现LDAP认证源插件

5.1 简介

这里以LDAP系统认证为例,实现一个认证源插件,提供一个登录页,前往LDAP系统认证流程;
image.png

5.1 下载DEMO项目

下载地址参考3资源下载章节

5.2 修改 pom.xml

打开 pom.xml,修改 artifactId 为 auth-plugin-ldap,此处要求格式必须为 auth-{ID}。
修改 description 为我们需要的描述,如 LDAP Module。
修改version 为我们的版本号, 如1.0.0

5.3 修改包名

Demo 项目包名为 com.idsmanager.idp.auth.plugin.demo​
我们需要修改为 com.idsmanager.idp.auth.plugin.ldap
注意插件的包名必须以com.idsmanager.idp.auth.plugin开头
修改包名后 pom.xml 中有两处出现了包名的地方,需要手动确认是跟着修改了的。
image.png

5.4 修改静态资源目录名

Demo 项目插件ID为 plugin_demo,因此目录名叫 plugin_demo,我们需要修改为 plugin_ldap
image.png

5.5 修改类名

我们需要将 DemoAuthenticate 修改为 LdapAuthenticate,修改后需要到 pom.xml 确认有两处出现了该名字的地方跟着修改到了.
将 DemoAuthenticateAPIController 修改为 LdapAuthenticateAPIController
将 DemoService 修改为 LdapService,
将 DemoServiceImpl 修改为 LdapServiceImpl
修改后的工程结构如下:
image.png

5.6 修改LdapAuthenticate

LdapAuthenticate实现了IDPAuthenticateAdapter,里面定义了插件的基础信息
IDPAuthenticateAdapter类定义请参考7.1
这里根据需要修改插件名,插件ID,描述,图标
修改插件名为: plugin_ldap
修改插件名为: LDAP
修改插件描述为: 使用LDAP认证
修改后的代码如下:

public class LdapAuthenticate extends IDPAuthenticateAdapter {
    private static final Logger LOG = LoggerFactory.getLogger(LdapAuthenticate.class);
    public static final String NAME = "LDAP";
    public static final String AUTH_ID = "plugin_ldap";
    private byte[] logoAsBytes;
    public LdapAuthenticate() {
    }
    public String name() {
        return NAME;
    }
    @Override
    public String description() {
        return "使用 LDAP 认证";
    }
    public String authId() {
        return AUTH_ID;
    }
    @Override
    public boolean customLogin() {
        return true;
    }
    @Override
    public boolean plugin() {
        return true;
    }
    @Override
    public long pluginVersion() {
        return 1;
    }
    @Override
    public long dependencyMinCoreVersion() {
        return 1;
    }
    /**
     * 设定认证源支持的终端设备,可选 有:
     * PC_BROWSER  ,  PC浏览器
     * MOBILE_H5  , 移动端H5
     * <p>
     * 默认  PC_BROWSER;具体由认证源支持情况决定
     * (如扫码类,证书类不能在移动端H5上使用)
     *
     * @return ClientDevice List
     * @since 1.1
     */
    @Override
    public List<ClientDevice> supportClientDevices() {
        return Arrays.asList(
                ClientDevice.PC_BROWSER
        );
    }
    @Override
    public String loginUrl(String enterpriseAuthId) {
        return "/public/api/authenticate/" + AUTH_ID + "/login_" + enterpriseAuthId;
    }
    public String enableApplicationUrl(String enterpriseAuthId, boolean enable) {
        return "/enterprise/authenticate/" + AUTH_ID + "/" + (enable ? "enable_" : "disable_") + enterpriseAuthId;
    }
    public String detailsUrl(String enterpriseAuthId) {
        return "/enterprise/authenticate/" + AUTH_ID + "/details_" + enterpriseAuthId;
    }
    public String configUrl(String enterpriseId) {
        return "/enterprise/authenticate/" + AUTH_ID + "/config_" + enterpriseId;
    }
    public byte[] logoAsBytes() throws IOException {
        if (this.logoAsBytes == null) {
            this.logoAsBytes = IOUtils.toByteArray(this.getClass().getClassLoader().getResourceAsStream("demo.png"));
            LOG.debug("[{}]- Initialed Authenticate logo from: demo.png", RIDHolder.id());
        }
        return this.logoAsBytes;
    }
    @Override
    public String bindingUrl(String enterpriseAuthId) {
        return "/enduser/authenticate/" + AUTH_ID + "/binding_" + enterpriseAuthId;
    }
    @Override
    public String unBoundUrl(String enterpriseAuthId) {
        return "/enduser/authenticate/" + AUTH_ID + "/unbond_" + enterpriseAuthId;
    }
    @Override
    public String modifyUrl(String enterpriseAuthId) {
        return "/enterprise/authenticate/" + AUTH_ID + "/modify_" + enterpriseAuthId;
    }
}

5.7 修改前端映射文件

我们需要修改 src/resources/plugin_ldap/schema 目录中 3 个 json 文件,
plus.json 为添加认证源时渲染前端页面使用,
modify.json 为修改认证源时渲染使用,
details.json 为查看认证源时渲染使用。
plus.json 和 modify.json 通常内容相同。
按需修改以上文件
如何定义UI Schema文件中的内容请参考UI Schema规范章节内容。

5.8 修改前端登录页面

修改 src/resources/static/index.html文件
修改其中的plugin_demo修改为plugin_ldap
image.png

5.9 修改登录接口

1.5.8登录页可以看见请求了一个登录接口
该接口在LdapAuthenticateAPIController中实现,接受用户名/密码,校验身份
Demo项目默认接受这4个参数:

字段

说明

username

用户名

password

密码

enterpriseAuthId

认证源ID,必传,

traceId

认证流程链路ID,必传

可以根据需要修改参数,比如加上验证码,去掉用户名密码,改为扫码登录,或者跳转其他地址认证

该接口具体实现在 com.idsmanager.idp.auth.plugin.ldap.service.buiness.LoginHandler中,实现如下:
如下图第8行,这里模拟请求Ldap系统,
判断用户名是不是admin,密码是不是password,失败则报错返回,成功则修改认证信息,并跳转IDaas中获取的链接

public LoginResult handle() {
    final IDPAdapterTraceIDDto traceIDDto = authenticateRepository.findIDPAdapterTraceIDDto(dto.getTraceId());
    if (traceIDDto == null) {
        LOG.warn("[{}]- Not found the traceId[{}]", RIDHolder.id(), dto.getTraceId());
        return new LoginResult("traceId.error");
    }
    if (!"admin".equals(dto.getUsername()) || !"password".equals(dto.getPassword())) {
        LOG.debug("[{}]- Username[{}] or password[length={}] error", RIDHolder.id(), dto.getUsername(), StringUtils.length(dto.getPassword()));
        return new LoginResult("username.or.password.error");
    }
    traceIDDto.setAuthSuccess(true);
    traceIDDto.setOpenId(dto.getUsername());
    traceIDDto.setUsername(dto.getUsername());
    traceIDDto.setMail(dto.getUsername() + "@idsmanager.com");
    authenticateRepository.updateIDPAdapterTraceIDDto(traceIDDto);
    final String frontendAdapterAddress = authenticateRepository.frontendAdapterAddress();
    final LoginResult loginResult = new LoginResult();
    loginResult.setSuccess(true);
    loginResult.setFrontendAdapterAddress(frontendAdapterAddress);
    LOG.debug("[{}]- Username[{}] login success", RIDHolder.id(), dto.getUsername());
    return loginResult;
}

关键类说明:
IDPAdapterTraceIDDto 全局认证链路类,该类存放traceId和认证结果的关联关系
EnterpriseAuthenticateRepository IDaas提供的工具类方法,定义参考2.6.2
具体参考2.6

5.10 打包

通过maven打包即可

 mvn clean package

打包完毕在target目录有打包完毕的jar文件
其中: xxxx-plugin.jar为插件文件
这里生成的插件文件为: auth-plugin-ldap-1.0.0-jar-with-plugin..jar

5.11 上传插件

在IDaas管理后台点击 其它管理/插件管理/认证源插件 点击上传认证源插件,选择刚刚打包完毕的jar
image.png
检查插件信息无误后点击‘确认上传’按钮,稍等片刻,上传完成即可,若有异常或失败会提示相关信息。

注意:若已经有相同的插件存在,则会检查版本号,若版本号变大则会覆盖已有的插件(即插件升级)。

5.12 使用插件

在IDaas管理后台点击 认证/认证源 点击添加认证源,选择刚刚上传的认证源插件(LDAP)
右侧弹出添加页面,这里和5.7定义的内容一致,
填写表单信息:

  • 用户关联方式: 当前认证源中的用户标识与本系统用户数据的关联方式

    • 自动关联: 认证用户的标识自动与本系统用户进行关联

    • 手动关联: 输入本系统账号密码与认证用户进行手动关联

    • 自动创建: 用户认证成功后自动在本系统创建对应新用户

  • 是否显示: 是否显示在登录页

根据需要选择自己的用户关联方式,本文演示自动关联
image.png
点击提交,创建认证源.
列表中出现刚刚添加的认证源,修改状态为开启
image.png

5.13 使用Ldap认证源登录

前往IDAAS登录页,此时登录页面下方出现了刚刚添加的认证源
image.png
点击选择该认证源,跳转到认证源中的登录界面,输入账户密码,点击登录

6 插件扩展接口

认证源 Core 中还提供了大量接口可供实现,用于增强认证源插件的功能。具体列表如下:

AdapterPluginExtension

所有拓展接口的父接口

AdapterLoadDetailsFormExtension

管理员查看认证源时加载表单回调

AdapterLoadFormExtension

管理员添加认证源时加载表单回调

AdapterLoadModifyFormExtension

管理员修改认证源时加载表单回调

AdapterPluginLoadExtension

加载、卸载认证源插件时回调函数

AdapterPluginStateExtension

管理员启用、禁用认证源插件时回调函数

AdapterSaveFormExtension

管理员保存、修改、删除认证源时回调函数

AdapterSaveModifyFormValidateExtension

管理员修改认证源时校验表单是否正确

AdapterSavePlusFormValidateExtension

管理员添加认证源时校验表单是否正确

AdapterSchemaExtension

管理员添加认证源、修改认证源、查看认证源详情时加载认证源 schema 回调函数

AdapterStateExtension

管理员启用、禁用认证源时回调函数

7 相关类说明

7.1 IDPAuthenticateAdapter

认证源插件必须实现该类,定义插件的一些基础信息,一些基础方法定义如下

方法名

说明

name()

插件名, 如 Demo

description()

插件描述, 如 使用 Demo 插件扫码登录

authId()

插件ID, 如 plugin_demo

customLogin()

是否为自定义登录,无特殊情况,均返回 true

plugin()

是否为插件,必须返回 true

pluginVersion()

插件内部版本,从 1 开始编号,每一次修改,该值均需要+1

dependencyMinCoreVersion()

依赖的认证源Core内部版本。
认证源Core工程同样有自己的内部版本,记录在 com.idsmanager.idp.auth.core.Version#AUTH_CORE_VERSION。
每次修改Core工程,该值均会加1。
插件中使用的Core中的方法、类等,若有注释 @since,则表明该方法/类是从@since后写的版本号开始才有的方法/类。若没有该注释,则表明该方法/类是从第 1 个版本开始就有的。
本函数必须返回使用的有 @since 注释的最大的值。如使用了五个core中的
函数,其中两个有标记@since注释。一个为 @since 2,一个为 @since 5,则本函数需要返回5。

loginUrl()

历史兼容问题,返回任意字符串即可。

enableApplicationUrl()

历史兼容问题,返回任意字符串即可。

detailsUrl()

历史兼容问题,返回任意字符串即可。

configUrl()

历史兼容问题,返回任意字符串即可。

logoAsBytes()

返回认证源插件的logo图标。

bindingUrl()

历史兼容问题,返回任意字符串即可。

unBoundUrl()

历史兼容问题,返回任意字符串即可。

modifyUrl()

历史兼容问题,返回任意字符串即可。

7.2 EnterpriseAuthenticateRepository

是插件中与IDP4通信的主要类,许多的操作最终通过此类完成,比如保存数据,保存账户关联信息等。
具体查看JavaDoc文档说明(章节3资源下载)

8 持久化

在插件中有两种方式可解决持久化需求

8.1 简单持久化

在认证源 Core 的 EnterpriseAuthenticateRepository 类中提供了 5 个函数可满足简单的持久化需求。

  IDPAdapterAuthenticateStoreDataDto findStoreDataByEnterpriseAuthenticateUuid(String enterpriseAuthenticateUuid, String key);
  IDPAdapterAuthenticateStoreDataDto findStoreDataByEnterpriseAuthId(String enterpriseAuthId, String key);
  List<IDPAdapterAuthenticateStoreDataDto> findStoreDataByEnterpriseAuthId(String enterpriseAuthId);
  IDPAdapterAuthenticateStoreDataDto updateStoreDataByEnterpriseAuthenticateUuid(IDPAdapterAuthenticateStoreDataDto storeDataDto);
  Boolean removeIDPAdapterAuthenticateStoreDataDto(IDPAdapterAuthenticateStoreDataDto storeDataDto);

8.2 自己操作数据库

对于复杂的持久化需求,可自己按照 hibernate 文档操作数据库。

/*
 * Copyright (c) 2016 BeiJing JZYT Technology Co. Ltd
 * www.idsmanager.com
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * BeiJing JZYT Technology Co. Ltd ("Confidential Information").
 * You shall not disclose such Confidential Information and shall use
 * it only in accordance with the terms of the license agreement you
 * entered into with BeiJing JZYT Technology Co. Ltd.
 */
package com.idsmanager.idp.auth.plugin.github.enterprise;
import com.idsmanager.micro.commons.domain.AbstractJpaDomain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
/**
 * 2020/11/12
 *
 * @author ilanyu
 */
@Entity
@Table(name = "example")
public class Example extends AbstractJpaDomain {
    private static final long serialVersionUID = 879615427616556444L;
    @Column(name = "username")
    private String username;
    @Column(name = "password")
    private String password;
    public Example() {
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public String toString() {
        return "Example{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                "} " + super.toString();
    }
}

若自己操作数据库,注意需要在 /src/main/resources/databases 目录中存在数据库初始化语句,用于初始化数据库,或者自己实现 AdapterPluginLoadExtension 接口,初始化数据库。
image.png
注意:若新版本插件中有SQL更新,则需要在database目录中新建 idp-plugin-{version}.ddl文件(如idp-plugin-2.ddl)并将新的完整SQL放在此文件中,新安装升级后插件时会执行该文件。并在database目录中新建 idp-plugin-{old_version}-{version}.ddl文件,并将增量SQL放到该文件中,在IDP中升级插件时会执行该文件。如IDP中已有阿里邮箱插件,版本号为1,现在开发了新版本,版本号为2,则需要创建 idp-plugin-1-2.ddl 文件,内容为增量SQL语句,在升级时IDP将会执行 idp-plugin-1-2.ddl 文件。

9 用户关联机制

根据过去的开发经验,大多数认证源都需要将 IDaaS 的用户和第三方系统的用户进行关联,因此,我们将关联 IDaaS 账户和第三方系统用户这个流程抽出来放到了认证源通用流程中。
账户关联一共有 3 种关联方式,为 自动关联、手动关联、自动创建,另外也可以设置为不进行账户关联。

9.1 自动关联

在 plus.json 和 modify.json 中 userLinkingType 的 association 中若有 AUTO_ASSOCIATION,则在管理员添加/修改认证源时将会展示自动关联选项。association 下方的 associationDataDictionary 代表自动关联时是否允许使用数据字典进行关联。管理员添加认证源时,若用户关联方式选择了自动关联,则需要进一步选择是根据账户名称(即用户名)还是手机号、邮箱,或者是某个数据字典进行关联。
开发 handle_callback 接口时,IDPAdapterTraceIDDto 对象能够设置 OpenId、Username、Mail、Phone、Custom 字段,这 5 个字段分别代表用户唯一标识,如用户ID;用户名;用户邮箱;用户手机号;用户数据字典。这 5 个字段中 OpenId 是必须要设置的,设置了 Username 字段,则管理员在添加认证源时支持使用用户名进行关联,设置了 Mail 字段,则支持使用邮箱进行关联,设置了 Phone 字段则支持使用手机号进行关联,设置了 Custom 字段,则支持使用数据字典进行关联。注意,若没有设置 Phone 字段,但管理员选择了使用手机号进行关联,那么在用户实际使用时,将会变成让用户手动进行关联。另外,OpenId字段要求必须是业务系统那边用户的无法修改的唯一标识。

9.2 手动关联

在 plus.json 和 modify.json 中 userLinkingType 的 association 中若有 BIND_ASSOCIATION,则在管理员添加/修改认证源时将会展示手动关联选项。若选择手动关联,则用户在使用认证源登录后,若系统检测到没有关联 IDaaS 用户,则会展示一个 IDaaS 系统的用户名、密码输入界面,用户需要在该界面输入 IDaaS 系统的用户名、密码,进行关联。

9.3 自动创建

在 plus.json 和 modify.json 中 userLinkingType 的 association 中若有 AUTO_CREATE,则在管理员添加/修改认证源时将会展示自动创建选项。
用户使用认证源登录时,将会根据 OpenId 检测该用户是否已关联,若没有关联,则使用 IDPAdapterTraceIDDto 对象中设置的用户信息创建用户,此处注意 OpenId、Username 必须要设置,邮箱、手机号至少需要填一个,否则无法创建成功。创建用户时,若 IDaaS 中出现了相同用户名,或相同手机号、邮箱等,将会创建失败。创建失败的情况下,将会提示用户需要手动关联。

9.3 不关联

部分认证源不需要使用用户关联,如短信认证源等。若不需要使用关联,则应当在 plus.json 和 modify.json 中 userLinkingType 的 association 中设置 NO_ASSOCIATION,同时不应出现其余 3 种关联机制的设置项。在认证源插件认为不需要进行账户关联时,插件中需要手动调用 EnterpriseAuthenticateRepository 中的 boundUserAuthentication 函数,提前将用户进行关联,否则在用户登录时 IDaaS 将会提示错误。

10 UI SChema规范

具体请查看 IDaaS插件前端集成文档

11 FAQ

11.1 能否支持认证后跳转到指定地址,如认证后立刻进行发起SSO?

答:支持,EnterpriseAuthenticateRepository中updateIDPAdapterTraceIDDto方法要求的参数IDPAdapterTraceIDDto,有一个属性为redirectUri,在调用updateIDPAdapterTraceIDDto前,给redirectUri赋值,将会在认证完成后通过GET方法携带token请求redirectUri的地址。

11.2 部分认证协议无法按照设计的认证流程进行,不能让用户在IDaaS登录界面点击认证源图标,如何处理?

答:不通过点击认证源图标的方式进入认证流程,将会缺少 traceId 参数,可调用 IDPAuthenticateService.generateTraceID 方法由认证源生成 traceId,认证通过后一样调用EnterpriseAuthenticateRepository.updateIDPAdapterTraceIDDto 即可。

11.3 若需要设置认证源只能在PC浏览器或移动端H5页面上使用,如何配置?

答:此功能需要在IDaaS v4.13及之后版本才能使用。具体是在认证源实现类(示例中的 DemoAuthenticate.java)中实现 supportClientDevices() 方法,返回支持的设备类型即可。
20210716170756.jpg

11.4 如何升级插件

修改插件实现类XXXAuthenticate.java类中 pluginVersion()方法返回值,如下图:
7.jpg

pluginVersion()方法默认的返回值为1,修改为比1大(如 2)即可。详细可查看方法注释说明。

11.5 增加(或修改)新版本插件的功能并重新打包

在新版本分支上增加或修改新的功能,开发完成后重新打包,并重新上传新的插件jar文件,IDP系统会根据插件版本号判断(即新插件的版本号大于已有的版本号)是新插件,会将旧插件覆盖,并使用新插件功能。

注意:若新版本插件中有SQL更新,则需要在database目录中新建 idp-plugin-{version}.ddl文件(如idp-plugin-2.ddl)并将新的完整SQL放在此文件中,新安装升级后插件时会执行该文件。并在database目录中新建 idp-plugin-{old_version}-{version}.ddl文件,并将增量SQL放到该文件中,在IDP中升级插件时会执行该文件。如IDP中已有阿里邮箱插件,版本号为1,现在开发了新版本,版本号为2,则需要创建 idp-plugin-1-2.ddl 文件,内容为增量SQL语句,在升级时IDP将会执行 idp-plugin-1-2.ddl 文件。