在ABP VNext框架中,即使在它提供的所有案例中,都沒有涉及到Winform程序的案例介紹,不過微服務解決方案中提供了一個控制台的程序供了解其IDS4的調用和處理,由於我開發過很多Winform項目,以前基於ABP框架基礎上開發的《ABP快速開發框架》中就包含了Winform客戶端,因此我對於ABP VNext在Winform上的使用也比較關心,花了不少時間來研究框架的相關的授權和窗體構建處理上,因此整理了該隨筆內容,主要用於介紹ABP VNext框架中Winform終端的開發和客戶端授權信息的處理。
1、ABP VNext框架中Winform終端的開發
不管對於那種終端項目,需要應用ABP VNext模塊的,都需要創建一個模塊類,繼承於AbpModule,然后引入相關的依賴模塊,並配置Servcie信息,如下是Winform項目中的Module類,如下所示。
namespace Winform.TestApp { [DependsOn( typeof(MicroBookStoreHttpApiClientModule), typeof(AbpHttpClientIdentityModelModule) )] public class WinformApiClientModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { } } }
ABP VNext模塊的初始化,根據依賴關系進行相關的初始化,我們在創建Winform項目(基於.net Core開發)的時候,需要在Main函數中創建一個應用接口,如下所示。
// 使用 AbpApplicationFactory 創建一個應用 var app = AbpApplicationFactory.Create<WinformApiClientModule>(); // 初始化應用 app.Initialize();
這個app接口對象非常重要,需要用它創建一些接口服務,如下所示。
var service = app.ServiceProvider.GetService<IService1>();
不過由於這個app對象需要在整個應用程序的生命周期中都可能會用到,用來構建一些用到的接口對象等,那么我們就需要創建一個靜態類對象用來存儲相關的應用接口信息,需要用到它的時候就可以直接使用了,否則丟掉了就沒法構建接口使用了。
首先我們創建一個用於存儲全局信息類GlobalControl,如下所示。
/// <summary> /// 應用程序全局對象 /// </summary> public class GlobalControl { public MainForm? MainDialog { get; set; } = null; public IAbpApplicationWithInternalServiceProvider? app { get; set; } /// <summary> /// 創建指定類型窗口實例 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T CreateForm<T>() where T :Form { if (app == null) return null; else { var form = app.ServiceProvider.GetService<T>(); return form; } } /// <summary> /// 創建服務類的接口實例 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T GetService<T>() where T : class { if (app == null) return null; else { var service = app.ServiceProvider.GetService<T>(); return service; } }
這樣我們在Main方法中創建的時候,構建一個靜態的類對象,用於存儲我們所需要的信息,這樣上面提到的應用接口對象,就可以存儲起來,
public static class Portal { /// <summary> /// 應用程序的全局靜態對象 /// </summary> public static GlobalControl gc = new GlobalControl(); /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .Enrich.FromLogContext() .WriteTo.Console() .WriteTo.File("logs/myapp.txt", rollingInterval: RollingInterval.Day) .CreateLogger(); // 使用 AbpApplicationFactory 創建一個應用 var app = AbpApplicationFactory.Create<WinformApiClientModule>(); // 初始化應用 app.Initialize(); gc.app = app; Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); var form = app.ServiceProvider.GetService<MainForm>(); gc.MainDialog = form; Application.Run(gc.MainDialog); } }
上面標注紅色的部分就是把這個重要的app存放起來,便於后期的使用。
而我們注意到,我們創建窗體的時候,不是使用
var form = new MainForm();
的方式構建,而是使用接口構建的方式。
var form = app.ServiceProvider.GetService<MainForm>();
和我們前面提到的方式構建接口是一樣的。
var service = app.ServiceProvider.GetService<IService1>();
這個是為什么呢?因為我們需要通過構造函數注入接口方式,在窗體中引用相關的接口服務。
由於沒有默認構造函數,因此不能再通過new的方式構建了,需要使用ABP VNext的常規接口解析的方式獲得對應的窗體對象了。
注意:這里窗體需要繼承自 ITransientDependency 接口,這樣才可以通過接口的方式構建,否則是不行的。
如果我們在主窗體或者其他界面事件中調用其他窗口,也是類似,如下調用所示。
private void button2_Click(object sender, EventArgs e) { var form2 = Portal.gc.CreateForm<SecondForm>(); form2.ShowDialog(); }
這個地方就是用到了靜態對象GlobalControl里面的方法構建,因為里面在程序啟動的時候,已經存儲了app應用接口對象了,可以用它來構建相關的接口或者窗體對象。
當然,這里的SecondForm也是不能使用New的方式構建窗體對象,也需要使用服務構建的標准方式來處理,畢竟它的默認構造函數用於接口的注入處理了。
程序看起來效果如下所示,可以正常打開窗體了。
2、Winform客戶端授權信息的處理
在ABP VNext微服務的解決方案中,有一個控制台調用服務接口的測試項目,如下所示。
它主要就是介紹如何配置IdentityServer4(也叫IDS4)的授權規則來獲得動態客戶端的接口調用服務的。
它的配置是通過appsettings.json中配置好IdentityServer4終端的節點信息,用來在客戶端調用類中進行相關的授權處理(獲得令牌)的,因為我們調用服務接口需要令牌信息,而這些都是封裝在內部里面的。
appsettings.json的配置信息如下所示,這個IDS4認證是采用client_credentials方式認證的。
而在構建ABP VNext項目模板的時候,也提供了一個類似控制台的測試項目,如下所示。
這個里面的appsettings.json是使用用戶名密碼的方式進行認證的,授權方式是密碼方式。
看到這些信息,你可能注意到了用戶名密碼都在里面。
我在想,如果每次讓用戶使用Winform程序的時候,來修改一下這個appsettings.json,那肯定是不友好的,如果把IDS4信息動態構建,傳入接口使用,是不是就可以不用配置文件了呢?
通過分析ABP VNExt框架的類庫,你可以看到IDS的授權認證處理是在IdentityModelAuthenticationService 接口實現類里面,它通過下面接口獲得通信令牌信息。
public async Task<string> GetAccessTokenAsync(IdentityClientConfiguration configuration)
我們傳入對應的IDS4的配置對象即可獲得接口的令牌信息。
我們通過IIdentityModelAuthenticationService 接口獲得令牌信息,緩存起來可以,但是每次調用的時候,如何設定HttpClient的令牌頭部信息呢,通過分析 IdentityModelAuthenticationService 類的代碼知道,如果我們在appsetting.json配置了IDS4的標准配置,它就可以根據配置信息獲得令牌信息的緩存,並設置到調用的HttpClient里面,如果我們采用剛才說的動態配置對象的傳入獲得token,沒有IDS4配置文件信息它是沒法提取出令牌緩存信息的。
public async Task<bool> TryAuthenticateAsync(HttpClient client, string identityClientName = null) { var accessToken = await GetAccessTokenOrNullAsync(identityClientName); if (accessToken == null) { return false; } SetAccessToken(client, accessToken); return true; }
那有沒有其他方式可以動態設定令牌信息或者類似的操作呢?
有!我們注意到,IRemoteServiceHttpClientAuthenticator 接口就是用來解決終端授權處理的接口,它的接口定義如下所示。
namespace Volo.Abp.Http.Client.Authentication { public interface IRemoteServiceHttpClientAuthenticator { Task Authenticate(RemoteServiceHttpClientAuthenticateContext context); } }
我們參考項目Volo.Abp.Http.Client.IdentityModel.Web的思路
這個項目使用了自定義的接口實現類HttpContextIdentityModelRemoteServiceHttpClientAuthenticator,替換默認的IdentityModelRemoteServiceHttpClientAuthenticator類,我們來看看它的具體實現
namespace Volo.Abp.Http.Client.IdentityModel.Web { [Dependency(ReplaceServices = true)] public class HttpContextIdentityModelRemoteServiceHttpClientAuthenticator : IdentityModelRemoteServiceHttpClientAuthenticator { public IHttpContextAccessor HttpContextAccessor { get; set; } public HttpContextIdentityModelRemoteServiceHttpClientAuthenticator( IIdentityModelAuthenticationService identityModelAuthenticationService) : base(identityModelAuthenticationService) { } public override async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context) { if (context.RemoteService.GetUseCurrentAccessToken() != false) { var accessToken = await GetAccessTokenFromHttpContextOrNullAsync(); if (accessToken != null) { context.Request.SetBearerToken(accessToken); return; } } await base.Authenticate(context); } protected virtual async Task<string> GetAccessTokenFromHttpContextOrNullAsync() { var httpContext = HttpContextAccessor?.HttpContext; if (httpContext == null) { return null; } return await httpContext.GetTokenAsync("access_token"); } } }
這里看到,它主要就是從httpContext中獲得access_token的頭部信息,然后通過SetBearerToken的接口設置到對應的HttpRequest請求中去的,也就是先獲得令牌,然后設置請求對象的令牌,從而完成了授權令牌的信息處理。
我們如果是Winform或者控制台,那么調用請求類是HttpClient,我們可以模仿項目 Volo.Abp.Http.Client.IdentityModel.Web 這個方式創建一個項目,然后通過依賴方式來替換默認授權處理接口的實現;也可以通過在本地項目中創建一個IdentityModelRemoteServiceHttpClientAuthenticator的子類來替換默認的,如下所示。
namespace Winform.TestApp { public class MyIdentityModelRemoteServiceHttpClientAuthenticator : IdentityModelRemoteServiceHttpClientAuthenticator {
在ABP VNext框架類IdentityModelAuthenticationService中獲得令牌的時候,就會設置獲得的令牌到分布式緩存中,它的鍵是IdentityClientConfiguration對象的鍵值生成的,如下代碼邏輯所示。
那么我們只需要在自定義的 MyIdentityModelRemoteServiceHttpClientAuthenticator 類中根據鍵獲得緩存就可以設置令牌信息了。
通過上面的處理,我們就可以動態根據賬號密碼獲得令牌,並根據配置信息的鍵從緩存中獲得令牌,設置到對應的對象上去,完成了令牌的信息設置,這樣ABP VNext動態客戶端的代理接口類,就可以正常調用獲得數據了。
數據記錄展示如下。
這樣,整個測試的例子就完成了多個Winform窗體的生成和調用展示,並通過令牌的處理,完成了客戶端的IDS4授權,可以正常調用動態客戶端的接口類,完美解決了相關的技術點了。