[]
        
(Showing Draft Content)

Java安全提供程序主要概念

至此,您应该已经对活字格安全提供程序有了大概的了解,并且也安装创建了一个最简单的安全提供程序集。

那么,从本章开始,您将可以完整地了解安全提供程序的核心接口、数据结构、认证模式等主要的核心开发概念,有了这些概念,就可以满足几乎所有的第三方用户的集成。

通过此篇文档,您将可以了解到:

  • ISecurityProvider 核心接口

  • ISupportSettings 认证参数配置化接口

  • 认证模式相关接口

  • 其他可选接口

ISecurityProvider 核心接口

ISecurityProvider 是安全提供程序的核心接口,活字格服务器会通过此接口读取第三方用户信息、校验第三方用户登录等。

目前,此接口共有5个属性,1个方法需要您实现。接下来,将为您分属性、方法介绍此接口。

getName()

getName方法用以指定安全提供程序的名称,主要用于在活字格服务器中的展示区分安全提供程序,如下示例中,微信公众平台即为getName的返回值:

    /**
     * 安全提供程序名称,主要用于在服务器中展示
     * @return 安全提供程序名称
     */
    @Override
    public String getName() {//返回securityProvider的名称(中文)
        return "微信公众平台";
    }

image

此外,一个比较重要的概念是,getName方法返回值是安全提供程序的唯一标识,如果您想上传了两个getName方法返回值相同的安全提供程序,则上传第二个时,会有覆盖提醒:

image

getUserInformations 用户信息

getUserInformations 返回一个特定的数据结构,其用于定义第三方系统中所有的用户信息。

当您希望将第三方系统中的用户集成到活字格管理控制台时,就需要实现此属性。

每当安全提供程序被加载以及数据同步时,活字格管理控制台就会通过此属性读取第三方系统中的用户信息并与活字格服务器进行集成。

 image

如下图所示就是一个传递了 getUserInfomations 方法的安全提供程序,当此属性值不为空时,属性中的用户信息会加载到 活字格管理控制平台 中:

image


说明:

getUserInfomations的返回值只有当 UserInformationStorageMode == InMemoryCache 才会被读取。具体可见 UserInformationStorageMode 小节。

AuthenticationType 认证类型

AuthenticationType 是一个枚举值,它代表了第三方用户的认证模式。对于不同的认证模式,活字格管理服务器会有不同的处理策略。

AuthenticationType.UserNameAndPassword

这是最常见的一种认证模式,当您选择 UserNameAndPassword 时,将表示您的第三方用户信息是通过用户名以及密码进行验证的。

需要登录时,用户会跳转到活字格的内建登录页面进行登录,活字格服务器会将用户录入的用户名以及密码通过入参传递到安全提供程序中,并由安全提供程序最终验证用户登录信息并最终返回用户详情信息。

使用这种认证方式,您需要自行在安全提供程序中完成用户名以及密码的校验逻辑,并将正确的用户信息进行返回。

第一章的示例代码就是以这种方式进行登录验证的。

AuthenticationType.OpenId

OpenId 即 OpenID Connect(OIDC),是建立在 OAuth2 之上的身份认证协议,它扩展了 OAuth2,以提供对用户身份的认证和用户信息的获取。

几乎对接所有第三方系统都会使用到的认证方式,例如微信,新浪,QQ等。

每当用户需要登录活字格服务器时,活字格会通过安全提供程序中的配置信息,获取到第三方站点的登录重定向地址,并将用户登录页面重定向到第三方站点供用户登录。

当用户在第三方站点登录成功后,基于OAuth2 协议,第三方站点会通过 Callback 地址请求活字格服务器中的登录地址,并返回 OpenID。

活字格服务器继续通过 OpenID 向安全提供程序获取用户详细信息,并最终记录活字格站点的用户认证策略,最终用户登录成功。

image

注意:

在这种模式下,安全提供程序是活字格服务器连接第三方系统的媒介,基于 OAuth2 协议,安全提供程序也需要特定的认证信息(通常为 ClientID + SecretKey的模式),为了灵活,这些信息应该做成可配置的。

