1

统一身份认证是整个 IT 架构的最基本的组成部分,而账号则是实现统一身份认证的基础。做好账号的规划和设计直接决定着企业整个信息系统建设的便利与难易程度,决定着系统能否足够敏捷和快速赋能,也决定了在数字化转型中的投入和效率。用户账号是用户身份的一种表示,传统统一身份认证系统往往被作为外围系统来集成各个应用系统,而不是作为核心基础系统被其他应用系统来集成。所以传统统一身份认证系统的建设存在众多的问题,使设计实现复杂化、管理复杂化、集成复杂化。
每个企业可能同时会有多套系统在运行,但每个用户的账号在企业中仅有一套,可以适用于各个系统当中。因此,这就涉及到我们如何将一套账号应用到各个系统中,保证账号的权限体系。
常见方法:
1、(最简单但最深恶痛绝的)数据复制一份导入到每一套系统中。这样会造成维护工作量大,数据混乱,如果是多级企业,将会发生难以想象的灾难。
2、在身份集成中,自定义安全程序的开发,用一套用户身份验证程序,集成到各个系统中。
本文将从以下三点来介绍如何编写自定义安全提供程序,并在项目中配置引用。

编写自定义安全提供程序

编写一个自定义安全提供程序的步骤如下:
(1) 创建项目

使用Microsoft Visual Studio 2017(以下简称VS2017),创建一个新的项目,类型选为 Visual C# - .NET Standard - 类库(.NET Standard),输入项目名称,如:MySecurityProvider:

(2) 添加程序包依赖
自定义安全提供程序所实现的接口是由几个程序包定义的,为此需要添加对这几个程序包的依赖。方法如下:
首先将下面这两个文件下载保存到本地硬盘,比如C:\Temp\pkg 文件夹下:
grapecity.enterprise.identity.externalidentityprovider.1.0.2.nupkg

GrapeCity.Enterprise.Identity.SecurityProvider.1.0.3.nupkg

单击VS2017的“工具”菜单的“NuGet包管理器”>“程序包管理器设置”:

选中“程序包源”,再单击加号按钮:

单击【...】按钮,指定“源”的路径为nupkg文件所在的文件夹,如:C:\temp\pkg

单击“确定”按钮保存设置。
在右侧解决方案资源管理器窗格中,右键单击“依赖项”,点击“管理NuGet程序包”,再点击“浏览”,选中新添加的程序包源,将会列出两个需要依赖的程序包:

GrapeCity.Enterprise.Identity.ExternalIdentityProvider和GrapeCity.Enterprise.Identity.SecurityProvider,如下图:

逐个选中程序包,点击“安装”,即可添加本项目对这两个程序包的依赖。
(3) 实现接口
自定义安全提供程序需要实现两个接口:ISecurityProviderFactory和ISecurityProvider。
实现第一个接口的操作步骤:
添加一个新的类文件,如MySecurityProviderFactory.cs,以实现ISecurityProviderFactory接口。
public class MySecurityProviderFactory: ISecurityProviderFactory
该接口规定了两个属性和一个方法:

public string Description // 本安全提供程序的描述字串。
public IEnumerable<ConfigurationItem> SupportedSettings // 本安全提供程序支持的用户配置项。

这些用户配置项将出现在Wyn的管理画面中,允许系统管理员进行设置。典型的配置项是用户信息数据库的连接字串。通过提供这种配置项目,可以避免在安全提供程序中硬编码用户信息数据库连接字串的问题。
public Task<ISecurityProvider> CreateAsync(IEnumerable<ConfigurationItem> settings) // 本安全提供程序的实例创建方法。
这个方法的内容几乎是固定的,如:

public Task<ISecurityProvider> CreateAsync(IEnumerable<ConfigurationItem> settings)
{
    return Task.FromResult<ISecurityProvider>(new MySecurityProvider(settings));
}

实现ISecurityProvider接口

