【复杂系统迁移 .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>

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

  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<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 AuthorizationMiddleware
    var 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 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对认证和授权进行分析的部分。

    因为只是使用来说已经比较简单这里不做过多的描述。这里有一个点需要注意,在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的方式有如下考虑:

  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基类如下:

//其他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平台系列】之静态文件

招商城科走进武汉研发中心,现场编码解锁平台内核技术

如何解决大批量数据保存的性能问题

发布了1537 篇原创文章 · 获赞 631 · 访问量 263万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览