因此,在这种模式下,您应该要额外实现一个接口 ISupportSettings 来完成第三方系统的登录配置。

此外,从上述流程可以看到,第三方系统的登录重定向地址也需要有安全提供程序提供,由于历史原因,您需要再额外实现一个接口 IOpenIdSecurityProvider 来提供登录重定向地址。

UserInformationStorageMode 用户信息存储模式

此属性是为了指定第三方的用户信息在活字格服务器中的保存方式。

InForguncyDatabase 写入活字格用户数据库

当您指定 UserInformationStorageModeInForguncyDatabase 时,用户首次登陆时,会把用户添加到活字格的用户数据库中。

管理员需要到活字格的用户数据库中为用户分配角色和组织信息。如果用户已经被加入到活字格的用户数据库中,此时在第三方应用中删除该用户或者修改用户属性不会自动同步到活字格的数据库中。

InMemoryCache 仅存在内存中

当您指定 UserInformationStorageModeInMemoryCache 时,活字格会在网站启动时从第三方应用获取全部用户信息,包括用户,用户属性,角色及组织结构信息。

并且缓存在网站的内存中,活字格应用会使用这些用户信息判断权限,工作流程等。活字格会每间隔一定时间从第三方网站同步一次最新的用户数据(默认为20分钟),可以根据业务需要调整同步的间隔时间。

getAllowLogout 允许用户执行登出操作

一般情况下,如果认证模式是 UserNameAndPassword 时,该方法的返回值应该为True。

如果认证模式是 OpenId 时,该方法的返回值有可能为 True 有可能为 False。

verifyUser(HashMap<String, String> properties) 方法

verifyUser(HashMap<String, String> properties) 作为接口中唯一的方法,其主要功能就是认证用户登录信息并返回用户详细信息。

每当用户需要登录时,活字格服务器就会请求此方法。

而登录的具体信息,则会通过字典类型入参 properties 传入,其定义具体如下:

  • 如果AuthenticationType是 UserNameAndPassword,包含的属性有 “userName” 和 “password”;

  • 如果AuthenticationType是 OpenId,不同的网站需要认证的属性不同,通常是token,code,state等。

下方展示了从入参中获得userName并进行登录的示例:

    @Override
    public User verifyUser(HashMap<String, String> properties) {
        String name = properties.get("userName");
        for (User user : getUserInformations().getUsers()) {
            if (user.getUserId().equals(name)) {
                return user;
            }
        }
        return null;
    }

您可以从定义发现,此方法除了认证用户信息外,还需要返回用户的详细信息,以提供给活字格服务器作为登录凭证记录在 Cookie 中。

ISupportSettings 认证参数配置化接口

上一节中,我们为您介绍了安全提供程序的核心接口 ISecurityProvider。但是,如果您只实现此接口,几乎只能做 UserNameAndPassword 模式的认证。

在您与第三方系统对接时,或多或少都需要建立一些授权信息,而这些信息则需要使用者进行参数配置。

这些配置信息当然可以在安全提供程序中写死,但是这样就导致安全提供程序和第三方账号绑定,想要切换账号必须修改安全提供程序的源代码,这明显不是一个很好的设计。

因此,我们为您提供了 ISupportSettings 接口用来在安全提供程序外定义一些配置信息。只要实现了此接口,那么活字格管理控制平台中就会展示安全系统程序的配置参数。

目前 ISupportSettings 接口只需要实现一个方法getSettings()用来展示参数列表,一个方法UpdateSetting()用以更新参数配置。

List<SecurityProviderSettings> getSettings()

getSettings() 返回一个SecurityProviderSettings 类型的列表,您可以在集合内定义任何想要的参数。

        ArrayList<SecurityProviderSettings> res = new ArrayList<SecurityProviderSettings>();
        Config config = loadConfig();

image

TextBoxEditor 文本框

文本框可以录入普通文本类型参数,比如ClientId等无需加密的内容。

文本框没有其他额外的配置,直接使用即可。

        
        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("Sk");
        setting.setDisplayName("Sk 参数");
        setting.setDescription("Security Key");
        setting.setEditor(new TextBoxEditor());
        setting.setValue(config.getSk());
        res.add(setting);

image

