【复杂系统迁移 .NET Core平台系列】之认证和授权
源寶導讀:微軟跨平臺技術框架—.NET Core已經日趨成熟,已經具備了支撐大型系統穩定運行的條件。本文將介紹明源云ERP平臺從.NET Framework向.NET Core遷移過程中的實踐經驗。
一、背景
? ?隨著ERP的產品線越來越多,業務關聯也日益復雜,應用間依賴關系也變得錯綜復雜,單體架構的弱點日趨明顯。19年初,由于平臺底層支持了分應用部署模式,將ERP從應用子系統層面進行了切割分離,邁出了從單體架構向微服務架構轉型的堅實一步。不久的將來,ERP會進一步將各業務拆分成眾多的微服務,而微服務勢必需要進行容器化部署和運行管理,這就要求ERP技術底層必須支持跨平臺,所以將現有ERP系統從.NET Framework遷移到 .NET Core平臺勢在必行。
? ? 上一篇我們講述了Erp改造.Net Core之靜態文件,這一篇我們將講述在認證和授權改造過程中遇到的問題和解決思路。
二、關于認證和授權
? ? 權限控制是應用程序中最常見的需求,無論是在Asp.Net還是Asp.Net Core中都有相應的支持,而權限控制在框架層面支持主要是兩部分,身份認證和身份授權:
身份認證:識別當前請求的用戶信息,一般是通過加密的Cookies實現。
身份授權:識別當前請求是否有訪問指定資源的權限,一般是根據當前請求識別的用戶信息,結合角色權限相關配置來判斷。
三、Asp.Net認證和授權的實現
? ? 這里不區分Asp.Net WebForm和Asp.Net MVC,因為兩者都是基于HttpModule來進行身份認證和授權。
? ?在Asp.Net中認證是通過FormsAuthenticationModule實現,授權是通過UrlAuthorizationModule實現,需要在web.config中做如下配置:
<system.web><authentication mode="Forms"><forms loginUrl="/PubPlatform/Login/index.aspx" name="userToken" defaultUrl="/PubPlatform/Nav/Home/Default.aspx" timeout="300" /></authentication><authorization><deny users="?" /><!--禁止匿名用戶的訪問--></authorization> </system.web><!--管理員有modeling目錄的訪問權限--> <location path="modeling"><system.web><authorization><allow roles="Admin"/><deny users="*"/></authorization></system.web> </location>? ? 簡述一下用戶訪問網站授權一個流程:
假設用戶訪問首頁/PubPlatform/Nav/Home/Default.aspx。
UrlAuthorizationModule 根據禁止匿名用戶訪問的配置,要求用戶進行身份認證。
跳轉到登陸頁面/ PubPlatform/ Login/index.aspx進行身份認證。
服務端判斷用戶輸入用戶密碼信息。
如果驗證通過將認證信息寫入Repsonse的Cookies中并跳轉到/PubPlatform/Nav/Home/Default.aspx。
UrlAuthorizationModule 根據配置發現已經有用戶信息直接,開始進入到HttpHandler處理程序。
PS:整個認證過程遠比這個復雜,因為本文重點不在這里,只做簡單的描述,了解基本流程即可,有興趣可以參考https://www.cnblogs.com /fish-li/archive/2012/04/15/2450571.html。
? ? 第二段配置中,通過用戶所在的組來控制是否有modeling目錄的訪問權限,如果需要更多的權限配置可以添加更多類似的配置,實際在使用過程中很少使用這種方式,因為用戶角色和頁面會有很多,配置會很多而且不能實現動態配置,所以一般是在應用程序中處理,在Webform中一般采用類似如下方式:
public class BacePage:Page {protected override void OnLoad(EventArgs e){//在這里根據HttpContext.User來獲取用戶信息然后獲取角色權限來判斷當前資源可否訪問base.OnLoad(e);} }? ? 在Mvc中一般采用繼承自AuthorizationFilterAttribute來處理,邏輯跟上述代碼中演示的類似。
四、Asp.Net Core
? ? 在Core中引入Authentication需要在Startup.cs進行引入,一般和老版本一樣使用Cookies做認證信息的承載,承載的代碼如下:
public void ConfigureServices(IServiceCollection services) {...services.AddAuthentication("Cookies").AddCookie("Cookies");... } public void Configure(IApplicationBuilder app, IHostingEnvironment env) {...app.UseAuthentication();... }? ? 在ConfigureServices(應用初始化過程),引入了Cookies作為默認的認證方式,在Configure(管道初始化)過程中引入認證的中間件。下面會簡要分析下認證和授權的過程:
請求進入Authentication Middleware中間件之后首先會IAuthentication RequestHandler 接口做權限判斷,這個組件主要使用來做遠程認證的,例如SSO登錄的場景。然后再做默認的認證:以Cookies認證為例,主要是讀取Cookies的信息到HttpContext.User中。
經過中間件之后會進入AuthorizeFilter中(這里和認證和授權的核心地方)。
獲取全局,Controller,Action之上的元數據計算之后得出AuthorizationPolicy。
通過IPolicyEvaluator調用Authentication模塊進行認證信息解析。
通過IPolicyEvaluator攜帶Authentication結果調用Authentization模塊進行授權信息解析。
? ? 核心代碼如下:
//AuthenticationMiddleware 部分代碼 public async Task Invoke(HttpContext context) {context.Features.Set(new AuthenticationFeature{OriginalPath = context.Request.Path,OriginalPathBase = context.Request.PathBase});// Give any IAuthenticationRequestHandler schemes a chance to handle the requestvar handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()){var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;if (handler != null && await handler.HandleRequestAsync()){return;}}var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();if (defaultAuthenticate != null){var result = await context.AuthenticateAsync(defaultAuthenticate.Name);if (result?.Principal != null){context.User = result.Principal;}}await _next(context); } // AuthorizeFilte部分代碼 public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context) {if(context == null){throw new ArgumentNullException(nameof(context));}if (!context.IsEffectivePolicy(this)){return;}// IMPORTANT: Changes to authorization logic should be mirrored in security's AuthorizationMiddlewarevar effectivePolicy = await GetEffectivePolicyAsync(context);if (effectivePolicy == null){return;}var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext);// Allow Anonymous skips all authorizationif (HasAllowAnonymous(context)){return;}var authorizeResult = await policyEvaluator.AuthorizeAsync(effectivePolicy, authenticateResult, context.HttpContext, context);if (authorizeResult.Challenged){context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray());}else if (authorizeResult.Forbidden){context.Result = new ForbidResult(effectivePolicy.AuthenticationSchemes.ToArray());} }? ? 如果有興趣的話可以繼續往里面跟蹤一下代碼發現最終起作用的是IAuthentication Handler和IAuthorizationHandler,而最終這兩個Handler的引入在Core中對應兩個概念:
AuthenticationScheme 認證計劃。
AuthorizationRequirement 授權條件。
? ? 最終Core的認證和授權一般都是通過這兩個來進行擴展,這里特別說明一點,在Mvc Core中將身份認證和授權進行了統一的規劃。因為在認證過程中,MVC Core只是只是解析,但并不實際將Http請求進行返回處理。實際過程是在AuthorizationRequirement進行處理的,這里特別提到Mvc Core中的默認授權DenyAnonymousAuthorizationRequirement,通過這個授權條件判斷是否有認證的用戶信息來進行權限的判斷。
資源能否訪問這個理解為授權,那么身份認證只是授權的一種方式。
? ? 在Asp.Net WebFrom中使用基于HttpModule進行身份認證會丟失掉很多元數據的信息,因為最終方法的執行是在HttpHandler上,而HttpModule并不知道那個Handler也不知道是那個方法,就會大量使用HttpContext的解析,提高了復雜度。
? ? 如果在HttpModule中進行,如果有多種身份認證方式,那么無論最終資源需要何種方式都會經過很多HttpModule,走了無謂的分支。
? ? 另外Webfrom中授權一般寫在基類中,基類必定會引入不同的繼承分支,這樣也不便于維護.對比Mvc Core中使用的特性標記+策略者的使用方式,更加利于維護和擴展,而不用對原有邏輯進行變動。
? ? 對比Asp.Net MVC中其中有IAuthenticationFilter和IAuthorizationFilter來處理授權和認證,然而在Asp.Net MVC Core中 只有IAuthorizationFilter。
? ? 對于資源的訪問應該只有可否訪問的判斷即授權,而是否身份認證只能說是判斷授權一種常用方式,在MVC Core中將認證通過DenyAnonymousAuthorizationRequirement融入授權以后,真正的將授權和認證融為一體,而非之前看似兩個并行的概念,相互不同的處理邏輯。
? ? 在Mvc Core通過AOP進行接入,然后一系列的封裝,最終將身份和授權通過Scheme + IAuthrenticationHandler和Requirement + AuthorizationHandler,這種類似于策略者模式的方式,對外提供擴展。更加易于擴展,而實際并不用太多考慮AOP部分的邏輯。最終授權和認證之需要我們進行Attribute標記然后MVC Core會自動加載到自己的元數據中,另外在RazorPage還可以主動調用如下方法進行授權的添加:
//對目錄進行授權 RazorPageOptions.Conventions.AuthorizeFolder(); //對頁面進行授權 RazorPageOptions.Convertions.AuthorizePage();五、ERP認證和授權
? ? ERP頁面的部分使用傳統webform的認證體系,而提供的接口部分使用了自定義HttpModule的方式,由于很多ERP演化升級過程中被動接受了很多身份認證方式的接入,所以原本實現授權的HttoModule的邏輯變得異常復雜,而這一次在改造Core的過程中進行了全面的改造后面會進行講述。同樣授權也是一樣,將原本實現在Page基類中的復雜的判斷邏輯進行了改造。
? ? ERP在pub接口的身份認證方面需要實現對如下不同的方式:
Pub接口
內部集成
接口管家
Mip集成
OA集成
? ? 之前講過ERP是通過元數據來驅動的頁面邏輯,權限實際是通過通過元數據配置到界面元素上的權限點來進行標識。通過常用的用戶-角色-權限的模型模型來關聯用戶和權限點,然后通過在Page的基類中通過拿到用戶的權限來判斷是否有當前URL的訪問權限,在按鈕渲染的時候來獲取是否有該按鈕的權限點來控制是否顯示。
? ? 在這次的改造過程中我們進行了如下改造:
Mvc Core CookiesAuthentication中間件來實現頁面的身份認證。
擴展MVC Core 的 AuthenticationScheme來實現接口的身份認證。
擴展MVC Core 的 Authentization Policy +Requirement來實現頁面的身份授權。
下面通過這三部分進行講述。
5.1、CookiesAuthentication身份認證
? ? MVC Core 自帶的基于Cookies的身份認證已經能滿足我們的需求,只需要稍微進行一下配置即可。通過一張圖我們了解下整個的身份認證流程,使用代碼參考本章開頭Asp.Net Core對認證和授權進行分析的部分。
? ? 因為只是使用來說已經比較簡單這里不做過多的描述。這里有一個點需要注意,在Cookie的認證中如果是負載均衡的場景,兩個不同進程的引用需要都可以進行客戶端Cookies的解密。那么就需要共享加解密的key,這里我們使用MVC Core自帶的基于Entity Framework Core 的密鑰持久化方式,通過存放到數據庫來實現密鑰的共享。這部分內容可以參考:https://docs.microsoft.com/zh-cn/aspnet/core/security/data-protection/implementation/key-storage-providers?view=aspnetcore-3.1&tabs=visual-studio。
5.2、Pub接口的身份認證
? ? 除了頁面訪問以及頁面ajax請求這些事基于Cookies的身份認證之外,在提供的接口用了其他的認證方式,這次將所有接口的認證方式進行了抽象,統一使用OpenApi的Scheme進行認證,而實際各種不同的認證方式我們是通過實入AuthenticationMiddleware中調用的IAuthenticationRequestHandler來實現的。首先我們看一下執行的流程:
? ? 之前有一篇關于Mvc Core的Controller改造的文章,其中將pub接口的發現和特性路由擴展進行了擴展,這里我們在擴展的地方添加OpenApi類型Scheme的元數據。
? ? 因為我們的接口統一是使用/pub/開頭的所以我們選擇使用擴展IAuthentication RequestHandler來實現,這樣在判斷是否要進行認證的時候需要判斷url是否以pub開頭即可,下面是類圖:
? ? 其中核心就是MockUserAuthHandler,因為在Erp的接口中都會用到用戶相關的信息,所以我們所有的pub接口操作實際的身份認證都是模擬了用戶的認證,這里將這個概念顯式的移動到基類之中,我們通過如下代碼進行接入:
public static AuthenticationBuilder AddOpenApiAuth(this AuthenticationBuilder builder) {builder.AddScheme(AppConst.ApiSchemeName, AppConst.ApiSchemeName,null);return builder.AddScheme<ApiAuthOptions, OpenApiAuthHandler>(AppConst.OpenApi, AppConst.OpenApi, null).AddScheme<ApiAuthOptions, AccessTokenAuthHandler>(AppConst.AccessToken, AppConst.AccessToken, null).AddScheme<ApiAuthOptions, OAuthV1AuthHandler>(AppConst.OAuthV1, AppConst.OAuthV1, null).AddScheme<ApiAuthOptions, OAuthV2AuthHandler>(AppConst.OAuthV2, AppConst.OAuthV2, null).AddScheme<ApiAuthOptions, InternalIntegrationAuthHandler>(AppConst.InternalIntegration, AppConst.InternalIntegration, null); }? ? 這里我們添加了眾多的Scheme只是為了在AuthenticationMiddleware中處理請求并進行模擬用戶登錄,并未有實際的Action上有此Sheme,在Action上使用的只有AppConst.ApiSchemeName(“OpenApiScheme”)。
? ? 在Cookies的認證的流程中因為是在登錄過程中將認證信息寫到Cookies中,而其他請求只需要進行Cookies解析即可獲得用戶信息。而pub接口請求是在一個用戶請求過程中完成整個過程,實際是通過解析Header的特定內容,然后跟保存在Erp中的認證信息進行對比來確定身份,最后在模擬用戶之后,進行登錄操作來完成整個身份認證的流程。
? ? 這里我們采用擴展IAuthentication RequestHandler的方式有如下考慮:
如果采用IAuthenticationHandler + Sheme,Core的標準是按照不同的請求使用不同Scheme無法支持,而實際來說都是通過pub接口進行區分,并沒有區分到具體的請求。
目前ERP的認證方式跟行業標準方案不同,以后如果接入標準方案,增加Scheme更加容易擴展并兼容現有方案。
IAuthenticationRequestHandler官方給出是遠程認證時候使用,在以后前后端分離加上微服務的升級過程中,只需要修改這部分請求中認證信息發送給遠程服務器認證即可,更加符合官方推薦使用的風格。
? 綜合來說采取IAuthentication RequestHandler而不采用IAuthenticationHandler + Sheme 方式,還是基于官方給出的推薦使用風格和Erp擴展需求綜合考慮而來。
5.3、Razor頁面授權
? ? 在頁面改造篇我們講述了頁面改造的過程,我們知道RazorPage實際是類似CodeBehind的模式,通過PageModel的OnGet方法,每個頁面都有對應的PageModel,所以我們這里定義的BasePageModel基類如下:
//其他Attribute [Authorize(Policy = AppConst.UserAuth,AuthenticationSchemes = “Cookies”)] public abstract class BasePageModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel {protected readonly MapPageContext _mapPageContext;protected BasePageModel(MapPageContext mapPageContext){_mapPageContext = mapPageContext;VerifyApplication.CheckStatus();} }? ? 這里我們使用Cookies的Scheme進行授權,特別的是我們還增加了AuthentizationPolicy的定義,通過定義的Policy加上PolicyProvider來實現不同的Policy的切換,Policy的使用代碼如下:
public static IMvcBuilder AddFrontend(this IMvcBuilder builder) {builder.AddRazorPagesOptions(options =>{//…//建模的權限options.Conventions.AuthorizeFolder(“/modeling”, AppConst.ModelingAuth);});//頁面權限builder.Services.AddMapAuth();//... } //注冊 private static void AddMapAuth(this IServiceCollection services) {services.AddSingleton();services.TryAddEnumerable(ServiceDescriptor.Transient());services.TryAddEnumerable(ServiceDescriptor.Transient());services.TryAddEnumerable(ServiceDescriptor.Transient()); }//Policy實現 internal class MapAuthPolicyProvider : IAuthorizationPolicyProvider{private DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }public MapAuthPolicyProvider(IOptions<AuthorizationOptions> options){FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);}public Task<AuthorizationPolicy> GetPolicyAsync(string policyName){if (policyName.StartsWith(AppConst.ModelingAuth, StringComparison.OrdinalIgnoreCase)){var policy = new AuthorizationPolicyBuilder();policy.AddRequirements(new ModelingRequirement());return Task.FromResult(policy.Build());}if (policyName.StartsWith(AppConst.AppAuth, StringComparison.OrdinalIgnoreCase)){var policy = new AuthorizationPolicyBuilder();policy.AddRequirements(new AppAuthRequirement());return Task.FromResult(policy.Build());}if (policyName.StartsWith(AppConst.UserAuth, StringComparison.OrdinalIgnoreCase)){var policy = new AuthorizationPolicyBuilder();policy.AddRequirements(new UserAuthRequirement());return Task.FromResult(policy.Build());}//返回默認的認證return FallbackPolicyProvider.GetPolicyAsync(policyName);}public Task<AuthorizationPolicy> GetDefaultPolicyAsync(){return FallbackPolicyProvider.GetDefaultPolicyAsync();} }? ? 通過上述代碼實現了不同策略使用不同的Requirement進行授權的校驗,至于授權校驗邏輯就是常見的用戶權限判斷。下面是整體的執行流程:
六、總結
? ? Core認證和授權改造主要是要對Core的認證和授權的邏輯要清晰,這里核心就是去扒源碼,由于之前對Asp.Net MVC的了解,直接就從AuthorizeFilter入手很快就可以找到切入點,然后了解到整個Core認證和授權的核心。
? ? 權限這部分的改造不僅僅只是代碼的遷移,也是風格的一個改造,將原來認證和授權的大泥球的設計,通過Core提供的Scheme+Requirement的擴展進行結合的重新設計。
------ END ------
作者簡介
熊同學:?研發工程師,目前負責ERP運行平臺的設計與開發工作。
也許您還想看
【復雜系統遷移 .NET Core平臺系列】之遷移項目工程
【復雜系統遷移 .NET Core平臺系列】之界面層
【復雜系統遷移 .NET Core平臺系列】之靜態文件
招商城科走進武漢研發中心,現場編碼解鎖平臺內核技術
如何解決大批量數據保存的性能問題
總結
以上是生活随笔為你收集整理的【复杂系统迁移 .NET Core平台系列】之认证和授权的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 优秀的开发者从命名开始
- 下一篇: .NET Core开发实战(第30课:领