这个ISecurityProvider接口是安全提供程序的核心,其规定的属性和方法如下:

成员类型名称说明
属性ProviderName返回本安全提供程序的名称。
方法GenerateTokenAsync验证用户名和密码,通过时生成Wyn的访问令牌。
方法GetUserContextAsync返回用户的上下文信息,一般是根据用户名,从数据库查询得到用户的所属部门和其他业务数据。
方法GetUserDescriptorAsync返回用户的说明信息,该信息将用于门户页面的当前登录用户显示。
方法GetUserOrganizationsAsync返回用户的所属组织结构信息。
方法GetUserRolesAsync返回用户的角色信息,多个角色以字符串数组的形式返回。
方法ValidateTokenAsync验证令牌的合法性。在业务系统集成中,使用Token直接访问系统时,此方法用于检查传入Token的正确性。

除了上表所列成员,还有IExternalUserDescriptor,IExternalUserContext等接口,这些接口只是规定了实体类的属性,使用自定义类实现这些接口即可。

下面的文件附件是一个自定义安全提供程序的示例代码。

MySecurityProvider.zip

此示例代码中的解决方案(.sln)可在Visual Studio 2017中直接打开。示例代码文件夹\bin\debug中也包含Build产物DLL,可直接配置为Wyn的安全提供程序。示例的用户信息是保存在SQL Server数据库中的,请将本文件包中的db\MyUsers.bak文件恢复为SQL Server数据库。

有关接口的详细说明,请参考下面的接口介绍。

接口介绍

ISecurityProviderFactory接口

定义

public interface ISecurityProviderFactory 
{
    string ProviderName { get; }
    string Description { get; }
    IEnumerable<ConfigurationItem> SupportedSettings { get; }
    Task<ISecurityProvider> CreateAsync(IEnumerable<ConfigurationItem> settings);
}

接口说明

属性和方法说明
ProviderName安全提供程序的名称,不能为空,不能和其它的安全提供程序重名。
Description安全提供程序的描述文本,可以为空。
SupportedSettings该安全提供程序加载和运行时所必须的配置项。比如安全提供程序需要访问数据库,那么数据库连接字符串即为一个必须的配置项,必须由管理员在安全提供程序管理页面配置好,该安全提供程序才能正常工作。可以没有任何必须的配置项,返回一个空列表即可。
CreateAsync创建一个安全提供程序的实例。参数settings即为管理员已经配置好的配置项列表,用户可以在这里把配置项列表通过构造函数传入构建的安全提供程序实例。

[ISecurityProvider接口]()

定义

public interface ISecurityProvider
{
    string ProviderName { get; }
    Task DisposeTokenAsync(string token);
    Task<string> GenerateTokenAsync(string username, string password, object customizedParam = null);
    Task<IExternalUserContext> GetUserContextAsync(string token);
    Task<IExternalUserDescriptor> GetUserDescriptorAsync(string token);
    Task<string[]> GetUserOrganizationsAsync(string token);
    Task<string[]> GetUserRolesAsync(string token);
    Task<bool> ValidateTokenAsync(string token);
}

接口说明

属性和方法说明
ProviderName安全提供程序的名称,不能为空,不能和其它的安全提供程序重名。
DesposeTokenAsync使给定的token失效。
GenerateTokenAsync判断给定的用户名和密码是否有效,如果有效,返回一个唯一的token;否则返回null或空字符串。注:该token可以是任何形式,比如用户的id,或这个用户信息加密后的字符串,只要确保安全提供程序可以根据这个token正确地返回这个用户的相关信息即可。
GetUserContextAsync使用给定的token获取用户的上下文信息。用户的上下文信息包含哪些内容可以是随意的。
GetUserDescriptor使用给定的token获取用户的基本信息。基本信息包括用户的id,用户名和安全提供程序的名称,都不能为空。
GetUserOrganizationsAsync使用给定的token获取用户所属的部门信息。(该接口暂时没有使用)。
GetUserRolesAsync使用给定的token获取用户的角色信息。返回用户所属角色的名称,这些角色的名称需要跟admin portal中列出的角色名完全匹配,否则会被忽略。
ValidateTokenAsync验证给定的token是否是该安全提出程序提供的一个合法有效的token。

