应用模板插件

最后更新:2021-12-02

1 背景

政企通常希望有自己的个性应用, 开发者可以按IDP的规范和文档, 开发出对应的jar包, 上传到IDP后台后, 形成插件, 扩展对应的SSO能力。

2 功能清单

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

3 资源下载

DEMO: 点击下载

4 准备工作

确定插件名称,插件ID

  • 插件名称:不能超过10字符,简单明了,如:OA应用

  • 插件ID:只能由字母,数字与下画线()构成,唯一,固定使用前缀:plugin_v2, 如:plugin_v2_oa

  • 插件ID不能重复

这里以实现一个OA系统的应用模板插件为例,插件名称为OA ,插件ID为: plugin_v2_oa

5 快速实现OA应用插件

5.1 简介

这里以OA系统为例,实现一个应用插件,实现从IDP单点到OA应用或者从OA应用发起SP登录
OA系统地址为: http://192.168.10.34/
提供了一个单点登录地址为: http://192.168.10.34/sso_login?username={Base64的RC4加密后的用户名}
允许传入一个RC4加密的用户名,浏览器访问后就能进入系统
通讯流程如下:

5.1 下载DEMO项目

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

5.2 修改 pom.xml

打开 pom.xml,修改 artifactId 为 idp-plugin-v2-oa,此处要求格式必须为 idp-plugin-v2-{ID}。
修改 description 为我们需要的描述,如 OA Module。
修改 version 为我们需要得版本, 如 1.0.0
image.png

5.3 修改包名

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

5.4 修改类名

我们需要将 DemoV2Application修改为 OAPluginApplication,修改后需要到 pom.xml 确认出现了该名字的地方跟着修改到了.
将 DemoV2SSOController修改为 OAPluginApplicationSSOController,
该类实现接收IDP传入的登录回调,加密username,并跳转到OA系统
其他相关类类似修改

5.5 修改OAPluginApplication

OAPluginApplication实现了IDPApplicationAdapter,里面定义了插件的基础信息
IDPApplicationAdapter类定义请参考7.1
这里根据需要修改插件名,插件ID,描述,图标
修改插件名为: plugin_v2_oa
修改插件名为: OA
修改插件描述为: OA Plugin
修改后的代码如下:

public class OAPluginApplication extends IDPApplicationAdapter {

    private static final Logger LOG = LoggerFactory.getLogger(OAPluginApplication.class);

    public static final List<DeviceType> DEVICE_TYPES = Arrays.asList(DeviceType.WEB);

    public static final String NAME = "OA ";

    public static final String APPLICATION_ID = "plugin_v2_oa";

    private byte[] logoAsBytes;

    public OAPluginApplication() {
    }

    @Override
    public boolean plugin() {
        return true;
    }

    @Override
    public long pluginVersion() {
        return 1;
    }

    @Override
    public String name() {
        return NAME;
    }

    @Override
    public boolean custom() {
        return true;
    }

    @Override
    public String applicationId() {
        return APPLICATION_ID;
    }

    @Override
    public List<DeviceType> supportedDeviceTypes() {
        return DEVICE_TYPES;
    }

    @Override
    public String description() {
        return "OA Plugin";
    }

    @Override
    public List<String> tags() {
        List<String> tags = new ArrayList<>();
        tags.add("Demo");
        return Collections.unmodifiableList(tags);
    }

    @Override
    public boolean developing() {
        return false;
    }

    @Override
    public byte[] logoAsBytes() throws IOException {
        if (this.logoAsBytes == null) {
            this.logoAsBytes = IOUtils.toByteArray(this.getClass().getClassLoader().getResourceAsStream(APPLICATION_ID + ".png"));
            LOG.debug("[{}]- Initialed " + this.getClass().getName() + " logo from: " + APPLICATION_ID + ".png", RIDHolder.id());
        }
        return this.logoAsBytes;
    }

    @Override
    public String plusApplicationUrl(String enterpriseId) {
        return "/api/bff/" + API_VERSION + "/application/" + APPLICATION_ID + "/plus?enterpriseId=" + enterpriseId;
    }

