BI如何實現使用者身份整合自定義安全程式開發

2022-09-01 15:01:29

統一身份認證是整個 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 CreateAsync(IEnumerable 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(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/integration?token=生成的token

到這裡已經全部實現使用者身份整合自定義開發,大家如果想了解更多商業BI行業精選模板,可以存取:
https://www.grapecity.com.cn/solutions/wyn/demo