TextAreaEditor 多行文本框

多行文本框和普通文本框的唯一区别就是可以录入可换行的文本内容。

多行文本框也没有其他额外的配置,直接使用即可。

        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("TextArea");
        setting.setDisplayName("文本区域");
        setting.setDescription("这是文本区域");
        setting.setEditor(new TextAreaEditor());
        String multiLineText = """
                       This is a multi-line text.
                       It spans across multiple lines
                       using text blocks.
                       """;
        setting.setValue(multiLineText);
        res.add(setting);

image

PasswordEditor 密码框

密码框中的参数会通过SHA256 非对称加密传输,其中加密的逻辑您无需关心,最终获取到的 Value 即为解密后的内容。

密码框也没有其他额外的配置,直接使用即可。

        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("PasswordEditor");
        setting.setDisplayName("密码");
        setting.setDescription("这是密码");
        setting.setEditor(new PasswordEditor());
        res.add(setting);

image

LabelEditor 标签

标签作为仅展示的值,无法在活字格管理控制平台修改。

标签也没有其他额外的配置,直接使用即可。

        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("LabelEditor");
        setting.setDisplayName("标签");
        setting.setDescription("这是标签");
        setting.setValue("这是标签的值");
        setting.setEditor(new LabelEditor());
        res.add(setting);

image

 

ComboEditor 组合框

普通的组合框展示的内容即为最终的 Value。

其需要额外配置Items属性才可以正确展示内容。

        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("ComboEditor");
        setting.setDisplayName("组合框");
        setting.setDescription("这是组合框");
        var combo=new ComboEditor();
        var items=new ArrayList<String>();
        items.add("参数1");
        items.add("参数2");
        items.add("参数3");
        combo.setItems(items);
        setting.setEditor(combo);
        setting.setValue("参数2");
        res.add(setting);

image

AdvancedComboEditor 高级组合框

高级组合框和普通组合框的区别是,它可以将显示值和最终值做区分。

同样的,您也需要配置Items属性才可以使用,与普通组合框不同的是,高级组合框的Items属性是AdvancedSourceItem类型,其中包括两个参数:DisplayValue 以及 Value。

顾名思义,DisplayValue 为展示在前端的内容,Value为最终获取到的值。

        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("ComboEditor");
        setting.setDisplayName("高级组合框");
        setting.setDescription("这是高级组合框");
        var combo = new AdvancedComboEditor();
        var items = new ArrayList<AdvancedSourceItem>();

        var item1 = new AdvancedSourceItem();
        item1.setValue("param1");
        item1.setDisplayValue("参数1");
        items.add(item1);

        var item2 = new AdvancedSourceItem();
        item2.setValue("param2");
        item2.setDisplayValue("参数2");
        items.add(item2);


        var item3 = new AdvancedSourceItem();
        item3.setValue("param3");
        item3.setDisplayValue("参数3");
        items.add(item3);
        combo.setItems(items);
        setting.setEditor(combo);
        setting.setValue("param2");
        res.add(setting);

image

PropertyBindings 参数展示状态绑定

有时,参数的展示可能是跟随其他参数而联动的,比如:当用户将 Combo 参数选择为 A 时,后续的参数展示 A 相关的内容,选择 B 则展示 B 相关的内容。

如果您有以上的需求场景,那么可以使用所有Editor 属性上的 PropertyBindings 属性来做到。

image