    @Override
    public String enableApplicationUrl(String enterpriseApplicationUuid, boolean enable) {
        return "/api/bff/" + API_VERSION + "/application/" + APPLICATION_ID + "/" + (enable ? "enable" : "disable") + "?applicationUuid=" + enterpriseApplicationUuid;
    }

    @Override
    public String detailsUrl(String enterpriseApplicationUuid) {
        return "/api/bff/" + API_VERSION + "/application/" + APPLICATION_ID + "/details?applicationUuid=" + enterpriseApplicationUuid;
    }

    @Override
    public String modifyApplicationUrl(String enterpriseApplicationUuid) {
        return "/api/bff/" + API_VERSION + "/application/" + APPLICATION_ID + "/modify?applicationUuid=" + enterpriseApplicationUuid;
    }

    @Override
    public String startSSOUrl(String enterpriseApplicationUuid) {
        return "/enduser/application/" + APPLICATION_ID + "/sso_" + enterpriseApplicationUuid;
    }

    @Override
    public String archiveApplicationUrl(String enterpriseApplicationUuid, boolean archived) {
        return "/api/bff/" + API_VERSION + "/application/" + APPLICATION_ID + "/" + (archived ? "archived" : "restore") + "?applicationUuid=" + enterpriseApplicationUuid;
    }

    @Override
    public String removeApplicationUrl(String enterpriseApplicationUuid) {
        return "/api/bff/" + API_VERSION + "/application/" + APPLICATION_ID + "/remove?applicationUuid=" + enterpriseApplicationUuid;
    }

    @Override
    public String bffSSOUrl(String enterpriseApplicationUuid) {
        return "/api/bff/" + API_VERSION + "/enduser/" + APPLICATION_ID + "/sso_" + enterpriseApplicationUuid;
    }
}

5.6 修改前端映射文件

我们需要修改 src/resources/{plugin_id}/schema 目录中 3 个 json 文件
这里修改文件夹名称为插件id:
plugin_v2_demo 改为 plugin_v2_oa
image.png

  • schema

    • details.json 应用详细UI schema定义

    • modify.json 应用修改UI schema定义

    • plus.json 应用添加UI schema定义

如何定义UI Schema文件中的内容请参考’UI Schema规范’章节内容。

5.6 修改插件图标

需要修改插件的图标文件,在代码的静态资源目录下,修改
plugin_v2_demo.png 为 plugin_v2_oa.png
image.png

  • 图片格式为png或jpeg,像素要求为 200x200 左右

  • 图片大小不超过200KB

  • 图片文件名和插件ID一样,如:plugin_oa.jpg

5.8 打包

DEMO插件工程使用Maven进行管理,在开发插件后,需要使用Maven命令来进行打包生成应用插件jar,
在工程的pom.xml文件所在目录,使用Maven 命令:

mvn clean package

运行成功后在target目录有以idp-plugin-xxx-{version}-jar-with-plugin.jar的文件生成即是插件jar。
考虑到应用插件开发时有可能依赖其他第三方lib来完成插件开发(即在pom.xml中会加入第三方依赖dependency),在打包时需要分两种情况来处理,具体如下:

6.1 无任何第三方lib依赖

若无任何第三方lib依赖,直接如上所说使用Maven命令运行,使用生成的idp-plugin-xxx-{version}-jar-with-plugin.jar文件即可,示例如下图:
image.png
如图中所示,直接使用 idp-plugin-oa-1.0.0-jar-with-plugin.jar 即可。

6.2 有第三方lib依赖

对于有第三方lib依赖的应用插件,需要配置使用Maven的assembly插件来将指定的lib一起打包到一个jar中。
假设我们需要依赖一个叫 jjwt的第三方lib库,
首先在 pom.xml中加入依赖,如下图:
image.png

其次增加assembly插件配置(在DEMO工程中已经有此插件配置,注释取消即可),如下图:
image.png

及下来在工程的 assembly配置文件(此配置文件内容默认时注释掉的)中增加对 jjwt库依赖的配置,如下图:
image.png

标签中的内容格式为:{groupId}:{artifactId}。

最后再使用Maven的命令生成对应的插件jar文件即可,如下图:
image.png

