rsviwe32 7.6 授权_「复杂系统迁移 .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中做如下配置:

简述一下用户访问网站授权一个流程:

  1. 假设用户访问首页/PubPlatform/Nav/Home/Default.aspx。
  2. UrlAuthorizationModule 根据禁止匿名用户访问的配置,要求用户进行身份认证。
  3. 跳转到登陆页面/ PubPlatform/ Login/index.aspx进行身份认证。
  4. 服务端判断用户输入用户密码信息。
  5. 如果验证通过将认证信息写入Repsonse的Cookies中并跳转到/PubPlatform/Nav/Home/Default.aspx。
  6. 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(管道初始化)过程中引入认证的中间件。下面会简要分析下认证和授权的过程:

  1. 请求进入Authentication Middleware中间件之后首先会IAuthentication RequestHandler 接口做权限判断,这个组件主要使用来做远程认证的,例如SSO登录的场景。然后再做默认的认证:以Cookies认证为例,主要是读取Cookies的信息到HttpContext.User中。
  2. 经过中间件之后会进入AuthorizeFilter中(这里和认证和授权的核心地方)。
  3. 获取全局,Controller,Action之上的元数据计算之后得出AuthorizationPolicy。
  4. 通过IPolicyEvaluator调用Authentication模块进行认证信息解析。
  5. 通过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 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());

}

}

如果有兴趣的话可以继续往里面跟踪一下代码发现最终起作用的是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的访问权限,在按钮渲染的时候来获取是否有该按钮的权限点来控制是否显示。

在这次的改造过程中我们进行了如下改造:

  1. Mvc Core CookiesAuthentication中间件来实现页面的身份认证。
  2. 扩展MVC Core 的 AuthenticationScheme来实现接口的身份认证。
  3. 扩展MVC Core 的 Authentization Policy +Requirement来实现页面的身份授权。

下面通过这三部分进行讲述。

5.1、CookiesAuthentication身份认证

MVC Core 自带的基于Cookies的身份认证已经能满足我们的需求,只需要稍微进行一下配置即可。通过一张图我们了解下整个的身份认证流程,使用代码参考本章开头Asp.Net Core对认证和授权进行分析的部分。

7d58782c5c0a5bf7606bd7af398823ea.png

因为只是使用来说已经比较简单这里不做过多的描述。这里有一个点需要注意,在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来实现的。首先我们看一下执行的流程:

a02207a5064551033db5f86006773510.png

之前有一篇关于Mvc Core的Controller改造的文章,其中将pub接口的发现和特性路由扩展进行了扩展,这里我们在扩展的地方添加OpenApi类型Scheme的元数据。

因为我们的接口统一是使用/pub/开头的所以我们选择使用扩展IAuthentication RequestHandler来实现,这样在判断是否要进行认证的时候需要判断url是否以pub开头即可,下面是类图:

3ca842e9ac4342cb5cff3e6f40eae993.png

其中核心就是MockUserAuthHandler,因为在Erp的接口中都会用到用户相关的信息,所以我们所有的pub接口操作实际的身份认证都是模拟了用户的认证,这里将这个概念显式的移动到基类之中,我们通过如下代码进行接入: 这里我们添加了众多的Scheme只是为了在AuthenticationMiddleware中处理请求并进行模拟用户登录,并未有实际的Action上有此Sheme,在Action上使用的只有AppConst.ApiSchemeName(“OpenApiScheme”)。

在Cookies的认证的流程中因为是在登录过程中将认证信息写到Cookies中,而其他请求只需要进行Cookies解析即可获得用户信息。而pub接口请求是在一个用户请求过程中完成整个过程,实际是通过解析Header的特定内容,然后跟保存在Erp中的认证信息进行对比来确定身份,最后在模拟用户之后,进行登录操作来完成整个身份认证的流程。

这里我们采用扩展IAuthentication RequestHandler的方式有如下考虑:

  1. 如果采用IAuthenticationHandler + Sheme,Core的标准是按照不同的请求使用不同Scheme无法支持,而实际来说都是通过pub接口进行区分,并没有区分到具体的请求。
  2. 目前ERP的认证方式跟行业标准方案不同,以后如果接入标准方案,增加Scheme更加容易扩展并兼容现有方案。
  3. IAuthenticationRequestHandler官方给出是远程认证时候使用,在以后前后端分离加上微服务的升级过程中,只需要修改这部分请求中认证信息发送给远程服务器认证即可,更加符合官方推荐使用的风格。

综合来说采取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来实现不同的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 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();

}

}

通过上述代码实现了不同策略使用不同的Requirement进行授权的校验,至于授权校验逻辑就是常见的用户权限判断。下面是整体的执行流程:

c57bd27124b5249c6f0960334a44c987.png

六、总结

Core认证和授权改造主要是要对Core的认证和授权的逻辑要清晰,这里核心就是去扒源码,由于之前对Asp.Net MVC的了解,直接就从AuthorizeFilter入手很快就可以找到切入点,然后了解到整个Core认证和授权的核心。

权限这部分的改造不仅仅只是代码的迁移,也是风格的一个改造,将原来认证和授权的大泥球的设计,通过Core提供的Scheme+Requirement的扩展进行结合的重新设计。

------ END ------

作者简介

熊同学: 研发工程师,目前负责ERP运行平台的设计与开发工作。

公众号:明源技术

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值