您可以设置不同参数 Editor 上的 PropertyBindings 属性来控制属性的展示状态。如下述代码所示:


    @Override
    public List<SecurityProviderSettings> getSettings() {
        ArrayList<SecurityProviderSettings> res = new ArrayList<SecurityProviderSettings>();
        Config config = loadConfig();
        addAdvanceType(res,config);
        addTypeA(res,config);
        addTypeB(res,config);
        return res;
    }

    private void addTypeB(ArrayList<SecurityProviderSettings> res, Config config) {
        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("B类型的文本");
        setting.setDisplayName("B类型的文本");
        setting.setDescription("这是B类型的文本");
        var editor = new TextBoxEditor();
        setting.setEditor(editor);
        var bindings = new ArrayList<EditorPropertyBinding>();
        var bindingItem = new EditorPropertyBinding();
        bindingItem.setPropertyType(EditorPropertyType.Visibility);
        bindingItem.setBindingTarget("TypeComboEditor");
        bindingItem.setBindingValue("类型2");
        bindings.add(bindingItem);
        editor.setPropertyBindings(bindings);
        res.add(setting);
    }

    private void addTypeA(ArrayList<SecurityProviderSettings> res, Config config) {
        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("A类型的文本");
        setting.setDisplayName("A类型的文本");
        setting.setDescription("这是A类型的文本");
        var editor = new TextBoxEditor();
        setting.setEditor(editor);
        var bindings = new ArrayList<EditorPropertyBinding>();
        var bindingItem = new EditorPropertyBinding();
        bindingItem.setPropertyType(EditorPropertyType.Visibility);
        bindingItem.setBindingTarget("TypeComboEditor");
        bindingItem.setBindingValue("类型1");
        bindings.add(bindingItem);
        editor.setPropertyBindings(bindings);
        res.add(setting);
    }

    private void addAdvanceType(ArrayList<SecurityProviderSettings> res, Config config) {
        SecurityProviderSettings setting = new SecurityProviderSettings();
        setting.setName("TypeComboEditor");
        setting.setDisplayName("组合框");
        setting.setDescription("这是组合框");
        var combo = new ComboEditor();
        var items = new ArrayList<String>();
        items.add("类型1");
        items.add("类型2");
        items.add("类型3");
        combo.setItems(items);
        setting.setEditor(combo);
        setting.setValue("类型3");
        res.add(setting);
    }

选择类型3的时候 类型1,2的选项都不显示。

image

选择类型2的时候显示类型2的选项。

 image

选择类型1的时候显示类型1的选项。

image

如果联动展示的参数中,设置了 Invert == True,则表示满足 Value 时不会展示,不满足 Value 时才会展示。

setIsHidden 隐藏控件

参数 Editor 上除了PropertyBindings以外还有一个setIsHidden方法。

如果您将其设置为 True,则在前端不会展示此属性,但是依然可以获取到 Value。

updateSetting() 更新参数设置

上一小节中,我们介绍了安全提供程序参数的数据结构以及控件展示。但是展示只是第一步,我们最终的目的是要在活字格管理控制平台中随时修改这些参数配置。

每当在管理控制平台中单击“保存设置”按钮,都会调用UpdateSetting()方法进行参数保存。

每当保存时,我们会将在前端设置好的数据通过字典类型的参数传入UpdateSetting()方法中,下面的例子为保存ak和sk参数到json文件:

写入根目录下的config.json


    @Override
    public void updateSetting(HashMap<String, Object> dictionary) {
        {
            String jarPath = null;
            try {
                jarPath = CustomSecurityProvider.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
            } catch (URISyntaxException e) {
                throw new RuntimeException(e);
            }
            String jsonPath = jarPath.substring(0, jarPath.lastIndexOf('/') + 1) + "config.json";
            File jsonFile = new File(jsonPath);

            ObjectMapper objectMapper = new ObjectMapper();
            Config newObj = new Config();
            newObj.setAk((String) dictionary.get("Ak"));
            newObj.setSk((String) dictionary.get("Sk"));
            try {
                objectMapper.writeValue(jsonFile, newObj);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

读取根目录下的config.json


    private Config loadConfig() {
        String jarPath = null;
        try {
            jarPath = CustomSecurityProvider.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        String jsonPath = jarPath.substring(0, jarPath.lastIndexOf('/') + 1) + "config.json";
        File jsonFile = new File(jsonPath);
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            return objectMapper.readValue(jsonFile, Config.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

 

您应该在pom.xml文件中的exec-maven-plugin中执行的packageZip_jwd.jar(即打包程序)的参数中手动指定每个需要被打包到最终zip包中的配置文件,由于文件读取和写入都由您来实现,所以对配置文件的格式没有要求,在大部分的场景下可以使用json文件,打包工具会把第三个起的命令行参数当作基于根目录的相对路径进行打包。

 image

config.xml打包结果如下:

image

rex/config.txt打包结果如下:

 image

至此,您的参数配置即可以持久化到文件中了。重启管理控制平台,依然可以保存上一次的配置信息。

认证模式相关接口

由于历史版本兼容问题,在您使用不同的认证模式时,可能还需要额外实现一些接口进行配合。本节将为您介绍这些特定认证模式下所需要的接口。

IOpenIdSecurityProvider

想要使用 OpenId 作为第三方系统的集成模式,您还需要额外实现IOpenIdSecurityProvider接口。

此接口只需要实现一个方法GetRedirectUrl(),用以在登录时获取第三方站点的登录地址。

方法共有三个入参:

  • redirect_uri:登录成功后的重定向地址,当您的用户在第三方系统登录成功后,依照 OAuth2.0 协议,需要跳转回一个活字格服务器的地址,即为此地址

  • state:是一个用于校验的参数,通常需要此参数防止跨站攻击

  • userAgent:用户使用的 Agent 信息,通常用以判断是否是手机端

如下是一个 钉钉 的OpenId登录地址获取逻辑示例:


    @Override
    public String getRedirectUrl(String redirectUri, String state, String userAgent) {

        if (!needRedirect(userAgent)) {
            return null;
        }

        HashMap<String, String> queryParams = new HashMap<>();
        String appId = loadConfig().getAppId();
        queryParams.put("appid", appId);
        queryParams.put("response_type", "code");
        queryParams.put("scope", "snsapi_auth");
        queryParams.put("state", state);
        queryParams.put("redirect_uri", redirectUri);

        return "https://oapi.dingtalk.com/connect/oauth2/sns_authorize" + buildUrlWithParams(queryParams);
    }

    private String buildUrlWithParams(HashMap<String, String> params)  {
        StringBuilder sb = new StringBuilder();
        if (!params.isEmpty()) {
            sb.append("?");
        }
        for (Map.Entry<String, String> entry : params.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8);
            String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8);
            sb.append(encodedKey).append("=").append(encodedValue).append("&");
        }

        sb.deleteCharAt(sb.length() - 1); // Remove the last "&" character

        return sb.toString();
    }

其他可选接口

以上小节所介绍的接口都是安全提供程序所必须的接口,除了上述那些,我们还提供几个额外的可选接口,用以提升代码性能以及可用性。

IAllowRelativeToExistForguncyUser

此接口用于将第三方系统中的用户与活字格服务器中的用户相关联。

说明:

  1. 此接口只在 UserInformationStorageMode == InForguncyDatabase 才会生效

  2. 此接口只能基于 OpenId 的认证模式才会生效

在不使用此接口的情况下,OpenId 在第三方系统登录成功后,会向活字格的用户数据库中写入一条以 OpenId 作为用户名的用户数据,未来登录活字格的系统就使用此用户实现。

当您使用了IAllowRelativeToExistForguncyUser接口后,上述逻辑就会发生改变:

当您使用了此接口后,活字格管理控制平台中,安全提供程序的参数中会多出一个“用户创建方式”供您选择:

 image

如果您选择了“以OpenID作为用户名”选项(默认为此选项),则所有的逻辑和不使用IAllowRelativeToExistForguncyUser接口完全一致。

如果您选择了“由用户决定用户名和密码”选项,则当您在第三方系统登录成功后,会跳转到一个用户关联页(PC与手机共用此页面)来关联您的一个已有账号:

 image

当您关联了一个已有账号后,活字格将不会写入以OpenId为用户名的活字格用户,而是以您关联的账号作为活字格系统内部的账号使用。

当然,如果您还没有对应的账号还可以跳转创建一个可关联的账号。

image

这时,活字格管理控制平台的用户列表页面会自动多一个自定义属性OpenID(此自定义属性的名称可以在安全提供程序参数配置中修改):

 image

您关联成功的用户的 OpenID 就会存储在此字段中。

未来,当您直接通过PC登录活字格站点时,就可以使用您关联的用户进行登录操作了。

下一步

在本章节中,我们向您介绍了安全提供程序所需要的所有核心接口以及用法。至此,您应该可以开发一个集成第三方系统的安全提供程序了。

下一小节,我们将为您展示钉钉安全提供程序的样例代码。