IExternalUserDescriptor接口

定义

public interface IExternalUserDescriptor
{
    string ExternalUserId { get; }
    string ExternalUserName { get; }
    string ExternalProvider { get; }
}

接口说明

参数说明
ExternalUserId用户的唯一标识符。
ExternalUserName用户名。
ExternalProvider用户的提供者,即为安全提供程序的名称。

IExternalUserContext接口
定义

public interface IExternalUserContext
{
    IEnumerable<string> Keys { get; }
    Task<string> GetValueAsync(string key);
    Task<IEnumerable<string>> GetValuesAsync(string key);
}

接口说明

参数说明
Keys用户上下文信息所包含的项目。
GetValueAsync对于给定的key,获取其对应的用户信息。
GetValuesAsync对于给定的key,获取其对应的用户信息,适用于多值情况。

注意

  • 在每个接口的实现函数中,必须有try-catch异常处理,在catch的异常处理部分,不要用throw语句再次抛出异常,而应返回Task对象,例如:return Task.FromResult<T>(null); 其中T为接口函数规定的某个类型。
  • 用户上下文的key不要用以下字符串:sub,name,auth_time,idp,userid,email。

配置自定义安全提供程序
(1) 文件部署
将编译得到的安全提供程序DLL文件,复制到Wyn安装目录下的SecurityProviders文件夹下,在Windows环境下,默认路径为:
C:\Program Files\Wyn\Server\SecurityProviders
提示
如果安全提供程序还依赖其他DLL,也请一并复制到同一目录。
(2)重启服务

(3) 添加用户安全提供程序
以管理员身份登录到系统的后台管理网站,单击“+添加用户提供程序”。

勾选自定义的安全提供程序后保存。

(4) 配置安全提供程序
选中刚添加的自定义安全提供程序,右边将会显示可配置的设定选项。具体有哪些选项是在安全提供程序的代码中确定的。按实际配置输入这些选项内容即可。

输入完毕,单击“保存”按钮。

(5) 重启服务
为使自定义安全提供程序的设置生效,需要进入任务管理器重启WynService服务。

此后,就可以在登录窗口输入业务系统的用户名和密码来登录Wyn门户了。

注意事项
在编写安全程序中,我们需要注意的几个方法

  1. MySecurityProvider.cs 文件中的 GenerateTokenAsync 方法,此方法用于第一次登录中,验证登录信息的方法。所以这一步需要完成的功能就是验证用户名密码,案例中所给的验证方式为从数据库中直接获取用户信息后判断登录。这里可以实现自定义的验证方式。只需要对此方法中的 Database.GetUserInfo 这个被调用的方法进行改造即可。
    【链接数据库进行验证】

public Task<string> GenerateTokenAsync(string username, string password, object customizedParam = null)
        {
            string rst = null;
            try
            {
                var userInfo = Database.GetUserInfo(username, password);
                var roles = userInfo.RoleNames.Split(',');
                var tokenValues = new string[roles.Length + 1];
                tokenValues[0] = userInfo.UserName;
                roles.CopyTo(tokenValues, 1);
                var token = string.Join(Constants.TokenDelimiter, tokenValues);
                token = Convert.ToBase64String(Encoding.UTF8.GetBytes(token));

                rst = token;

                Database.WriteLogS("GenerateTokenAsync token=", token);
                return Task.FromResult(rst);
            }
            catch (Exception e)
            {
                Database.WriteLogS("GenerateTokenAsync", e.ToString());
                return null;
            }

        }

【自定义验证,这里可以使用api,可以使用加密字符串等各类操作】