注意:此时生成的插件完整jar文件为:idp-plugin-xxx-{version}-jar-with-plugin.jar,即图中的idp-plugin-OA-1.0.0-jar-with-plugin.jar文件(此时的idp-plugin-OA-1.0.0.jar只包含工程本身代码)。

5.11 上传插件

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

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

5.12 使用插件

在IDaaS管理后台点击 应用/添加应用 选择刚刚上传的应用插件(OA)
右侧弹出添加页面,这里和5.7定义的内容一致,
image.png

登录地址: 填入上文提到的地址: http://192.168.10.34/sso_login
密钥: 输入RC4加密所需的密钥
点击提交,创建应用.
image.png

6 插件扩展接口

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

PluginExtension

所有拓展接口的父接口

PluginStateExtension

启用、禁用应用回调函数

PluginLoadExtension

加载插件时回调

PluginAccountDataDictionaryUpdateExtension

IDP中增加、修改账户时更新数据字典拓展

PluginApplicationCiperExtension

应用秘钥轮转使用的扩展接口

PluginApplicationCodeExtension

应用 授权码扩展接口

PluginApplicationDeleteExtension

插件应用删除回调接口

更多请查看SDK文档

6.1 PluginLoadExtension

PluginLoadExtension.java接口,当插件中需要扩展实现以下几点时使用到

  • onNewInstall()  插件新安装时调用此函数

  • onUpgrade(long oldVersion)   插件升级时调用此函数

  • onLoad(boolean state)  加载插件到JVM中时调用此函数

  • onUnLoad() 从JVM卸载插件时调用此函数


插件中需要写一个类来实现此接口,并配置为Spring Bean(类上加 @Component 注解),实现对应的方法加上自己的代码逻辑即可:
注意:若扩展实现了PluginLoadExtension.java接口,则默认的实现将不会执行,如:安装时执行SQL文件。

6.2 PluginStateExtension

PluginStateExtension.java接口用于处理当插件状态(启用/禁用)变化时扩展调用。

  • onEnable()  启用插件时调用

  • onDisable()  禁用插件时调用


