rsviwe32 7.6 授权_「复杂系统迁移 .NET Core平台系列」之认证和授权
源寶導讀:微軟跨平臺技術框架—.NET Core已經日趨成熟,已經具備了支撐大型系統(tǒng)穩(wěn)定運行的條件。本文將介紹明源云ERP平臺從.NET Framework向.NET Core遷移過程中的實踐經驗。
一、背景
隨著ERP的產品線越來越多,業(yè)務關聯(lián)也日益復雜,應用間依賴關系也變得錯綜復雜,單體架構的弱點日趨明顯。19年初,由于平臺底層支持了分應用部署模式,將ERP從應用子系統(tǒng)層面進行了切割分離,邁出了從單體架構向微服務架構轉型的堅實一步。不久的將來,ERP會進一步將各業(yè)務拆分成眾多的微服務,而微服務勢必需要進行容器化部署和運行管理,這就要求ERP技術底層必須支持跨平臺,所以將現(xiàn)有ERP系統(tǒng)從.NET Framework遷移到 .NET Core平臺勢在必行。
上一篇我們講述了Erp改造.Net Core之靜態(tài)文件,這一篇我們將講述在認證和授權改造過程中遇到的問題和解決思路。
二、關于認證和授權
權限控制是應用程序中最常見的需求,無論是在Asp.Net還是Asp.Net Core中都有相應的支持,而權限控制在框架層面支持主要是兩部分,身份認證和身份授權:
- 身份認證:識別當前請求的用戶信息,一般是通過加密的Cookies實現(xiàn)。
- 身份授權:識別當前請求是否有訪問指定資源的權限,一般是根據(jù)當前請求識別的用戶信息,結合角色權限相關配置來判斷。
三、Asp.Net認證和授權的實現(xiàn)
這里不區(qū)分Asp.Net WebForm和Asp.Net MVC,因為兩者都是基于HttpModule來進行身份認證和授權。
在Asp.Net中認證是通過FormsAuthenticationModule實現(xiàn),授權是通過UrlAuthorizationModule實現(xiàn),需要在web.config中做如下配置:
簡述一下用戶訪問網站授權一個流程:
PS:整個認證過程遠比這個復雜,因為本文重點不在這里,只做簡單的描述,了解基本流程即可,有興趣可以參考https://www.cnblogs.com /fish-li/archive/2012/04/15/2450571.html。
第二段配置中,通過用戶所在的組來控制是否有modeling目錄的訪問權限,如果需要更多的權限配置可以添加更多類似的配置,實際在使用過程中很少使用這種方式,因為用戶角色和頁面會有很多,配置會很多而且不能實現(xiàn)動態(tài)配置,所以一般是在應用程序中處理,在Webform中一般采用類似如下方式:
public class BacePage:Page
{
protected override void OnLoad(EventArgs e)
{
//在這里根據(jù)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(管道初始化)過程中引入認證的中間件。下面會簡要分析下認證和授權的過程:
核心代碼如下:
//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 request
var handlers = context.RequestServices.GetRequiredService();
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 AuthorizationMiddleware
var effectivePolicy = await GetEffectivePolicyAsync(context);
if (effectivePolicy == null)
{
return;
}
var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService();
var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext);
// Allow Anonymous skips all authorization
if (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());
}
}
如果有興趣的話可以繼續(xù)往里面跟蹤一下代碼發(fā)現(xiàn)最終起作用的是IAuthentication Handler和IAuthorizationHandler,而最終這兩個Handler的引入在Core中對應兩個概念:
- AuthenticationScheme 認證計劃。
- AuthorizationRequirement 授權條件。
最終Core的認證和授權一般都是通過這兩個來進行擴展,這里特別說明一點,在Mvc Core中將身份認證和授權進行了統(tǒng)一的規(guī)劃。因為在認證過程中,MVC Core只是只是解析,但并不實際將Http請求進行返回處理。實際過程是在AuthorizationRequirement進行處理的,這里特別提到Mvc Core中的默認授權DenyAnonymousAuthorizationRequirement,通過這個授權條件判斷是否有認證的用戶信息來進行權限的判斷。
資源能否訪問這個理解為授權,那么身份認證只是授權的一種方式。
在Asp.Net WebFrom中使用基于HttpModule進行身份認證會丟失掉很多元數(shù)據(jù)的信息,因為最終方法的執(zhí)行是在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會自動加載到自己的元數(shù)據(jù)中,另外在RazorPage還可以主動調用如下方法進行授權的添加:
//對目錄進行授權
RazorPageOptions.Conventions.AuthorizeFolder();
//對頁面進行授權
RazorPageOptions.Convertions.AuthorizePage();
五、ERP認證和授權
ERP頁面的部分使用傳統(tǒng)webform的認證體系,而提供的接口部分使用了自定義HttpModule的方式,由于很多ERP演化升級過程中被動接受了很多身份認證方式的接入,所以原本實現(xiàn)授權的HttoModule的邏輯變得異常復雜,而這一次在改造Core的過程中進行了全面的改造后面會進行講述。同樣授權也是一樣,將原本實現(xiàn)在Page基類中的復雜的判斷邏輯進行了改造。
ERP在pub接口的身份認證方面需要實現(xiàn)對如下不同的方式:
- Pub接口
- 內部集成
- 接口管家
- Mip集成
- OA集成
之前講過ERP是通過元數(shù)據(jù)來驅動的頁面邏輯,權限實際是通過通過元數(shù)據(jù)配置到界面元素上的權限點來進行標識。通過常用的用戶-角色-權限的模型模型來關聯(lián)用戶和權限點,然后通過在Page的基類中通過拿到用戶的權限來判斷是否有當前URL的訪問權限,在按鈕渲染的時候來獲取是否有該按鈕的權限點來控制是否顯示。
在這次的改造過程中我們進行了如下改造:
下面通過這三部分進行講述。
5.1、CookiesAuthentication身份認證
MVC Core 自帶的基于Cookies的身份認證已經能滿足我們的需求,只需要稍微進行一下配置即可。通過一張圖我們了解下整個的身份認證流程,使用代碼參考本章開頭Asp.Net Core對認證和授權進行分析的部分。
因為只是使用來說已經比較簡單這里不做過多的描述。這里有一個點需要注意,在Cookie的認證中如果是負載均衡的場景,兩個不同進程的引用需要都可以進行客戶端Cookies的解密。那么就需要共享加解密的key,這里我們使用MVC Core自帶的基于Entity Framework Core 的密鑰持久化方式,通過存放到數(shù)據(jù)庫來實現(xiàn)密鑰的共享。這部分內容可以參考: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的身份認證之外,在提供的接口用了其他的認證方式,這次將所有接口的認證方式進行了抽象,統(tǒng)一使用OpenApi的Scheme進行認證,而實際各種不同的認證方式我們是通過實入AuthenticationMiddleware中調用的IAuthenticationRequestHandler來實現(xiàn)的。首先我們看一下執(zhí)行的流程:
之前有一篇關于Mvc Core的Controller改造的文章,其中將pub接口的發(fā)現(xiàn)和特性路由擴展進行了擴展,這里我們在擴展的地方添加OpenApi類型Scheme的元數(shù)據(jù)。
因為我們的接口統(tǒng)一是使用/pub/開頭的所以我們選擇使用擴展IAuthentication RequestHandler來實現(xiàn),這樣在判斷是否要進行認證的時候需要判斷url是否以pub開頭即可,下面是類圖:
其中核心就是MockUserAuthHandler,因為在Erp的接口中都會用到用戶相關的信息,所以我們所有的pub接口操作實際的身份認證都是模擬了用戶的認證,這里將這個概念顯式的移動到基類之中,我們通過如下代碼進行接入: 這里我們添加了眾多的Scheme只是為了在AuthenticationMiddleware中處理請求并進行模擬用戶登錄,并未有實際的Action上有此Sheme,在Action上使用的只有AppConst.ApiSchemeName(“OpenApiScheme”)。
在Cookies的認證的流程中因為是在登錄過程中將認證信息寫到Cookies中,而其他請求只需要進行Cookies解析即可獲得用戶信息。而pub接口請求是在一個用戶請求過程中完成整個過程,實際是通過解析Header的特定內容,然后跟保存在Erp中的認證信息進行對比來確定身份,最后在模擬用戶之后,進行登錄操作來完成整個身份認證的流程。
這里我們采用擴展IAuthentication RequestHandler的方式有如下考慮:
綜合來說采取IAuthentication RequestHandler而不采用IAuthenticationHandler + Sheme 方式,還是基于官方給出的推薦使用風格和Erp擴展需求綜合考慮而來。
5.3、Razor頁面授權
在頁面改造篇我們講述了頁面改造的過程,我們知道RazorPage實際是類似CodeBehind的模式,通過PageModel的OnGet方法,每個頁面都有對應的PageModel,所以我們這里定義的BasePageModel基類如下:
public static AuthenticationBuilder AddOpenApiAuth(this AuthenticationBuilder builder)
{
builder.AddScheme(AppConst.ApiSchemeName, AppConst.ApiSchemeName,null);
return builder.AddScheme(AppConst.OpenApi, AppConst.OpenApi, null)
.AddScheme(AppConst.AccessToken, AppConst.AccessToken, null)
.AddScheme(AppConst.OAuthV1, AppConst.OAuthV1, null)
.AddScheme(AppConst.OAuthV2, AppConst.OAuthV2, null)
.AddScheme(AppConst.InternalIntegration, AppConst.InternalIntegration, null);
}
這里我們使用Cookies的Scheme進行授權,特別的是我們還增加了AuthentizationPolicy的定義,通過定義的Policy加上PolicyProvider來實現(xiàn)不同的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實現(xiàn)
internal class MapAuthPolicyProvider : IAuthorizationPolicyProvider
{
private DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public MapAuthPolicyProvider(IOptions options)
{
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task 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 GetDefaultPolicyAsync()
{
return FallbackPolicyProvider.GetDefaultPolicyAsync();
}
}
通過上述代碼實現(xiàn)了不同策略使用不同的Requirement進行授權的校驗,至于授權校驗邏輯就是常見的用戶權限判斷。下面是整體的執(zhí)行流程:
六、總結
Core認證和授權改造主要是要對Core的認證和授權的邏輯要清晰,這里核心就是去扒源碼,由于之前對Asp.Net MVC的了解,直接就從AuthorizeFilter入手很快就可以找到切入點,然后了解到整個Core認證和授權的核心。
權限這部分的改造不僅僅只是代碼的遷移,也是風格的一個改造,將原來認證和授權的大泥球的設計,通過Core提供的Scheme+Requirement的擴展進行結合的重新設計。
------ END ------
作者簡介
熊同學: 研發(fā)工程師,目前負責ERP運行平臺的設計與開發(fā)工作。
公眾號:明源技術
總結
以上是生活随笔為你收集整理的rsviwe32 7.6 授权_「复杂系统迁移 .NET Core平台系列」之认证和授权的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 玉米要煮多久才会熟 完美煮玉米的技巧和注
- 下一篇: 肉末蒸蛋要多久 做法详解?