public Task<string> GenerateTokenAsync(string username, string password, object customizedParam = null)
        {
            string rst = null;
            try
            {
                if (customizedParam==null)
                {
                    return null;
                }
                Dictionary<string, string> parameters = (Dictionary<string, string>)customizedParam;
                var userInfo = RSAHelper.UserDecrypt(username, parameters["key"], keyFileName);
                if (userInfo == null)
                {
                    return null;
                }
                var roles = userInfo.RoleNames.Split(',');
                var tokenValues = new string[roles.Length + 1];
                tokenValues[0] = userInfo.UserName;
                roles.CopyTo(tokenValues, 1);
                var token = string.Join(Constants.TokenDelimiter, tokenValues);
                token = Convert.ToBase64String(Encoding.UTF8.GetBytes(token));

                rst = token;

                Database.WriteLogS("GenerateTokenAsync token=", token);
                return Task.FromResult(rst);
            }
            catch (Exception e)
            {
                Database.WriteLogS("GenerateTokenAsync", e.ToString());
                return null;
            }

        }

上图方法中,我们使用了自定义参数 其中key为我们自定义的键值对内容

在使用时可以这样配置:(自定义参数部分必须以 key:value 配置)

  1. MySecurityProvider.cs 文件中的 GetUserContextAsync 方法,根据方法追踪,最终所调用的方法为 Database.cs中的GetUserInfoByName 方法,所以过程忽略,直接改造此方法即可。
    注意:这里所返回的用户信息,则直接会在wyn中登录后所用到,所以这里注意返回结果信息。
    (图例3)【根据用户名获取用户相关信息】

【自定义返回信息】

  1. 若在程序中引用了其他dll,则需要在放入安全自定义程序时,将对应的dll放置到指定文件夹中。
    路径为此(默认安装路径,若更改安装路径,则自行寻找)
    C:\Program Files\Wyn\Server\SecurityProviders
  2. 日志打印,需要设置路径,在C盘创建log文件夹,否则打印不到。

  1. 配置界面信息设置


在当前页面看到的链接字符被修改为 秘钥(Base64) 这个可以在程序中直接配置,可以在 MySecurityProviderFactory.cs 文件中直接配置 SupportedSettings 此方法内容即可。

获取时在:MySecurityProvider.cs 自定义获取即可。

  1. 设置门户网站
  2. 返回的组织,角色如何处理?
    返回信息中,若组织,角色在系统中没有,则无法正常访问,可以在后台管理中设置对应的组织,角色,并且给角色分配响应的权限。
    设置组织:

传递的组织内容为:"/A/B" 顶级组织信息为 "/"
角色配置:给对应的角色设置权限

  1. 进程调试 ctrl+alt+p,选择 显示用户所有进程,选中dotnet.exe 打中断点。点击附加。程序中选择断点。

接下来就是打包测试了,将程序配置好之后,就可以正常测试使用了。

登录API客户端管理
登录api代码示例

通过postman调用生成token ,生成对应的安全自定义程序。
请求参数中:client_id,client_secret 为 Client Management 中所生成内容. 具内容参考 登录API客户端管理 默认信息有:
client_id:integration
client_secret:eunGKas3Pqd6FMwx9eUpdS7xmz
后期可以自行设置修改。
请求截图:

②代码请求如下:

fetch("http://localhost:51980/connect/token", {
"method": "POST",
"headers": {
"Content-type": "application/x-www-form-urlencoded"
},
"body": "grant_type=password&username=admin&password=admin&client_id=integration&client_secret=eunGKas3Pqd6FMwx9eUpdS7xmz"
}).then(function(res){
res.json()
.then(function(data){
console.log(data)
});
})

至此,已经可以获取到token了,获取后可以直接登录访问。
http://localhost:51980/integr...生成的token

到这里已经全部实现用户身份集成自定义开发,大家如果想了解更多商业BI行业精选模板,可以访问:
https://www.grapecity.com.cn/...


葡萄城技术团队
2.7k 声望29.3k 粉丝

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。