插件中需要写一个类来实现此接口,并配置为Spring Bean(类上加 @Component 注解

6.3 PluginApplicationDeleteExtension

IDP中删除应用回调接口。

  • onDelete(String applicationUuid)  物理删除应用时回调函数

  • onDisable(String applicationUuid)  逻辑删除应用时回调函数

7 相关类说明

7.1 IDPApplicationAdapter

应用插件必须实现该类,定义插件的一些基础信息,一些基础方法定义如下
该类继承IDPApplication,默认实现了一些常用实现
下面列举一些常用方法,更多请查看JavaDoc文档说明(章节3资源下载)

方法名

说明

applicationId()

应用ID

name()

插件名, 如 Demo

description()

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

custom()

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

privately()

应用是否为 私有定制的若该应用是给具体的公司定制的, 值为true

developing()

标识此插件是否开发中,在IDP4中上传此插件时必须为 false

pluginVersion()

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

host()

应用的主页URL

tags()

应用标签

logoAsBytes()

应用Logo

7.2 EnterpriseApplicationRepository

EnterpriseApplicationRepository.java是插件中与IDP4通信的主要类,许多的操作最终通过此类完成,比如保存数据,查询账户关联信息,获取当前登录用户信息(currentUsername()方法)等。

8 持久化

在插件开发过程中可能会遇到需要保存一些其他的数据,可使用com.idsmanager.idp.core.IDPApplicationService.saveIDPApplicationPluginAdditionalDataDto() 方法保存
也可以在插件 com.idsmanager.plugin.xxx.enterprise 包中使用 hibernate 的方式增加表,具体示例如下

8.1 增加hibernate 实体类

package com.idsmanager.plugin.oa.enterprise;

import com.idsmanager.micro.commons.domain.AbstractJpaDomain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

/**
 * 2021/10/18
 *
 * @author ilanyu
 */
@Entity
@Table(name = "my_data")
public class MyData extends AbstractJpaDomain {

    @Column(name = "purchase_id")
    private String purchaseId;

    @Column(name = "name")
    private String name;

    public MyData() {
    }

    public String purchaseId() {
        return purchaseId;
    }

    public MyData purchaseId(String purchaseId) {
        this.purchaseId = purchaseId;
        return this;
    }

    public String name() {
        return name;
    }

    public MyData name(String name) {
        this.name = name;
        return this;
    }
}
package com.idsmanager.plugin.oa.enterprise;

import com.idsmanager.micro.commons.domain.RepositoryJpa;

/**
 * 2021/10/18
 *
 * @author ilanyu
 */
public interface MyDataRepository extends RepositoryJpa {
    MyData findByName(String purchaseId, String name);
}
package com.idsmanager.plugin.oa.infrastructure;

import com.idsmanager.micro.commons.repository.AbstractRepositoryHibernate;
import com.idsmanager.plugin.oa.enterprise.MyData;
import com.idsmanager.plugin.oa.enterprise.MyDataRepository;
import org.springframework.stereotype.Repository;

/**
 * 2021/10/18
 *
 * @author ilanyu
 */
@Repository
public class MyDataRepositoryHibernate extends AbstractRepositoryHibernate<MyData> implements MyDataRepository {

    @Override
    public MyData findByName(String purchaseId, String name) {
        final MyData data = (MyData) session().createQuery("select p from MyData p where p.archived = false and p.purchaseId = :purchaseId and p.name = :name ")
                .setParameter("purchaseId", purchaseId)
                .setParameter("name", name)
                .setMaxResults(1).uniqueResult();
        return data;
    }
}

8.2 执行自定义SQL脚本

若在插件中使用了自己创建表这种方式的自定义数据功能,则需要自己编写SQL初始化脚本,在插件初始化以及升级时将会自动执行脚本。需要执行的脚本要求放在 src/main/resources/database/ 目录中,文件名要求为 idp-plugin-{插件版本}.ddl,或 idp-plugin-{原插件版本}-{新插件版本}.ddl。自动执行脚本功能分为插件在租户中首次上传以及进行升级操作两个场景,首次上传时会执行 idp-plugin-{插件版本}.ddl 文件,其中插件版本要求和 pluginVersion 的返回值一致,如 OAApplication 中 pluginVersion 的返回值为1,则需要将文件命名为 idp-plugin-1.ddl;进行升级操作时执行的文件为 idp-plugin-{原插件版本}-{新插件版本}.ddl,如租户中已经上传了版本号为1的OA应用,现在需要上传版本号为2的OA应用,则在上传时会执行 idp-plugin-1-2.ddl,如果租户中已经有版本号为1的OA应用,现在直接去上传版本号为3的OA应用,则会执行 idp-plugin-1-3.ddl。
idp-plugin-1.ddl 示例如下

-- ###############
--  Domain: MyData
-- ###############
DROP TABLE IF EXISTS my_data;
CREATE TABLE `my_data`
(
  `id`           INT(11) NOT NULL AUTO_INCREMENT,
  `archived`     TINYINT(1) DEFAULT '0',
  `version`      INT(11)    DEFAULT 0,
  `uuid`         VARCHAR(255),
  `create_time`  DATETIME,
  `updated_time` TIMESTAMP  DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  
  `purchase_id`  VARCHAR(255),
  `name`         VARCHAR(255),
  PRIMARY KEY (`id`),
  INDEX `id_index` (`id`)
)
  ENGINE = InnoDB
  AUTO_INCREMENT = 20
  DEFAULT CHARSET = utf8;

9 UI SChema规范

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

10 FAQ

10.1 如何升级插件

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

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

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

在新版本分支上增加或修改新的功能,开发完成后重新打包,并重新上传新的插件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 文件。

10.3 插件上传后发现其中的数据库脚本没有执行

这是因为插件的版本和数据库版本不一致导致的
检查XXXPluginApplication中的pluginVersion值和database目录下的idp_plugin_{version}.ddl版本需要一致
image.png

10.4 重启IDaaS后升级插件失败,提示url映射已被占用

重启IDaaS后,需要一定的时间初始化所有插件,如果插件尚未初始化完成就去升级插件,变会有这个提示,需要再次重启IDaaS,并多等待一段时间后重新升级插件。