Error: Cannot find module '@vue/cli-plugin-babel'_前端段的博客-CSDN博客_@vue/cli-plugin-babel

mikel阅读(937)

来源: Error: Cannot find module ‘@vue/cli-plugin-babel’_前端段的博客-CSDN博客_@vue/cli-plugin-babel

运行npm run serve报错:
Error: Cannot find module ‘@vue/cli-plugin-babel’

解决办法:
1.安装开发依赖:
npm install babel-plugin-import -D

2.查看package.json里面的版本号,安装对应版本试试。
————————————————
版权声明:本文为CSDN博主「前端段」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/hkduan/article/details/105772792

使用手机控制PPT播放实现方法(含源码)_humbinal的博客-CSDN博客

mikel阅读(1140)

来源: 使用手机控制PPT播放实现方法(含源码)_humbinal的博客-CSDN博客

本文教你如何实现通过手机控制PC的幻灯片放映。

本篇文章是基于我的上一篇文章,上一篇文章中分享了这样一个小工具,这里把源码什么的分享出来。

本工具的制作使用Node.JS以及WebSocket技术,大致如下:

首先是搭建Node.JS平台,这里就不在说了,网上教程很多,我们从模块安装开始:

1.创建项目文件夹,在该文件夹下运行CMD,输入:

nmp install express

完成后继续:

npm install socket.io

以及:

npm install ejs

到这里我们需要使用的模块就装好了,我使用了EJS作为模板引擎配合express使用,socket.io用来进行WebSocket通信。

下载下面的附件一,解压后修改public文件夹中js文件夹下的script.js(第二行) 中的网址为你的网址即可。

下载附件二修改文件中src文件中remote.js(第三行)中的网址为你的网址即可。

这是启动你的服务就可以开始运行了,由于文件稍多,这里就不在一一分析具体的代码了,直接下载查看吧,有什么问题欢迎留言咨询。

这是可以手机打开sample.com;

放映PPT的电脑使用IE浏览器打开附件二中的ppt.html ;

具体使用详情见我的上一篇教程。

尽情的使用这样一款装逼神器吧!

源码下载地址:humbinal/ppt-controller (github.com)
————————————————
版权声明:本文为CSDN博主「humbinal」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012234419/article/details/45618517

ASP.NET Core MVC 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide - alby - 博客园

mikel阅读(819)

来源: ASP.NET Core MVC 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide – alby – 博客园

一、概述

ASP.NET Core MVC 提供了基于角色( Role )、声明( Chaim ) 和策略 ( Policy ) 等的授权方式。在实际应用中,可能采用部门( Department , 本文采用用户组 Group )、职位 ( 可继续沿用 Role )、权限( Permission )的方式进行授权。要达到这个目的,仅仅通过自定义 IAuthorizationPolicyProvider 是不行的。本文通过自定义 IApplicationModelProvide 进行扩展。

二、PermissionAuthorizeAttribute : IPermissionAuthorizeData

AuthorizeAttribute 类实现了 IAuthorizeData 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace Microsoft.AspNetCore.Authorization
{
  /// <summary>
  /// Defines the set of data required to apply authorization rules to a resource.
  /// </summary>
  public interface IAuthorizeData
  {
   /// <summary>
   /// Gets or sets the policy name that determines access to the resource.
   /// </summary>
   string Policy { get; set; }
   /// <summary>
   /// Gets or sets a comma delimited list of roles that are allowed to access the resource.
   /// </summary>
   string Roles { get; set; }
   /// <summary>
   /// Gets or sets a comma delimited list of schemes from which user information is constructed.
   /// </summary>
   string AuthenticationSchemes { get; set; }
  }
}

使用 AuthorizeAttribute 不外乎如下几种形式:

1
2
3
4
[Authorize]
[Authorize("SomePolicy")]
[Authorize(Roles = "角色1,角色2")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

当然,参数还可以组合起来。另外,Roles 和 AuthenticationSchemes 的值以半角逗号分隔,是 Or 的关系;多个 Authorize 是 And 的关系;Policy 、Roles 和 AuthenticationSchemes 如果同时使用,也是 And 的关系。

如果要扩展 AuthorizeAttribute,先扩展 IAuthorizeData 增加新的属性:

1
2
3
4
5
public interface IPermissionAuthorizeData : IAuthorizeData
{
    string Groups { get; set; }
    string Permissions { get; set; }
}

然后定义 AuthorizeAttribute:

1
2
3
4
5
6
7
8
9
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionAuthorizeAttribute : Attribute, IPermissionAuthorizeData
{
    public string Policy { get; set; }
    public string Roles { get; set; }
    public string AuthenticationSchemes { get; set; }
    public string Groups { get; set; }
    public string Permissions { get; set; }
}

现在,在 Controller 或 Action 上就可以这样使用了:

1
2
3
[PermissionAuthorize(Roles = "经理,副经理")] // 经理或部门经理
[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理"] // 研发部经理或生成部经理。Groups 和 Roles 是 `And` 的关系。
[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] // 研发部经理或生成部经理,并且有请假审批的权限。Groups 、Roles 和 Permission 是 `And` 的关系。

数据已经准备好,下一步就是怎么提取出来。通过扩展 AuthorizationApplicationModelProvider 来实现。

三、PermissionAuthorizationApplicationModelProvider : IApplicationModelProvider

AuthorizationApplicationModelProvider 类的作用是构造 AuthorizeFilter 对象放入 ControllerModel 或 ActionModel 的 Filters 属性中。具体过程是先提取 Controller 和 Action 实现了 IAuthorizeData 接口的 Attribute,如果使用的是默认的DefaultAuthorizationPolicyProvider,则会先创建一个 AuthorizationPolicy 对象作为 AuthorizeFilter 构造函数的参数。
创建 AuthorizationPolicy 对象是由 AuthorizationPolicy 的静态方法 public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 来完成的。该静态方法会解析 IAuthorizeData 的数据,但不懂解析 IPermissionAuthorizeData

因为 AuthorizationApplicationModelProvider 类对 AuthorizationPolicy.CombineAsync 静态方法有依赖,这里不得不做一个类似的 PermissionAuthorizationApplicationModelProvider 类,在本类实现 CombineAsync 方法。暂且不论该方法放在本类是否合适的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
       public static AuthorizeFilter GetFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authData)
       {
           // The default policy provider will make the same policy for given input, so make it only once.
           // This will always execute synchronously.
           if (policyProvider.GetType() == typeof(DefaultAuthorizationPolicyProvider))
           {
               var policy = CombineAsync(policyProvider, authData).GetAwaiter().GetResult();
               return new AuthorizeFilter(policy);
           }
           else
           {
               return new AuthorizeFilter(policyProvider, authData);
           }
       }
       private static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
       {
           if (policyProvider == null)
           {
               throw new ArgumentNullException(nameof(policyProvider));
           }
           if (authorizeData == null)
           {
               throw new ArgumentNullException(nameof(authorizeData));
           }
           var policyBuilder = new AuthorizationPolicyBuilder();
           var any = false;
           foreach (var authorizeDatum in authorizeData)
           {
               any = true;
               var useDefaultPolicy = true;
               if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
               {
                   var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy);
                   if (policy == null)
                   {
                       //throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy));
                       throw new InvalidOperationException(nameof(authorizeDatum.Policy));
                   }
policyBuilder.Combine(policy);
                   useDefaultPolicy = false;
               }
               var rolesSplit = authorizeDatum.Roles?.Split(',');
               if (rolesSplit != null && rolesSplit.Any())
               {
                   var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                   policyBuilder.RequireRole(trimmedRolesSplit);
                   useDefaultPolicy = false;
               }
               if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum )
               {
                   var groupsSplit = permissionAuthorizeDatum.Groups?.Split(',');
                   if (groupsSplit != null && groupsSplit.Any())
                   {
                       var trimmedGroupsSplit = groupsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                       policyBuilder.RequireClaim("Group", trimmedGroupsSplit); // TODO: 注意硬编码
                       useDefaultPolicy = false;
                   }
                   var permissionsSplit = permissionAuthorizeDatum.Permissions?.Split(',');
                   if (permissionsSplit != null && permissionsSplit.Any())
                   {
                       var trimmedPermissionsSplit = permissionsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                       policyBuilder.RequireClaim("Permission", trimmedPermissionsSplit);// TODO: 注意硬编码
                       useDefaultPolicy = false;
                   }
               }
               var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
               if (authTypesSplit != null && authTypesSplit.Any())
               {
                   foreach (var authType in authTypesSplit)
                   {
                       if (!string.IsNullOrWhiteSpace(authType))
                       {
                           policyBuilder.AuthenticationSchemes.Add(authType.Trim());
                       }
                   }
               }
               if (useDefaultPolicy)
               {
policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
               }
           }
           return any ? policyBuilder.Build() : null;
       }

if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum ) 为扩展部分。

四、Startup

注册 PermissionAuthorizationApplicationModelProvider 服务,需要在 AddMvc 之后替换掉 AuthorizationApplicationModelProvider 服务。

1
2
services.AddMvc();
services.Replac(ServiceDescriptor.Transient<IApplicationModelProvider,PermissionAuthorizationApplicationModelProvider>());

五、Jwt 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
    [HttpGet]
    [Route("SignIn")]
    public async Task<ActionResult<string>> SignIn()
    {
        var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
        {
            // 备注:Claim Type: Group 和 Permission 这里使用的是硬编码,应该定义为类似于 ClaimTypes.Role 的常量;另外,下列模拟数据不一定合逻辑。
            new Claim(ClaimTypes.Name, "Bob"),
            new Claim(ClaimTypes.Role, "经理"),  // 注意:不能使用逗号分隔来达到多个角色的目的,下同。
            new Claim(ClaimTypes.Role, "副经理"),
            new Claim("Group", "研发部"),
            new Claim("Group", "生产部"),
            new Claim("Permission", "请假审批"),
            new Claim("Permission", "权限1"),
            new Claim("Permission", "权限2"),
        }, JwtBearerDefaults.AuthenticationScheme));
        var token = new JwtSecurityToken(
            "SignalRAuthenticationSample",
            "SignalRAuthenticationSample",
            user.Claims,
            expires: DateTime.UtcNow.AddDays(30),
            signingCredentials: SignatureHelper.GenerateSigningCredentials("1234567890123456"));
        return _tokenHandler.WriteToken(token);
    }
    [HttpGet]
    [Route("Test")]
    [PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] // 研发部经理或生成部经理,并且有请假审批的权限。Groups 、Roles 和 Permission 是 `And` 的关系。
    public async Task<ActionResult<IEnumerable<string>>> Test()
    {
        var user = HttpContext.User;
        return new string[] { "value1", "value2" };
    }
 }

六、问题

AuthorizeFilter 类显示实现了 IFilterFactory 接口的 CreateInstance 方法:

1
2
3
4
5
6
7
8
9
10
11
12
IFilterMetadata IFilterFactory.CreateInstance(IServiceProvider serviceProvider)
{
    if (Policy != null || PolicyProvider != null)
    {
        // The filter is fully constructed. Use the current instance to authorize.
        return this;
    }

    Debug.Assert(AuthorizeData != null);
    var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>();
    return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData);
}

竟然对 AuthorizationApplicationModelProvider.GetFilter 静态方法产生了依赖。庆幸的是,如果通过 AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 或 AuthorizeFilter(AuthorizationPolicy policy) 创建 AuthorizeFilter 对象不会产生什么不良影响。

七、下一步

[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] 这种形式还是不够灵活,哪怕用多个 Attribute, And 和 Or 的逻辑组合不一定能满足需求。可以在 IPermissionAuthorizeData 新增一个 Rule 属性,实现类似的效果:

1
[PermissionAuthorize(Rule = "(Groups:研发部,生产部)&&(Roles:请假审批||Permissions:超级权限)"]

通过 Rule 计算复杂的授权。

八、如果通过自定义 IAuthorizationPolicyProvider 实现?

另一种方式是自定义 IAuthorizationPolicyProvider ,不过还需要自定义 AuthorizeFilter。因为当不是使用 DefaultAuthorizationPolicyProvider 而是自定义 IAuthorizationPolicyProvider 时,AuthorizationApplicationModelProvider(或前文定义的 PermissionAuthorizationApplicationModelProvider)会使用 AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 创建 AuthorizeFilter 对象,而不是 AuthorizeFilter(AuthorizationPolicy policy)。这会造成 AuthorizeFilter 对象在 OnAuthorizationAsync 时会间接调用 AuthorizationPolicy.CombineAsync 静态方法。

这可以说是一个设计上的缺陷,不应该让 AuthorizationPolicy.CombineAsync 静态方法存在,哪怕提供个 IAuthorizationPolicyCombiner 也好。另外,上文提到的 AuthorizationApplicationModelProvider.GetFilter 静态方法同样不是一种好的设计。等微软想通吧。

参考资料

https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.1

 

排版问题:http://blog.tubumu.com/2018/11/28/aspnetcore-mvc-extend-authorization/

使用nginx后net core无法获取ip问题_qq_34897745的博客-CSDN博客

mikel阅读(760)

来源: 使用nginx后net core无法获取ip问题_qq_34897745的博客-CSDN博客

使用了nginx后net core获取ip地址居然全部是本地的地址,不是外网的地址

这是因为nginx转发了一次后,我们直接使用常规获取ip地址的方式就是本地的地址了

 

瞧瞧nginx的配置,然后找获取外网ip的方法

 

 

这里我们可以看到,我们配了一个real-ip,nginx会转发给你,通过请求的header获取就行了

context.HttpContext.Request.Headers[“X-Real-IP”].FirstOrDefault();

————————————————
版权声明:本文为CSDN博主「qq_34897745」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34897745/article/details/106093714

net core中获取用户请求ip地址_qq_34897745的博客-CSDN博客_.net core 获取请求ip

mikel阅读(783)

来源: net core中获取用户请求ip地址_qq_34897745的博客-CSDN博客_.net core 获取请求ip

方法一:通过注入来获取

先添加一个依赖注入

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
在控制器里边使用构造函数注入

private readonly IHttpContextAccessor _httpContextAccessor;
public TestController( IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
获取

//获取ip地址
string ipaddress = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();

方法二:直接获取

public void OnActionExecuting(ActionExecutingContext context)
{
//获取ip地址
string ipaddress = context.HttpContext.Connection.RemoteIpAddress.ToString();
}

但是这两种写法,使用了nginx后是无法访问的

使用了nginx后无法获取ip问题:

https://blog.csdn.net/qq_34897745/article/details/106093714
————————————————
版权声明:本文为CSDN博主「qq_34897745」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34897745/article/details/106093610

ASP.NET Core 认证与授权[6]:授权策略是怎么执行的? - 雨夜朦胧 - 博客园

mikel阅读(793)

来源: ASP.NET Core 认证与授权[6]:授权策略是怎么执行的? – 雨夜朦胧 – 博客园

在上一章中,详细介绍了 ASP.NET Core 中的授权策略,在需要授权时,只需要在对应的Controler或者Action上面打上[Authorize]特性,并指定要执行的策略名称即可,但是,授权策略是怎么执行的呢?怀着一颗好奇的心,忍不住来探索一下它的执行流程。

目录

  1. MVC中的授权
  2. IPolicyEvaluator
  3. IAuthorizationService

在《(上一章》中提到,AuthorizeAttribute只是一个简单的实现了IAuthorizeData接口的特性,并且在 ASP.NET Core 授权系统中并没有使用到它。我们知道在认证中,还有一个UseAuthentication扩展方法来激活认证系统,但是在授权中并没有类似的机制。

这是因为当我们使用[Authorize]通常是在MVC中,由MVC来负责激活授权系统。本来在这个系列的文章中,我并不想涉及到MVC的知识,但是为了能更好的理解授权系统的执行,就来简单介绍一下MVC中与授权相关的知识。

MVC中的授权

当我们使用MVC时,首先会调用MVC的AddMvc扩展方法,用来注册一些MVC相关的服务:

public static IMvcBuilder AddMvc(this IServiceCollection services)
{
    var builder = services.AddMvcCore();

    builder.AddAuthorization();

    ...
}

public static IMvcCoreBuilder AddAuthorization(this IMvcCoreBuilder builder)
{
    AddAuthorizationServices(builder.Services);
    return builder;
}

internal static void AddAuthorizationServices(IServiceCollection services)
{
    services.AddAuthenticationCore();
    services.AddAuthorization();
    services.AddAuthorizationPolicyEvaluator();

    services.TryAddEnumerable(
        ServiceDescriptor.Transient<IApplicationModelProvider, AuthorizationApplicationModelProvider>());
}

在上面AddAuthorizationServices中的前三个方法都属于 ASP.NET Core 《Security》项目中提供的扩展方法,其中前两个在前面几章已经介绍过了,对于AddAuthorizationPolicyEvaluator放到后面再来介绍,我们先来看一下MVC中的AuthorizationApplicationModelProvider

AuthorizationApplicationModelProvider

在MVC中有一个ApplicationModel的概念,它用来封装ControllerFilterApiExplorer等。对应的,在MVC中还提供了一系列的ApplicationModelProvider来初始化ApplicationModel的各个部分,而AuthorizationApplicationModelProvider就是用来初始化与授权相关的部分。

public class AuthorizationApplicationModelProvider : IApplicationModelProvider
{
    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {
        foreach (var controllerModel in context.Result.Controllers)
        {
            var controllerModelAuthData = controllerModel.Attributes.OfType<IAuthorizeData>().ToArray();
            if (controllerModelAuthData.Length > 0)
            {
                controllerModel.Filters.Add(GetFilter(_policyProvider, controllerModelAuthData));
            }
            foreach (var attribute in controllerModel.Attributes.OfType<IAllowAnonymous>())
            {
                controllerModel.Filters.Add(new AllowAnonymousFilter());
            }
            foreach (var actionModel in controllerModel.Actions)
            {
                var actionModelAuthData = actionModel.Attributes.OfType<IAuthorizeData>().ToArray();
                if (actionModelAuthData.Length > 0)
                {
                    actionModel.Filters.Add(GetFilter(_policyProvider, actionModelAuthData));
                }
                foreach (var attribute in actionModel.Attributes.OfType<IAllowAnonymous>())
                {
                    actionModel.Filters.Add(new AllowAnonymousFilter());
                }
            }
        }
    }
}

如上,首先查找每个Controller中实现了IAuthorizeData接口的特性,然后将其转化为AuthorizeFilter并添加到Controller的Filter集合中,紧接着再查找实现了IAllowAnonymous接口的特性,将其转化为AllowAnonymousFilter过滤器也添加到Filter集合中,然后以同样的逻辑查找Action上的特性并添加到Action的Filter集合中。

其中的关键点就是将IAuthorizeData(也就是通过我们熟悉的[Authorize]特性)转化为MVC中的AuthorizeFilter过滤器:

public static AuthorizeFilter GetFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authData)
{
    if (policyProvider.GetType() == typeof(DefaultAuthorizationPolicyProvider))
    {
        var policy = AuthorizationPolicy.CombineAsync(policyProvider, authData).GetAwaiter().GetResult();
        return new AuthorizeFilter(policy);
    }
    else
    {
        return new AuthorizeFilter(policyProvider, authData);
    }
}

CombineAsync在上一章的《AuthorizationPolicy》中已经介绍过了,我们往下看看AuthorizeFilter的实现。

AuthorizeFilter

在MVC中有一个AuthorizeFilter过滤器,类似我们在ASP.NET 4.x中所熟悉的[Authorize],它实现了IAsyncAuthorizationFilter接口,定义如下:

public class AuthorizeFilter : IAsyncAuthorizationFilter, IFilterFactory
{
    public AuthorizeFilter(AuthorizationPolicy policy) {}
    public AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) : this(authorizeData) {}
    public AuthorizeFilter(IEnumerable<IAuthorizeData> authorizeData) {}

    public IEnumerable<IAuthorizeData> AuthorizeData { get; }
    public AuthorizationPolicy Policy { get; }

    public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var effectivePolicy = Policy;
        if (effectivePolicy == null)
        {
            effectivePolicy = await AuthorizationPolicy.CombineAsync(PolicyProvider, AuthorizeData);
        }
        var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();
        var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext);
        if (context.Filters.Any(item => item is IAllowAnonymousFilter))
        {
            return;
        }
        var authorizeResult = await policyEvaluator.AuthorizeAsync(effectivePolicy, authenticateResult, context.HttpContext, context);

        ... // 如果授权失败,返回ChallengeResult或ForbidResult
    }
}

AuthorizeFilter的OnAuthorizationAsync方法会在Action执行之前触发,其调用IPolicyEvaluator来完成授权,将执行流程切回到 ASP.NET Core 授权系统中。关于MVC中IApplicationModelProvider以及Filter的概念,在以后MVC系列的文章中再来详细介绍,下面就继续介绍 ASP.NET Core 的授权系统,也就是《Security》项目。

IPolicyEvaluator

IPolicyEvaluator是MVC调用授权系统的入口点,其定义如下:

public interface IPolicyEvaluator
{
    Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context);
    Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource);
}

在上面介绍的AddMVC中,调用了AddAuthorizationPolicyEvaluator扩展方法,它有如下定义:

public static class PolicyServiceCollectionExtensions
{
    public static IServiceCollection AddAuthorizationPolicyEvaluator(this IServiceCollection services)
    {
        services.TryAdd(ServiceDescriptor.Transient<IPolicyEvaluator, PolicyEvaluator>());
        return services;
    }
}

由此可知IPolicyEvaluator的默认实现为PolicyEvaluator,我们就从它入手,来一步一步解剖 ASP.NET Core 授权系统的执行步骤。

AuthorizeFilter中,依次调到了AuthenticateAsyncAuthorizeAsync方法,我们就一一来看。

AuthenticateAsync(AuthenticationSchemes)

为什么还有一个AuthenticateAsync方法呢,这不是在认证阶段执行的吗?我们看下它的实现:

public class PolicyEvaluator : IPolicyEvaluator
{
    public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
    {
        if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
        {
            ClaimsPrincipal newPrincipal = null;
            foreach (var scheme in policy.AuthenticationSchemes)
            {
                var result = await context.AuthenticateAsync(scheme);
                if (result != null && result.Succeeded)
                {
                    newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
                }
            }

            if (newPrincipal != null)
            {
                context.User = newPrincipal;
                return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
            }
            else
            {
                context.User = new ClaimsPrincipal(new ClaimsIdentity());
                return AuthenticateResult.NoResult();
            }
        }

        return (context.User?.Identity?.IsAuthenticated ?? false) 
            ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User"))
            : AuthenticateResult.NoResult();
    }
}

在《上一章》中,我们知道在AuthorizationPolicy中有AuthenticationSchemesIAuthorizationRequirement两个属性,并详细介绍介绍了Requirement,但是没有提到AuthenticationSchemes的调用。

那么,看到这里,也就大概明白了,它与Requirements的执行是完全独立的,并在它之前执行,用于重置Claims,那么为什么要重置呢?

在认证的章节介绍过,在认证阶段,只会执行默认的认证Scheme,context.User就是使用context.AuthenticateAsync(DefaultAuthenticateScheme)来赋值的,当我们希望使用非默认的Scheme,或者是想合并多个认证Scheme的Claims时,就需要使用基于Scheme的授权来重置Claims了。

它的实现也很简单,直接使用我们在授权策略中指定的Schemes来依次调用认证服务的AuthenticateAsync方法,并将生成的Claims合并,最后返回我们熟悉的AuthenticateResult认证结果。

AuthorizeAsync(Requirements)

接下来再看一下PolicyEvaluatorAuthorizeAsync方法:

public class PolicyEvaluator : IPolicyEvaluator
{
    private readonly IAuthorizationService _authorization;
    public PolicyEvaluator(IAuthorizationService authorization)
    {
        _authorization = authorization;
    }

    public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource)
    {
        var result = await _authorization.AuthorizeAsync(context.User, resource, policy);
        if (result.Succeeded) return PolicyAuthorizationResult.Success();
        return (authenticationResult.Succeeded) ? PolicyAuthorizationResult.Forbid() : PolicyAuthorizationResult.Challenge();
    }
}

该方法会根据Requirements来完成授权,具体的实现是通过调用IAuthorizationService来实现的。

最终返回的是一个PolicyAuthorizationResult对象,并在授权失败时,根据认证结果来返回Forbid(未授权)Challenge(未登录)

public class PolicyAuthorizationResult
{
    private PolicyAuthorizationResult() { }
    public bool Challenged { get; private set; }
    public bool Forbidden { get; private set; }
    public bool Succeeded { get; private set; }
}

IAuthorizationService

然后就到了授权的核心对象AuthorizationService,也可以称为授权的外交官,我们也可以直接在应用代码中调用该对象来实现授权,它有如下定义:

public interface IAuthorizationService
{    
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
}

AuthorizeAsync中还涉及到一个resource对象,用来实现面向资源的授权,放在《下一章》中再来介绍,而在本章与《前一章》的示例中,该值均为null

ASP.NET Core 中还为IAuthorizationService提供了几个扩展方法:

public static class AuthorizationServiceExtensions
{
    public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, string policyName) {}
    public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, AuthorizationPolicy policy) {}
    public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, IAuthorizationRequirement requirement) {}
    public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, AuthorizationPolicy policy) {}
}

其默认实现为DefaultAuthorizationService:

public class DefaultAuthorizationService : IAuthorizationService
{
    private readonly AuthorizationOptions _options;
    private readonly IAuthorizationHandlerContextFactory _contextFactory;
    private readonly IAuthorizationHandlerProvider _handlers;
    private readonly IAuthorizationEvaluator _evaluator;
    private readonly IAuthorizationPolicyProvider _policyProvider;

    public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
    {        
        var policy = await _policyProvider.GetPolicyAsync(policyName);
        return await this.AuthorizeAsync(user, resource, policy);
    }

    public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
    {
        var authContext = _contextFactory.CreateContext(requirements, user, resource);
        var handlers = await _handlers.GetHandlersAsync(authContext);
        foreach (var handler in handlers)
        {
            await handler.HandleAsync(authContext);
            if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
            {
                break;
            }
        }
        return _evaluator.Evaluate(authContext);
    }
}

通过上面代码可以看出,在《上一章》中介绍的授权策略,在这里获取到它的Requirements,后续便不再需要了。而在AuthorizationService中是通过调用四大核心对象来完成授权,我们一一来看。

IAuthorizationPolicyProvider

由于在[Authorize]中,我们指定的是策略的名称,因此需要使用IAuthorizationPolicyProvider来根据名称获取到策略对象,默认实现为DefaultAuthorizationPolicyProvider

public class DefaultAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
    private readonly AuthorizationOptions _options;

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
    {
        return Task.FromResult(_options.DefaultPolicy);
    }

    public virtual Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        return Task.FromResult(_options.GetPolicy(policyName));
    }
}

在上一章中介绍过,我们定义的策略都保存在《AuthorizationOptions》的字典中,因此在这里只是简单的将AuthorizationOptions中的同名方法异步化。

IAuthorizationHandlerContextFactory

授权上下文是我们接触较多的对象,当我们自定义授权Handler时就会用到它,它是使用简单工厂模式来创建的:

public class DefaultAuthorizationHandlerContextFactory : IAuthorizationHandlerContextFactory
{
    public virtual AuthorizationHandlerContext CreateContext(IEnumerable<IAuthorizationRequirement> requirements, ClaimsPrincipal user, object resource)
    {
        return new AuthorizationHandlerContext(requirements, user, resource);
    }
}

授权上下文中主要包含用户的Claims和授权策略的Requirements

public class AuthorizationHandlerContext
{
    private HashSet<IAuthorizationRequirement> _pendingRequirements;
    private bool _failCalled;
    private bool _succeedCalled;

    public AuthorizationHandlerContext(IEnumerable<IAuthorizationRequirement> requirements, ClaimsPrincipal user, object resource)
    {
        Requirements = requirements; User = user; Resource = resource;
        _pendingRequirements = new HashSet<IAuthorizationRequirement>(requirements);
    }

    public virtual bool HasFailed { get { return _failCalled; } }
    public virtual bool HasSucceeded => !_failCalled && _succeedCalled && !_pendingRequirements.Any();
    public virtual void Fail()
    {
        _failCalled = true;
    }
    public virtual void Succeed(IAuthorizationRequirement requirement)
    {
        _succeedCalled = true;
        _pendingRequirements.Remove(requirement);
    }
}

如上,_pendingRequirements中保存着所有待验证的Requirements,验证成功的Requirement则从中移除。

IAuthorizationHandlerProvider

兜兜转转,终于进入到了授权的最终验证逻辑中了,首先,使用IAuthorizationHandlerProvider来获取到所有的授权Handler

IAuthorizationHandlerProvider的默认实现为DefaultAuthorizationHandlerProvider:

public class DefaultAuthorizationHandlerProvider : IAuthorizationHandlerProvider
{
    private readonly IEnumerable<IAuthorizationHandler> _handlers;

    public DefaultAuthorizationHandlerProvider(IEnumerable<IAuthorizationHandler> handlers)
    {
        _handlers = handlers;
    }

    public Task<IEnumerable<IAuthorizationHandler>> GetHandlersAsync(AuthorizationHandlerContext context)
        => Task.FromResult(_handlers);
}

在《上一章》中,我们还介绍到,我们定义的Requirement,可以直接实现IAuthorizationHandler接口,也可以单独定义Handler,但是需要注册到DI系统中去。

在默认的AuthorizationHandlerProvider中,会从DI系统中获取到我们注册的所有Handler,最终调用其HandleAsync方法。

我们在实现IAuthorizationHandler接口时,通常是继承自AuthorizationHandler<TRequirement>来实现,它有如下定义:

public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler where TRequirement : IAuthorizationRequirement
{
    public virtual async Task HandleAsync(AuthorizationHandlerContext context)
    {
        foreach (var req in context.Requirements.OfType<TRequirement>())
        {
            await HandleRequirementAsync(context, req);
        }
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement);
}

如上,首先会在HandleAsync过滤出与Requirement对匹配的Handler,然后再调用其HandleRequirementAsync方法。

那我们定义的直接实现IAuthorizationHandler了接口的Requirement又是如何执行的呢?

AddAuthorization扩展方法中可以看到,默认还为IAuthorizationHandler注册了一个PassThroughAuthorizationHandler,定义如下:

public class PassThroughAuthorizationHandler : IAuthorizationHandler
{
    public async Task HandleAsync(AuthorizationHandlerContext context)
    {
        foreach (var handler in context.Requirements.OfType<IAuthorizationHandler>())
        {
            await handler.HandleAsync(context);
        }
    }
}

它负责调用该策略中所有实现了IAuthorizationHandler接口的Requirement

IAuthorizationEvaluator

最后,通过调用IAuthorizationEvaluator接口,来完成最终的授权结果,默认实现为DefaultAuthorizationEvaluator:

public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator
{
    public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
        => context.HasSucceeded
            ? AuthorizationResult.Success()
            : AuthorizationResult.Failed(context.HasFailed
                ? AuthorizationFailure.ExplicitFail()
                : AuthorizationFailure.Failed(context.PendingRequirements));
}

当我们在一个策略中指定多个Requirement时,只有全部验证通过时,授权上下文中的HasSucceeded才会为True,而HasFailed代表授权结果的显式失败。

这里根据授权上下文的验证结果来生成授权结果:

public class AuthorizationResult
{
    public bool Succeeded { get; private set; }
    public AuthorizationFailure Failure { get; private set; }
    public static AuthorizationResult Success() => new AuthorizationResult { Succeeded = true };
    public static AuthorizationResult Failed(AuthorizationFailure failure) => new AuthorizationResult { Failure = failure };
    public static AuthorizationResult Failed() => new AuthorizationResult { Failure = AuthorizationFailure.ExplicitFail() };
}

public class AuthorizationFailure
{
    private AuthorizationFailure() { }
    public bool FailCalled { get; private set; }
    public IEnumerable<IAuthorizationRequirement> FailedRequirements { get; private set; }
    public static AuthorizationFailure ExplicitFail()
    {
        return new AuthorizationFailure { FailCalled = true, FailedRequirements = new IAuthorizationRequirement[0] };
    }
    public static AuthorizationFailure Failed(IEnumerable<IAuthorizationRequirement> failed)
        => new AuthorizationFailure { FailedRequirements = failed };

}

整个授权流程的结构大致如下:

authorization_service

总结

通过对 ASP.NET Core 授权系统执行流程的探索,可以看出授权是主要是通过调用IAuthorizationService来完成的,而授权策略的本质是提供 Requirement ,我们完全可以使用它们两个来完成各种灵活的授权方式,而不用局限于策略。在 ASP.NET Core 中,还提供了基于资源的授权,放在《下一章》中来介绍,并会简单演示一下在一个通用权限管理系统中如何来授权。

ASP.Net Core下Authorization的几种方式_nic7968的博客-CSDN博客_.net authorization

mikel阅读(749)

来源: ASP.Net Core下Authorization的几种方式_nic7968的博客-CSDN博客_.net authorization

Authorization其目标就是验证Http请求能否通过验证。ASP.NET Core提供了很多种Authorization方式,详细可以参考 微软官方文档。在这里只详细介绍三种方式:

  • Policy
  • Middleware
  • Custom Attribute

1. Policy : 策略授权

先定义一个IAuthorizationRequirement类来定义策略的要求,以下例子支持传递一个age参数。

  1. public class AdultPolicyRequirement : IAuthorizationRequirement
  2. {
  3. public int Age { get; }
  4. public AdultPolicyRequirement(int age)
  5. {
  6. //年龄限制
  7. this.Age = age;
  8. }
  9. }

然后定义策略要求的Handler,当提供的Controller被请求时先根据请求的Http报文来决定是否可以通过验证。

  1. public class AdultAuthorizationHandler : AuthorizationHandler
  2. {
  3. protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdultPolicyRequirement requirement)
  4. {
  5. //获取当前http请求的context对象
  6. var mvcContext = context.Resource as AuthorizationFilterContext;
  7. //以下代码都不是必须的,只是展示一些使用方法,你可以选择使用
  8. ……
  9. //
  10. var age = mvcContext.HttpContext.Request.Query.FirstOrDefault(u => u.Key == “age”);
  11. if (age.Value.Count <= 0|| Convert.ToInt16(age.Value[0]) < requirement.Age)
  12. {
  13. context.Fail();
  14. }
  15. else
  16. {
  17. //通过验证,这句代码必须要有
  18. context.Succeed(requirement);
  19. }
  20. return Task.CompletedTask;
  21. }
  22. }

还需要在启动时,在services里注册定义的策略和对应的Handler

  1. //添加二种认证策略,一种以12岁为界限,一种是18岁
  2. services.AddAuthorization(options =>
  3. {
  4. options.AddPolicy(“Adult1”, policy =>
  5. policy.Requirements.Add(new AdultPolicyRequirement(12)));
  6. options.AddPolicy(“Adult2”, policy =>
  7. policy.Requirements.Add(new AdultPolicyRequirement(18)));
  8. });
  9. //添加策略验证handler
  10. services.AddSingleton();

最后在相应的Controller前加上Authroize特性 [Authorize("Adult1")]。总体上Policy这种方式比较简单,但是也有不灵活的地方,不同的策略要求都需要提前在services里注册。完整的例子可以参考 PolicySample

2. Middleware: 中间件方式

这种方式并不是专门用于授权,它的用途更广更灵活,它用于在所有Http Request和Response前授权检查、数据处理、错误跳转、日志处理等。详细说明可以参考 官方Middleware说明

image

这个图是基本的结构和运行图,我们可以创建多个中间组件,组成一个管道,在Http的Request发出和Response创建之前对HttpContext.Request和HttpContext.Response进行处理。看以下例子,定义了2个中间组件类:

  1. public class AuthorizeMiddleware
  2. {
  3. private readonly RequestDelegate next;
  4. public AuthorizeMiddleware(RequestDelegate next)
  5. {
  6. this.next = next;
  7. }
  8. public async Task Invoke(HttpContext context /* other scoped dependencies */)
  9. {
  10. //以下代码都不是必须的,只是展示一些使用方法,你可以选择使用
  11. //这个例子只是修改一下response的header
  12. context.Response.OnStarting(state => {
  13. var httpContext = (HttpContext)state;
  14. httpContext.Response.Headers.Add(“test2”, “testvalue2”);
  15. return Task.FromResult(0);
  16. }, context);
  17. //处理结束转其它中间组件去处理
  18. await next(context);
  19. }
  20. }
  1. public class OtherMiddleware
  2. {
  3. private readonly RequestDelegate next;
  4. public OtherMiddleware(RequestDelegate next)
  5. {
  6. this.next = next;
  7. }
  8. public async Task Invoke(HttpContext context )
  9. {
  10. //这个例子只是修改一下response的header
  11. context.Response.OnStarting(state => {
  12. var httpContext = (HttpContext)state;
  13. httpContext.Response.Headers.Add(“test1”, “testvalue1” );
  14. return Task.FromResult(0);
  15. }, context);
  16. await next(context);
  17. }
  18. }

这里定义的类不需要实现接口或集成系统的类。只需要给app增加middleware代理类的定义。注意在HttpResponse发出之后就不要再调用next.invoke()。以下三句代码的执行顺序不能弄错。

  1. app.UseMiddleware(typeof(AuthorizeMiddleware));
  2. app.UseMiddleware(typeof(OtherMiddleware));
  3. app.UseMvc();

执行web请求后执行的顺序是:

  • 执行AuthorizeMiddleware的invoke方法
  • 执行OtherMiddleware的invoke方法
  • 执行ValueController的Get方法
  • 执行OtherMiddleware的修改header方法
  • 执行AuthorizeMiddleware的修改header方法
  • 发出Http Response

大家可以自己执行一下代码来理解。代码参考 Github地址

3. Custom Attribute:自定义特性

这里其实是第一种Policy策略和自定义特性的结合,从而实现在Controller的具体方法位置自定义不同参数的Policy策略。

首先需要定义这个Attribute和策略要求类

  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
  2. public class PermissionCheckAttribute : AuthorizeAttribute
  3. {
  4. public string Id { get; set; }
  5. public int Operation { get; set; }
  6. public PermissionCheckAttribute() : base(“PermissionCheck”)
  7. {
  8. }
  9. }
  10. public class PermissionCheckPolicyRequirement : IAuthorizationRequirement
  11. {
  12. //Add any custom requirement properties if you have them
  13. public PermissionCheckPolicyRequirement()
  14. {
  15. }
  16. }

再定义策略和Attribute对应的Handler处理类

  1. public class PermissionCheckPolicyHandler : AttributeAuthorizationHandler<PermissionCheckPolicyRequirement, PermissionCheckAttribute>
  2. {
  3. protected override Task HandleRequirementAsync(AuthorizationHandlerContext authoriazationContext,
  4. PermissionCheckPolicyRequirement requirement, IEnumerable<PermissionCheckAttribute> attributes)
  5. {
  6. var context = authoriazationContext.Resource as AuthorizationFilterContext;
  7. foreach (var permissionAttribute in attributes)
  8. {
  9. this.checkPermission(context, permissionAttribute.Id, permissionAttribute.Operation);
  10. }
  11. authoriazationContext.Succeed(requirement);
  12. return Task.FromResult<object>(null);
  13. }
  14. private void checkPermission(AuthorizationFilterContext context, string _Id, int _Operation)
  15. {
  16. if (_Operation > 0)
  17. {
  18. if (_Id != “user1”)
  19. {
  20. throw new Exception(“不具备操作权限”);
  21. }
  22. }
  23. else
  24. {
  25. //dosomething
  26. }
  27. return;
  28. }
  29. }

同样还需要在service里添加策略和策略处理类,这里不贴代码了。最后在Controller里使用带参数的Attribute,类似如下:

  1. [HttpGet]
  2. [PermissionCheck (Id =“user1”, Operation=2)]
  3. public IEnumerable Get()
  4. {
  5. return new string[] { “value1”, “value2” };
  6. }
  7. // GET api/values/5
  8. [HttpGet(“{id}”)]
  9. [PermissionCheck(Id = “user2”, Operation = 4)]
  10. public string Get(int id)
  11. {
  12. return “value”;
  13. }

完整的代码参考Github地址

作者:voxer
链接:https://www.jianshu.com/p/0ed4d820809c

探索 .NET Core 依赖注入的 IServiceCollection - 腾讯云开发者社区-腾讯云

mikel阅读(738)

来源: 探索 .NET Core 依赖注入的 IServiceCollection – 腾讯云开发者社区-腾讯云

如果您使用了.NET Core,则很可能已使用Microsoft.Extensions.DependencyInjection中的内置依赖项注入容器,在本文中,我想更深入地了解Microsoft Dependency Injection(DI)容器中的 IServiceCollection。

什么是依赖注入(DI)和DI容器?

Microsoft依赖项注入容器只是一组类,它们组合到一个代码库中,这个库会自动创建并管理程序中需要的对象。

我们先看下面的代码:

public class ClassA
{
    public void DoWork() 
    {
        var b = new ClassB();
        b.DoStuff();
    }
}

public class ClassB
{
    public void DoStuff()
    {
        // ...
    }
}

ClassA直接依赖ClassB,并且在它的DoWork方法中,new了一个ClassB,然后调用了ClassB的DoStuff方法。

我们改写一下代码看看:

public class ClassA
{
    private readonly ClassB _dependency;

    public ClassA(ClassB classB) => _dependency = classB;

    public void DoWork() => _dependency.DoStuff();
}

public class ClassB : IThing
{
    public void DoStuff()
    {
        // ...
    }
}

首先,我加了一个构造函数,并且指定了ClassA依赖的类型,调用构造函数时,必须提供ClassB的实例, 在ClassA的内部,我们不会去new一个ClassB,ClassB完全是由外部传入的,这里就是控制反转(IoC)。

进一步改进代码:

public interface IThing
{
    public void DoStuff();
}

public class ClassA
{
    private readonly IThing _dependency;

    public ClassA(IThing thing) => _dependency = thing;

    public void DoWork() => _dependency.DoStuff();
}

public class ClassB : IThing
{
    public void DoStuff()
    {
        // ...
    }
}

加了一个接口IThing,现在,我们已经应用了SOLID的依赖倒置原则,我们不再依赖具体的实现,相反,我们依赖于IThing抽象,在构造函数中,只需要传入IThing的实现就好了。

然后在我们的代码中,可以这样用:

class Program
{
    static void Main(string[] args)
    {
        IThing thing = new ClassB();
        ClassA classA = new ClassA(thing);
        classA.DoWork();
    }
}

我们手动new了一个ClassB,它实现了IThing接口,然后创建ClassA的时候,直接把thing传入构造函数中。

上面的代码演示,我们只处理了ClassA和ClassB的依赖注入关系,但是在实际中呢,我们代码中有很多类型,然后有各种各样的依赖关系。

这个时候我们就需要一个DI容器,我们对容器进行配置,然它知道什么类型,然后负责自动创建并管理对象(通常称为服务)。

注册服务

通常, Microsoft DI 容器需要在Startup类中配置,在这里,您可以使用ConfigureServices方法向容器注册服务,在应用程序托管生命周期的早期,将调用ConfigureServices方法,它有一个参数IServiceCollection,这个参数在初始化应用程序时传入。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 注册服务
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    }
}

为了尽可能的简单,我们也可以在控制台中使用 Microsoft DependencyInjection。

创建控制台程序后,我们首先在项目中引入Microsoft.Extensions.DependencyInjection

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
  </ItemGroup>

</Project>

现在我们开始注册我们的服务,但是我们需要一个IServiceCollection,让我们看一下IServiceCollection的定义。

public interface IServiceCollection : IList<ServiceDescriptor>
{
}

IServiceCollection没有定义其任何成员,而是从IList<ServiceDescriptor>派生。

Microsoft.Extensions.DepenencyInjection程序包里面,它有一个默认的实现:ServiceCollection。

public class ServiceCollection : IServiceCollection
{
    private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();

    public int Count => _descriptors.Count;

    public bool IsReadOnly => false;

    public ServiceDescriptor this[int index]
    {
        get
        {
            return _descriptors[index];
        }
        set
        {
            _descriptors[index] = value;
        }
    }

    // ...
}

它有一个私有的List集合:_descriptors,里面是ServiceDescriptor。

让我们从创建一个ServiceCollection,然后注册两个服务。

static void Main(string[] args)
{
    var serviceCollection = new ServiceCollection();

    serviceCollection.AddSingleton<ClassA>();
    serviceCollection.AddSingleton<IThing, ClassB>();

    Console.WriteLine("Done");
}

在前面的代码中,我们已经使用AddSingleton方法注册了两个服务,这不是IServiceCollection接口定义的方法,也不在ServiceCollection上,这是IServiceCollection的扩展方法,这个方法在ServiceCollectionServiceExtensions的扩展类中,接下来,我会介绍这个方法是如何注册服务的,不过这之前,我们首先回顾下服务生命周期的概念。

服务生命周期

在Microsoft依赖项注入框架中,我们可以使用三种生命周期注册服务,分别是单例(Singleton)、瞬时(Transient)、作用域(Scoped),在上面的代码中,我使用了AddSingleton()来注册服务。

使用Singleton服务的优点是我们不会创建多个服务实例,只会创建一个实例,保存到DI容器中,直到程序退出,这不仅效率高,而且性能高,但是有一个要注意的点,如果在多线程中使用了Singleton,要考虑线程安全的问题,保证它不会有冲突。

瞬时(Transient)和单例(Singleton)模式是相反的,每次使用时,DI容器都是创建一个新的实例。

作用域(Scoped),在一个作用域内,会使用同一个实例,像EF Core的DbContext上下文就被注册为作用域服务。

我们注册服务时会发生什么?

在上面的代码中,我已经注册了两个单例服务。

serviceCollection.AddSingleton<ClassA>();
serviceCollection.AddSingleton<IThing, ClassB>();

这是最终的AddSingleton方法:

public static IServiceCollection AddSingleton(
    this IServiceCollection services,
    Type serviceType,
    Type implementationType)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }
    if (serviceType == null)
    {
        throw new ArgumentNullException(nameof(serviceType));
    }
    if (implementationType == null)
    {
        throw new ArgumentNullException(nameof(implementationType));
    }
    return Add(services, serviceType, implementationType, ServiceLifetime.Singleton);
}

我们可以看到AddSingleton方法调用了私有的Add方法,并且传入了一个生命周期的枚举值ServiceLifetime.Singleton

让我们看一下Add方法的工作原理:

private static IServiceCollection Add(
    IServiceCollection collection,
    Type serviceType,
    Type implementationType,
    ServiceLifetime lifetime)
{
    var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
    collection.Add(descriptor);
    return collection;
}

它创建一个新的ServiceDescriptor实例,传入服务类型,实现类型(可能与服务类型相同)和生命周期,然后调用Add方法添加到列表中。

之前,我们了解到IServiceCollection本质上是包装了List <ServiceDescriptor>, ServiceDescriptor类很简单,代表一个注册的服务,包括其服务类型,实现类型和生命周期。

实例注册

我们也可以手动new一个实例,然后传入到AddSingleton()方法中:

var myInstance = new ClassB();
serviceCollection.AddSingleton<IThing>(myInstance);

使用 ServiceDescriptor

我们还可以手动定义一个ServiceDescriptor,然后直接添加到IServiceCollection中。

var descriptor = new ServiceDescriptor(typeof(IThing), typeof(ClassB), ServiceLifetime.Singleton);
serviceCollection.Add(descriptor);

总结

在本文中,介绍了.NET中的DI的一些核心知识,可以直接创建ServiceCollection来使用Microsoft DI框架,了解了IServiceCollection上的AddSingleton扩展方法是如何工作,以及它们最终创建了一个ServiceDescriptor,然后添加到一个ServiceCollection包装的List集合中。

原文链接: https://www.stevejgordon.co.uk/aspnet-core-dependency-injection-what-is-the-iservicecollection

浅析 .NET 中 AsyncLocal 的实现原理 - 黑洞视界 - 博客园

mikel阅读(876)

来源: 浅析 .NET 中 AsyncLocal 的实现原理 – 黑洞视界 – 博客园

浅析 .NET 中 AsyncLocal 的实现原理

 

 

前言

对于写过 ASP.NET Core 的童鞋来说,可以通过 HttpContextAccessor 在 Controller 之外的地方获取到HttpContext,而它实现的关键其实是在于一个AsyncLocal<HttpContextHolder> 类型的静态字段。接下来就和大家来一起探讨下这个 AsyncLocal 的具体实现原理。如果有讲得不清晰或不准确的地方,还望指出。

public class HttpContextAccessor : IHttpContextAccessor
{
    private static AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>();

	// 其他代码这里不展示
}

本文源码参考为发文时间点为止最新的 github 开源代码,和之前实现有些许不同,但设计思想基本一致。

代码库地址:https://github.com/dotnet/runtime

1、线程本地存储

如果想要整个.NET程序中共享一个变量,我们可以将想要共享的变量放在某个类的静态属性上来实现。

而在多线程的运行环境中,则可能会希望能将这个变量的共享范围缩小到单个线程内。例如在web应用中,服务器为每个同时访问的请求分配一个独立的线程,我们要在这些独立的线程中维护自己的当前访问用户的信息时,就需要需要线程本地存储了。

例如下面这样一个例子。

class Program
{
    [ThreadStatic]
    private static string _value;
    static void Main(string[] args)
    {
        Parallel.For(0, 4, _ =>
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;

            _value ??= $"这是来自线程{threadId}的数据";
            Console.WriteLine($"Thread:{threadId}; Value:{_value}");
        });
    }
}

输出结果:

Thread:4; Value:这是来自线程4的数据
Thread:1; Value:这是来自线程1的数据
Thread:5; Value:这是来自线程5的数据
Thread:6; Value:这是来自线程6的数据

除了可以使用 ThreadStaticAttribute 外,我们还可以使用 ThreadLocal<T> 、CallContext 、AsyncLocal<T> 来实现一样的功能。由于 .NET Core 不再实现 CallContext,所以下列代码只能在 .NET Framework 中执行。

class Program
{
    [ThreadStatic]
    private static string _threadStatic;
    private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
    private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    static void Main(string[] args)
    {
        Parallel.For(0, 4, _ =>
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;

            var value = $"这是来自线程{threadId}的数据";
            _threadStatic ??= value;
            CallContext.SetData("value", value);
            _threadLocal.Value ??= value;
            _asyncLocal.Value ??= value;
            Console.WriteLine($"Use ThreadStaticAttribute; Thread:{threadId}; Value:{_threadStatic}");
            Console.WriteLine($"Use CallContext;           Thread:{threadId}; Value:{CallContext.GetData("value")}");
            Console.WriteLine($"Use ThreadLocal;           Thread:{threadId}; Value:{_threadLocal.Value}");
            Console.WriteLine($"Use AsyncLocal;            Thread:{threadId}; Value:{_asyncLocal.Value}");
        });

        Console.Read();
    }
}

输出结果:

Use ThreadStaticAttribute; Thread:3; Value:这是来自线程3的数据
Use ThreadStaticAttribute; Thread:4; Value:这是来自线程4的数据
Use ThreadStaticAttribute; Thread:1; Value:这是来自线程1的数据
Use CallContext; Thread:1; Value:这是来自线程1的数据
Use ThreadLocal; Thread:1; Value:这是来自线程1的数据
Use AsyncLocal; Thread:1; Value:这是来自线程1的数据
Use ThreadStaticAttribute; Thread:5; Value:这是来自线程5的数据
Use CallContext; Thread:5; Value:这是来自线程5的数据
Use ThreadLocal; Thread:5; Value:这是来自线程5的数据
Use AsyncLocal; Thread:5; Value:这是来自线程5的数据
Use CallContext; Thread:3; Value:这是来自线程3的数据
Use CallContext; Thread:4; Value:这是来自线程4的数据
Use ThreadLocal; Thread:4; Value:这是来自线程4的数据
Use AsyncLocal; Thread:4; Value:这是来自线程4的数据
Use ThreadLocal; Thread:3; Value:这是来自线程3的数据
Use AsyncLocal; Thread:3; Value:这是来自线程3的数据

上面的例子都只是在同一个线程中对线程进行存和取,但日常开发的过程中,我们会有很多异步的场景,这些场景可能会导致执行代码的线程发生切换。

比如下面的例子

class Program
{
    [ThreadStatic]
    private static string _threadStatic;
    private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
    private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    static void Main(string[] args)
    {
        _threadStatic = "ThreadStatic保存的数据";
        _threadLocal.Value = "ThreadLocal保存的数据";
        _asyncLocal.Value = "AsyncLocal保存的数据";
        PrintValuesInAnotherThread();
        Console.ReadKey();
    }

    private static void PrintValuesInAnotherThread()
    {
        Task.Run(() =>
        {
            Console.WriteLine($"ThreadStatic: {_threadStatic}");
            Console.WriteLine($"ThreadLocal: {_threadLocal.Value}");
            Console.WriteLine($"AsyncLocal: {_asyncLocal.Value}");
        });
    }
}

输出结果:

ThreadStatic:
ThreadLocal:
AsyncLocal: AsyncLocal保存的数据

在线程发生了切换之后,只有 AsyncLocal 还能够保留原来的值,当然,.NET Framework 中的 CallContext 也可以实现这个需求,下面给出一个相对完整的总结。

实现方式 .NET FrameWork 可用 .NET Core 可用 是否支持数据流向辅助线程
ThreadStaticAttribute
ThreadLocal<T>
CallContext.SetData(string name, object data) 仅当参数 data 对应的类型实现了 ILogicalThreadAffinative 接口时支持
CallContext.LogicalSetData(string name, object data)
AsyncLocal<T>

2、AsyncLocal 实现

我们主要对照 .NET Core 源码进行学习,源码地址:https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Threading/AsyncLocal.cs

2.1、主体 AsyncLocal<T>#

AsyncLocal<T> 为我们提供了两个功能

  • 通过 Value 属性存取值
  • 通过构造函数注册回调函数监听任意线程中对值做出的改动,需记着这个功能,后面介绍源码的时候会有很多地方涉及

其内部代码相对简单

public sealed class AsyncLocal<T> : IAsyncLocal
{
    private readonly Action<AsyncLocalValueChangedArgs<T>>? m_valueChangedHandler;
    
    // 无参构造
    public AsyncLocal()
    {
    }
    
    // 可以注册回调的构造函数,当 Value 在任意线程被改动,将调用回调
    public AsyncLocal(Action<AsyncLocalValueChangedArgs<T>>? valueChangedHandler)
    {
        m_valueChangedHandler = valueChangedHandler;
    }
    
    [MaybeNull]
    public T Value
    {
        get
        {
            // 从 ExecutionContext 中以自身为 Key 获取值
            object? obj = ExecutionContext.GetLocalValue(this);
            return (obj == null) ? default : (T)obj;
        }
        // 是否注册回调将回影响到 ExecutionContext 是否保存其引用
        set => ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler != null);
    }
    
    // 在 ExecutionContext 如果判断到值发生了变化,此方法将被调用
    void IAsyncLocal.OnValueChanged(object? previousValueObj, object? currentValueObj, bool contextChanged)
    {
        Debug.Assert(m_valueChangedHandler != null);
        T previousValue = previousValueObj == null ? default! : (T)previousValueObj;
        T currentValue = currentValueObj == null ? default! : (T)currentValueObj;
        m_valueChangedHandler(new AsyncLocalValueChangedArgs<T>(previousValue, currentValue, contextChanged));
    }
}

internal interface IAsyncLocal
{
    void OnValueChanged(object? previousValue, object? currentValue, bool contextChanged);
}

真正的数据存取是通过 ExecutionContext.GetLocalValue 和 ExecutionContext.SetLocalValue 实现的。

public class ExecutionContext
{
    internal static object? GetLocalValue(IAsyncLocal local);
    internal static void SetLocalValue(
        IAsyncLocal local,
        object? newValue,
        bool needChangeNotifications);
}

需要注意的是这边通过 IAsyncLocal 这一接口实现了 AsyncLocal 与 ExcutionContext 的解耦。 ExcutionContext 只关注数据的存取本身,接口定义的类型都是 object,而不关心具体的类型 T

2.2、AsyncLocal<T> 在 ExecutionContext 中的数据存取实现#

在.NET 中,每个线程都关联着一个 执行上下文(execution context) 。 可以通过Thread.CurrentThread.ExecutionContext 属性进行访问,或者通过 ExecutionContext.Capture() 获取(前者的实现) 。

AsyncLocal 最终就是把数据保存在 ExecutionContext 上的,为了更深入地理解 AsyncLocal 我们需要先理解一下它。

源码地址:https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs

2.2.1、 ExecutionContext 与 线程的绑定关系#

ExecutionContext 被保存 Thread 的 internal 修饰的 _executionContext 字段上。但Thread.CurrentThread.ExecutionContext 并不直接暴露 _executionContext 而与 ExecutionContext.Capture() 共用一套逻辑。

class ExecutionContext
{
    public static ExecutionContext? Capture()
    {
        ExecutionContext? executionContext = Thread.CurrentThread._executionContext;
        if (executionContext == null)
        {
            executionContext = Default;
        }
        else if (executionContext.m_isFlowSuppressed)
        {
            executionContext = null;
        }

        return executionContext;
    }
}

下面是经过整理的 Thread 的与 ExecutionContext 相关的部分,Thread 属于部分类,_executionContext 字段定义在 Thread.CoreCLR.cs 文件中

class Thread
{
	// 保存当前线程所关联的 执行上下文
    internal ExecutionContext? _executionContext;

    [ThreadStatic]
    private static Thread? t_currentThread;
	
    public static Thread CurrentThread => t_currentThread ?? InitializeCurrentThread();
	
	public ExecutionContext? ExecutionContext => ExecutionContext.Capture();
}

2.2.2、ExecutionContext 的私有变量#

public sealed class ExecutionContext : IDisposable, ISerializable
{
    // 默认执行上下文
    internal static readonly ExecutionContext Default = new ExecutionContext(isDefault: true);
    // 执行上下文禁止流动后的默认上下文
    internal static readonly ExecutionContext DefaultFlowSuppressed = new ExecutionContext(AsyncLocalValueMap.Empty, Array.Empty<IAsyncLocal>(), isFlowSuppressed: true);
	// 保存所有注册了修改回调的 AsyncLocal 的 Value 值,本文暂不涉及对此字段的具体讨论
    private readonly IAsyncLocalValueMap? m_localValues;
    // 保存所有注册了回调的 AsyncLocal 的对象引用
    private readonly IAsyncLocal[]? m_localChangeNotifications;
    // 当前线程是否禁止上下文流动
    private readonly bool m_isFlowSuppressed;
    // 当前上下文是否是默认上下文
    private readonly bool m_isDefault;
}

2.2.3、IAsyncLocalValueMap 接口及其实现#

在同一个线程中,所有 AsyncLocal 所保存的 Value 都保存在 ExecutionContext 的 m_localValues 字段上。

public class ExecutionContext
{
    private readonly IAsyncLocalValueMap m_localValues;
}

为了优化查找值时的性能,微软为 IAsyncLocalValueMap 提供了6个实现

类型 元素个数
EmptyAsyncLocalValueMap 0
OneElementAsyncLocalValueMap 1
TwoElementAsyncLocalValueMap 2
ThreeElementAsyncLocalValueMap 3
MultiElementAsyncLocalValueMap 4 ~ 16
ManyElementAsyncLocalValueMap > 16

随着 ExecutionContext 所关联的 AsyncLocal 数量的增加,IAsyncLocalValueMap 的实现将会在ExecutionContext的SetLocalValue方法中被不断替换。查询的时间复杂度和空间复杂度依次递增。代码的实现与 AsyncLocal 同属于 一个文件。当然元素数量减少时也会替换成之前的实现。

// 这个接口是用来在 ExecutionContext 中保存 IAsyncLocal => object 的映射关系。
// 其实现被设定为不可变的(immutable),随着元素的数量增加而变化,空间复杂度和时间复杂度也随之增加。
internal interface IAsyncLocalValueMap
{
    bool TryGetValue(IAsyncLocal key, out object? value);
	// 通过此方法新增 AsyncLocal 或修改现有的 AsyncLocal
    // 如果数量无变化,返回同类型的 IAsyncLocalValueMap 实现类实例
	// 如果数量发生变化(增加或减少,将value设值为null时会减少),则可能返回不同类型的 IAsyncLocalValueMap 实现类实例
    IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent);
}

Map 的创建是以静态类 AsyncLocalValueMap 的 Create 方法作为创建的入口的。

internal static class AsyncLocalValueMap
{
    // EmptyAsyncLocalValueMap 设计上只在这边实例化,其他地方当作常量使用
    public static IAsyncLocalValueMap Empty { get; } = new EmptyAsyncLocalValueMap();

    public static bool IsEmpty(IAsyncLocalValueMap asyncLocalValueMap)
    {
        Debug.Assert(asyncLocalValueMap != null);
        Debug.Assert(asyncLocalValueMap == Empty || asyncLocalValueMap.GetType() != typeof(EmptyAsyncLocalValueMap));

        return asyncLocalValueMap == Empty;
    }

    public static IAsyncLocalValueMap Create(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
    {
        // 创建最初的实例
        // 如果 AsyncLocal 注册了回调,则需要保存 null 的 Value,以便下次设置非null的值时因为值发生变化而触发回调
        return value != null || !treatNullValueAsNonexistent ?
            new OneElementAsyncLocalValueMap(key, value) :
            Empty;
    }
}

此后每次更新元素时都必须调用 IAsyncLocalValueMap 实现类的 Set 方法,原实例是不会发生变化的,需保存 Set 的返回值。

接下来以 ThreeElementAsyncLocalValueMap 为例进行解释

private sealed class ThreeElementAsyncLocalValueMap : IAsyncLocalValueMap
{
	// 申明三个私有字段保存 key
    private readonly IAsyncLocal _key1, _key2, _key3;
	// 申明三个私有字段保存
    private readonly object? _value1, _value2, _value3;

    public ThreeElementAsyncLocalValueMap(IAsyncLocal key1, object? value1, IAsyncLocal key2, object? value2, IAsyncLocal key3, object? value3)
    {
        _key1 = key1; _value1 = value1;
        _key2 = key2; _value2 = value2;
        _key3 = key3; _value3 = value3;
    }

    public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
    {
		// 如果 AsyncLocal 注册过回调,treatNullValueAsNonexistent 的值是 false,
		// 意思是就算 value 是 null,也认为它是有效的
        if (value != null || !treatNullValueAsNonexistent)
        {
			// 如果现在的 map 已经保存过传入的 key ,则返回一个更新了 value 值的新 map 实例
            if (ReferenceEquals(key, _key1)) return new ThreeElementAsyncLocalValueMap(key, value, _key2, _value2, _key3, _value3);
            if (ReferenceEquals(key, _key2)) return new ThreeElementAsyncLocalValueMap(_key1, _value1, key, value, _key3, _value3);
            if (ReferenceEquals(key, _key3)) return new ThreeElementAsyncLocalValueMap(_key1, _value1, _key2, _value2, key, value);

            // 如果当前Key不存在map里,则需要一个能存放第四个key的map
            var multi = new MultiElementAsyncLocalValueMap(4);
            multi.UnsafeStore(0, _key1, _value1);
            multi.UnsafeStore(1, _key2, _value2);
            multi.UnsafeStore(2, _key3, _value3);
            multi.UnsafeStore(3, key, value);
            return multi;
        }
        else
        {
			// value 是 null,对应的 key 会被忽略或者从 map 中去除,这边会有两种情况
			// 1、如果当前的 key 存在于 map 当中,则将这个 key 去除,map 类型降级为 TwoElementAsyncLocalValueMap
            return
                ReferenceEquals(key, _key1) ? new TwoElementAsyncLocalValueMap(_key2, _value2, _key3, _value3) :
                ReferenceEquals(key, _key2) ? new TwoElementAsyncLocalValueMap(_key1, _value1, _key3, _value3) :
                ReferenceEquals(key, _key3) ? new TwoElementAsyncLocalValueMap(_key1, _value1, _key2, _value2) :
				// 2、当前 key 不存在于 map 中,则会被直接忽略
                (IAsyncLocalValueMap)this;
        }
    }

	// 至多对比三次就能找到对应的 value
    public bool TryGetValue(IAsyncLocal key, out object? value)
    {
        if (ReferenceEquals(key, _key1))
        {
            value = _value1;
            return true;
        }
        else if (ReferenceEquals(key, _key2))
        {
            value = _value2;
            return true;
        }
        else if (ReferenceEquals(key, _key3))
        {
            value = _value3;
            return true;
        }
        else
        {
            value = null;
            return false;
        }
    }
}

2.2.4、ExecutionContext – SetLocalValue#

需要注意的是这边会涉及到两个 Immutable 结构,一个是 ExecutionContext 本身,另一个是 IAsyncLocalValueMap 的实现类。同一个 key 前后两次 value 发生变化后,会产生新的 ExecutionContext 的实例和 IAsyncLocalMap 实现类实例(在 IAsyncLocalValueMap 实现类的 Set 方法中完成)。

internal static void SetLocalValue(IAsyncLocal local, object? newValue, bool needChangeNotifications)
{
	// 获取当前执行上下文
    ExecutionContext? current = Thread.CurrentThread._executionContext;

    object? previousValue = null;
    bool hadPreviousValue = false;
    if (current != null)
    {
        Debug.Assert(!current.IsDefault);
        Debug.Assert(current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context");
		
		// 判断当前作为 Key 的 AsyncLocal 是否已经有对应的 Value 
        hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);
    }

	// 如果前后两次 Value 没有发生变化,则继续处理
    if (previousValue == newValue)
    {
        return;
    }

	// 对于 treatNullValueAsNonexistent: !needChangeNotifications 的说明
	// 如果 AsyncLocal 注册了回调,则 needChangeNotifications 为 ture,m_localValues 会保存 null 值以便下次触发change回调
    IAsyncLocal[]? newChangeNotifications = null;
    IAsyncLocalValueMap newValues;
    bool isFlowSuppressed = false;
    if (current != null)
    {
        Debug.Assert(!current.IsDefault);
        Debug.Assert(current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context");

        isFlowSuppressed = current.m_isFlowSuppressed;
		// 这一步很关键,通过调用 m_localValues.Set 对 map 进行修改,这会产生一个新的 map 实例。
        newValues = current.m_localValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
        newChangeNotifications = current.m_localChangeNotifications;
    }
    else
    {
        // 如果当前上下文不存在,创建第一个 IAsyncLocalValueMap 实例
        newValues = AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
    }

    // 如果 AsyncLocal 注册了回调,则需要保存 AsyncLocal 的引用
    // 这边会有两种情况,一个是数组未创建过,一个是数组已存在
    if (needChangeNotifications)
    {
        if (hadPreviousValue)
        {
            Debug.Assert(newChangeNotifications != null);
            Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
        }
        else if (newChangeNotifications == null)
        {
            newChangeNotifications = new IAsyncLocal[1] { local };
        }
        else
        {
            int newNotificationIndex = newChangeNotifications.Length;
			// 这个方法会创建一个新数组并将原来的元素拷贝过去
			Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
            newChangeNotifications[newNotificationIndex] = local;
        }
    }

	// 如果 AsyncLocal 存在有效值,且允许执行上下文流动,则创建新的 ExecutionContext实例,新实例会保存所有的AsyncLocal的值和所有需要通知的 AsyncLocal 引用。
    Thread.CurrentThread._executionContext =
        (!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
        null : // No values, return to Default context
        new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);

    if (needChangeNotifications)
    {
		// 调用先前注册好的委托
        local.OnValueChanged(previousValue, newValue, contextChanged: false);
    }
}

2.2.5、ExecutionContext – GetLocalValue#

值的获取实现相对简单

internal static object? GetLocalValue(IAsyncLocal local)
{
    ExecutionContext? current = Thread.CurrentThread._executionContext;
    if (current == null)
    {
        return null;
    }

    Debug.Assert(!current.IsDefault);
    Debug.Assert(current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context");
    current.m_localValues.TryGetValue(local, out object? value);
    return value;
}

3、ExecutionContext 的流动

在线程发生切换的时候,ExecutionContext 会在前一个线程中被默认捕获,流向下一个线程,它所保存的数据也就随之流动。

在所有会发生线程切换的地方,基础类库(BCL) 都为我们封装好了对执行上下文的捕获。

例如:

  • new Thread(ThreadStart start).Start()
  • Task.Run(Action action)
  • ThreadPool.QueueUserWorkItem(WaitCallback callBack)
  • await 语法糖
class Program
{
    static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();

    static async Task Main(string[] args)
    {
        _asyncLocal.Value = "AsyncLocal保存的数据";

        new Thread(() =>
        {
            Console.WriteLine($"new Thread: {_asyncLocal.Value}");
        })
        {
            IsBackground = true
        }.Start();

        ThreadPool.QueueUserWorkItem(_ =>
        {
            Console.WriteLine($"ThreadPool.QueueUserWorkItem: {_asyncLocal.Value}");
        });

        Task.Run(() =>
        {
            Console.WriteLine($"Task.Run: {_asyncLocal.Value}");
        });

        await Task.Delay(100);
        Console.WriteLine($"after await: {_asyncLocal.Value}");
    }
}

输出结果:

new Thread: AsyncLocal保存的数据
ThreadPool.QueueUserWorkItem: AsyncLocal保存的数据
Task.Run: AsyncLocal保存的数据
after await: AsyncLocal保存的数据

3.1、流动的禁止和恢复#

ExecutionContext 为我们提供了 SuppressFlow(禁止流动) 和 RestoreFlow (恢复流动)这两个静态方法来控制当前线程的执行上下文是否像辅助线程流动。并可以通过 IsFlowSuppressed 静态方法来进行判断。

class Program
{
    static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();

    static async Task Main(string[] args)
    {
        _asyncLocal.Value = "AsyncLocal保存的数据";

        Console.WriteLine("默认:");
        PrintAsync(); // 不 await,后面的线程不会发生切换

        Thread.Sleep(1000); // 确保上面的方法内的所有线程都执行完

        ExecutionContext.SuppressFlow();
        Console.WriteLine("SuppressFlow:");
        PrintAsync();

        Thread.Sleep(1000);

        Console.WriteLine("RestoreFlow:");

        ExecutionContext.RestoreFlow();
        await PrintAsync();

        Console.Read();
    }

    static async ValueTask PrintAsync()
    {
        new Thread(() =>
        {
            Console.WriteLine($"    new Thread: {_asyncLocal.Value}");
        })
        {
            IsBackground = true
        }.Start();

        Thread.Sleep(100); // 保证输出顺序

        ThreadPool.QueueUserWorkItem(_ =>
        {
            Console.WriteLine($"    ThreadPool.QueueUserWorkItem: {_asyncLocal.Value}");
        });

        Thread.Sleep(100);

        Task.Run(() =>
        {
            Console.WriteLine($"    Task.Run: {_asyncLocal.Value}");
        });

        await Task.Delay(100);
        Console.WriteLine($"    after await: {_asyncLocal.Value}");

        Console.WriteLine();
    }
}

输出结果:

默认:
new Thread: AsyncLocal保存的数据
ThreadPool.QueueUserWorkItem: AsyncLocal保存的数据
Task.Run: AsyncLocal保存的数据
after await: AsyncLocal保存的数据

SuppressFlow:
new Thread:
ThreadPool.QueueUserWorkItem:
Task.Run:
after await:

RestoreFlow:
new Thread: AsyncLocal保存的数据
ThreadPool.QueueUserWorkItem: AsyncLocal保存的数据
Task.Run: AsyncLocal保存的数据
after await: AsyncLocal保存的数据

需要注意的是,在线程A中创建线程B之前调用 ExecutionContext.SuppressFlow 只会影响 ExecutionContext 从线程A => 线程B的传递,线程B => 线程C 不受影响。

class Program
{
    static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    static void Main(string[] args)
    {
        _asyncLocal.Value = "A => B";
        ExecutionContext.SuppressFlow();
        new Thread((() =>
        {
            Console.WriteLine($"线程B:{_asyncLocal.Value}"); // 输出线程B:

            _asyncLocal.Value = "B => C";
            new Thread((() =>
            {
                Console.WriteLine($"线程C:{_asyncLocal.Value}"); // 输出线程C:B => C
            }))
            {
                IsBackground = true
            }.Start();
        }))
        {
            IsBackground = true
        }.Start();

        Console.Read();
    }
}

3.2、ExcutionContext 的流动实现#

上面举例了四种场景,由于每一种场景的传递过程都比较复杂,目前先介绍其中一个。

但不管什么场景,都会涉及到 ExcutionContext 的 Run 方法。在Run 方法中会调用 RunInternal 方法,

public static void Run(ExecutionContext executionContext, ContextCallback callback, object? state)
{
    if (executionContext == null)
    {
        ThrowNullContext();
    }

	// 内部会调用 RestoreChangedContextToThread 方法
    RunInternal(executionContext, callback, state);
}

RunInternal 调用下面一个 RestoreChangedContextToThread 方法将 ExcutionContext.Run 方法传入的 ExcutionContext 赋值给当前线程的 _executionContext 字段。

internal static void RestoreChangedContextToThread(Thread currentThread, ExecutionContext? contextToRestore, ExecutionContext? currentContext)
{
    Debug.Assert(currentThread == Thread.CurrentThread);
    Debug.Assert(contextToRestore != currentContext);

	// 在这边把之前的 ExecutionContext 赋值给了当前线程
    currentThread._executionContext = contextToRestore;
    if ((currentContext != null && currentContext.HasChangeNotifications) ||
        (contextToRestore != null && contextToRestore.HasChangeNotifications))
    {
        OnValuesChanged(currentContext, contextToRestore);
    }
}

3.2.1、new Thread(ThreadStart start).Start() 为例说明 ExecutionContext 的流动#

这边可以分为三个步骤:

在 Thread 的 Start 方法中捕获当前的 ExecutionContext,将其传递给 Thread 的构造函数中实例化的 ThreadHelper 实例,ExecutionContext 会暂存在 ThreadHelper 的实例字段中,线程创建完成后会调用ExecutionContext.RunInternal 将其赋值给新创建的线程。

代码位置:

https://github.com/dotnet/runtime/blob/5fca04171171f118bca0f93aa9741f205b8cdc29/src/coreclr/src/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs#L200

        public void Start()
        {
#if FEATURE_COMINTEROP_APARTMENT_SUPPORT
            // Eagerly initialize the COM Apartment state of the thread if we're allowed to.
            StartupSetApartmentStateInternal();
#endif // FEATURE_COMINTEROP_APARTMENT_SUPPORT

            // Attach current thread's security principal object to the new
            // thread. Be careful not to bind the current thread to a principal
            // if it's not already bound.
            if (_delegate != null)
            {
                // If we reach here with a null delegate, something is broken. But we'll let the StartInternal method take care of
                // reporting an error. Just make sure we don't try to dereference a null delegate.
                Debug.Assert(_delegate.Target is ThreadHelper);
                // 由于 _delegate 指向 ThreadHelper 的实例方法,所以 _delegate.Target 指向 ThreadHelper 实例。
                var t = (ThreadHelper)_delegate.Target;

                ExecutionContext? ec = ExecutionContext.Capture();
                t.SetExecutionContextHelper(ec);
            }

            StartInternal();
        }

https://github.com/dotnet/runtime/blob/5fca04171171f118bca0f93aa9741f205b8cdc29/src/coreclr/src/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs#L26

class ThreadHelper
{
    internal ThreadHelper(Delegate start)
    {
        _start = start;
    }

    internal void SetExecutionContextHelper(ExecutionContext? ec)
    {
        _executionContext = ec;
    }

    // 这个方法是对 Thread 构造函数传入的委托的包装
    internal void ThreadStart()
    {
        Debug.Assert(_start is ThreadStart);

        ExecutionContext? context = _executionContext;
        if (context != null)
        {
			// 将 ExecutionContext 与 CurrentThread 进行绑定
            ExecutionContext.RunInternal(context, s_threadStartContextCallback, this);
        }
        else
        {
            InitializeCulture();
            ((ThreadStart)_start)();
        }
    }
}

4、总结

  1. AsyncLocal 本身不保存数据,数据保存在 ExecutionContext 实例的 m_localValues 的私有字段上,字段类型定义是 IAsyncLocalMap ,以 IAsyncLocal => object 的 Map 结构进行保存,且实现类型随着元素数量的变化而变化。
  2. ExecutionContext 实例 保存在 Thread.CurrentThread._executionContext 上,实现与当前线程的关联。
  3. 对于 IAsyncLocalMap 的实现类,如果 AsyncLocal 注册了回调,value 传 null 不会被忽略。

    没注册回调时分为两种情况:如果 key 存在,则做删除处理,map 类型可能出现降级。如果 key 不存在,则直接忽略。

  4. ExecutionContext 和 IAsyncLocalMap 的实现类都被设计成不可变(immutable)。同一个 key 前后两次 value 发生变化后,会产生新的 ExecutionContext 的实例和 IAsyncLocalMap 实现类实例。
  5. ExecutionContext 与当前线程绑定,默认流动到辅助线程,可以禁止流动和恢复流动,且禁止流动仅影响当前线程向其辅助线程的传递,不影响后续。

5、参考

  1. https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/
  2. 《CLR via C#》27.3 章节
  3. github 代码库 https://github.com/dotnet/runtime

ASP.NET Core 认证与授权[1]:初识认证 - 雨夜朦胧 - 博客园

mikel阅读(652)

来源: ASP.NET Core 认证与授权[1]:初识认证 – 雨夜朦胧 – 博客园

ASP.NET 4.X 中,我们最常用的是Forms认证,它既可以用于局域网环境,也可用于互联网环境,有着非常广泛的使用。但是它很难进行扩展,更无法与第三方认证集成,因此,在 ASP.NET Core 中对认证与授权进行了全新的设计,并使用基于声明的认证(claims-based authentication),以适应现代化应用的需求。在运行原理解剖[5]:Authentication中介绍了一下HttpContext与认证系统的集成,本系列文章则来详细介绍一下 ASP.NET Core 中认证与授权。

目录

  1. 基于声明的认证
  2. ASP.NET Core 中的用户身份
  3. Microsoft.AspNetCore.Authentication
  4. 认证Handler

基于声明的认证

Claim 通常被翻译成声明,但是感觉过于生硬,还是使用Claim来称呼更加自然一些。记得是在MVC5中,第一次接触到 “Claim” 的概念。在MVC5之前,我们所熟悉的是Windows认证和Forms认证,Windows认证通常用于企业内部,我们使用最多的还是Forms认证,先来回顾一下,以前是怎么使用的:

首先我们会在web.config中配置认证模式:

<authentication mode="Forms">
    <forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

认证票据的生成是使用FormsAuthentication来完成的:

FormsAuthentication.SetAuthCookie("bob", true);

然后便可以通过HttpContext.User.Identity.Name获取到当前登录用户的名称:”bob”,那么它是如何来完成认证的呢?

在 ASP.NET 4.x 中,我们应该都对 HttpModule 比较了解,它类似于 ASP.NET Core 中的中件间,ASP.NET 默认会在全局的 administration.config 文件中注册一大堆HttpModule,其中就包括WindowsAuthenticationFormsAuthentication,用来实现Windows认证和Forms认证:

<moduleProviders>
    <!-- Server Modules-->
    <add name="Authentication" type="Microsoft.Web.Management.Iis.Authentication.AuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="AnonymousAuthentication" type="Microsoft.Web.Management.Iis.Authentication.AnonymousAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="BasicAuthentication" type="Microsoft.Web.Management.Iis.Authentication.BasicAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="ActiveDirectoryAuthentication" type="Microsoft.Web.Management.Iis.Authentication.ActiveDirectoryAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="WindowsAuthentication" type="Microsoft.Web.Management.Iis.Authentication.WindowsAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="DigestAuthentication" type="Microsoft.Web.Management.Iis.Authentication.DigestAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

    <!-- ASP.NET Modules-->
    <add name="FormsAuthentication" type="Microsoft.Web.Management.AspNet.Authentication.FormsAuthenticationModuleProvider, Microsoft.Web.Management.Aspnet, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />        

可能大多人都不知道有这些Module,这也是微软技术的一大弊端,总想着封装成傻瓜化,造成入门容易,精通太难的局面。

如上,我们可以看到生成票据时,默认只能转入一个Name,当然也可以通过手动创建FormsAuthenticationTicket来附带一些额外的信息,但是都太过麻烦。

在传统的身份认证中,每个应用程序都有它自己的验证用户身份的方式,以及它自己的用户数据库。这种方式有很大的局限性,因为它很难集成多种认证方式以支持用户使用不同的方式来访问我们的应用程序,比如组织内的用户(Windows-baseed 认证),其它组织的用户(Identity federation)或者是来自互联网的用户(Forms-based 认证)等等。

Claim 是关于一个人或组织的某个主题的陈述,比如:一个人的名称,角色,个人喜好,种族,特权,社团,能力等等。它本质上就是一个键值对,是一种非常通用的保存用户信息的方式,可以很容易的将认证和授权分离开来,前者用来表示用户是/不是什么,后者用来表示用户能/不能做什么。

因此基于声明的认证有两个主要的特点:

  • 将认证与授权拆分成两个独立的服务。
  • 在需要授权的服务中,不用再去关心你是如何认证的,你用Windows认证也好,Forms认证也行,只要你出示你的 Claims 就行了。

ASP.NET Core 中的用户身份

Claim

在 ASP.NET Core 中,使用Cliam类来表示用户身份中的一项信息,它由核心的TypeValue属性构成:

public class Claim
{
    private readonly string _type;
    private readonly string _value;

    public Claim(string type, string value)
        : this(type, value, ClaimValueTypes.String, ClaimsIdentity.DefaultIssuer, ClaimsIdentity.DefaultIssuer, null, null, null)
    {
    }

    internal Claim(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity subject, string propertyKey, string propertyValue)
    {
        ...
    }

    public string Type => _type;
    public string Value => _value;
}

一个Claim可以是“用户的姓名”,“邮箱地址”,“电话”,等等,而多个Claim构成一个用户的身份,使用ClaimsIdentity类来表示:

ClaimsIdentity

public class ClaimsIdentity : IIdentity
{    
    public virtual IEnumerable<Claim> Claims {get;}

    public virtual string AuthenticationType => _authenticationType;
    public virtual bool IsAuthenticated => !string.IsNullOrEmpty(_authenticationType);
    public virtual string Name
    {
        get
        {
            Claim claim = FindFirst(_nameClaimType);
            if (claim != null) return claim.Value;
            return null;
        }
    }

}

如上,其Name属性用来查找Claims中,第一个Type为我们创建ClaimsIdentity时指定的NameClaimType的Claim的值,若未指定Type时则使用默认的ClaimTypes.Name。而IsAuthenticated只是判断_authenticationType是否为空,_authenticationType则对应上一章中介绍的Scheme

下面,我们演示一下用户身份的创建:

// 创建一个用户身份,注意需要指定AuthenticationType,否则IsAuthenticated将为false。
var claimIdentity = new ClaimsIdentity("myAuthenticationType");
// 添加几个Claim
claimIdentity.AddClaim(new Claim(ClaimTypes.Name, "bob"));
claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "bob@gmail.com"));
claimIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone, "18888888888"));

如上,我们可以根据需要添加任意个的Claim,最后我们还需要再将用户身份放到ClaimsPrincipal对象中。

ClaimsPrincipal

那么,ClaimsPrincipal是什么呢?在 ASP.NET 4.x 中我们可能对IPrincipal接口比较熟悉,在Controller中的User属性便是IPrincipal类型:

public interface IPrincipal
{
    IIdentity Identity { get; }
    bool IsInRole(string role);
}

可以看到IPrincipal除了包含用户身份外,还有一个IsInRole方法,用于判断用户是否属于指定角色,在基于角色的授权当中便是调用此方法来实现的。

而在 ASP.NET Core 中,HttpContext直接使用的就是ClaimsPrincipal类型,而不再使用IPrincipal

public abstract class HttpContext
{
    public abstract ClaimsPrincipal User { get; set; }
}

而在ClaimsPrincipal中,可以包含多个用户身份(ClaimsIdentity),除了对用户身份的操作,还提供了针对Claims的查询:

public class ClaimsPrincipal : IPrincipal
{
    private readonly List<ClaimsIdentity> _identities = new List<ClaimsIdentity>();

    public ClaimsPrincipal(IEnumerable<ClaimsIdentity> identities) 
    {
        _identities.AddRange(identities);
    }

    // 默认从_identities中查找第一个不为空的ClaimsIdentity,也可以自定义查找方式。
    public virtual System.Security.Principal.IIdentity Identity {}

    // 查找_identities中是否包含类型为RoleClaimType(在创建ClaimsIdentity时指定,或者默认的ClaimTypes.Role)的Claim。
    public virtual bool IsInRole(string role) {}

    // 获取所有身份的Claim集合
    public virtual IEnumerable<Claim> Claims
    {
        get
        {
            foreach (ClaimsIdentity identity in Identities)
            {
                foreach (Claim claim in identity.Claims)
                {
                    yield return claim;
                }
            }
        }
    }
}

ClaimsPrincipal的创建非常简单,只需传入我们上面创建的用户身份即可:

var principal = new ClaimsPrincipal(claimIdentity);

由于HTTP是无状态的,我们通常使用Cookie,请求头或请求参数等方式来附加用户的信息,在网络上进行传输,这就涉及到序列化和安全方面的问题。因此,还需要将principal对象包装成AuthenticationTicket对象。

AuthenticationTicket

当我们创建完ClaimsPrincipal对象后,需要将它生成一个用户票据并颁发给用户,然后用户拿着这个票据,便可以访问受保持的资源,而在 ASP.NET Core 中,用户票据用AuthenticationTicket来表示,如在Cookie认证中,其认证后的Cookie值便是对该对象序列化后的结果,它的定义如下:

public class AuthenticationTicket
{
    public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme)
    {
        AuthenticationScheme = authenticationScheme;
        Principal = principal;
        Properties = properties ?? new AuthenticationProperties();
    }
    public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme) 
        : this(principal, properties: null, authenticationScheme: authenticationScheme) { }
    public string AuthenticationScheme { get; private set; }
    public ClaimsPrincipal Principal { get; private set; }
    public AuthenticationProperties Properties { get; private set; }
}

用户票据除了包含上面创建的principal对象外,还需要指定一个AuthenticationScheme (通常在授权中用来验证Scheme),并且还包含一个AuthenticationProperties对象,它主要是一些用户票据安全方面的一些配置,如过期时间,是否持久等。

var properties = new AuthenticationProperties();
var ticket = new AuthenticationTicket(principal, properties, "myScheme");
// 加密 序列化
var token = Protect(ticket);

最后,我们可以将票据(token)写入到Cookie中,或是也可以以JSON的形式返回让客户端自行保存,由于我们对票据进行了加密,可以保证在网络中安全的传输而不会被篡改。

最终身份令牌的结构大概是这样的:

claim-token

Microsoft.AspNetCore.Authentication

上面,我们介绍了身份票据的创建过程,下面就来介绍一下 ASP.NET Core 中的身份认证。

ASP.NET Core 中的认证系统具体实现在 Security 项目中,它包含 CookieJwtBearerOAuthOpenIdConnect 等:

security_src_dir

认证系统提供了非常灵活的扩展,可以让我们很容易的实现自定义认证方式。

Usage

而对于认证系统的配置,分为两步,也是我们所熟悉的注册服务和配置中间件:

首先,在DI中注册服务认证所需的服务:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(o =>
    {
        o.ClientId = "server.hybrid";
        o.ClientSecret = "secret";
        o.Authority = "https://demo.identityserver.io/";
        o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    });
}

最后,注册认证中间件:

public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();
}

如上,我们的系统便支持了CookieJwtBearer两种认证方式,是不是非常简单,在我们的应用程序中使用认证系统时,只需要调用 上一章 介绍的 HttpContext 中认证相关的扩展方法即可。

Microsoft.AspNetCore.Authentication,是所有认证实现的公共抽象类,它定义了实现认证Handler的规范,并包含一些共用的方法,如令牌加密,序列化等,AddAuthentication 便是其提供的统一的注册认证服务的扩展方法:

AddAuthentication

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
{
    services.AddAuthenticationCore();
    services.AddDataProtection();
    services.AddWebEncoders();
    services.TryAddSingleton<ISystemClock, SystemClock>();
    return new AuthenticationBuilder(services);
}

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) 
{
    var builder = services.AddAuthentication();
    services.Configure(configureOptions);
    return builder;
}

如上,它首先会调用上一章中介绍的AddAuthenticationCore方法,然后注册了DataProtectionWebEncoders两个服务。而对 AuthenticationOptions 我们之前在IAuthenticationSchemeProvider也介绍过,它用来配置Scheme。

AddScheme

在上面的 AddAuthentication 中返回的是一个AuthenticationBuilder类型,所有认证Handler的注册都是以它的扩展形式来实现的,它同时也提供了AddScheme扩展方法,使我们可以更加方便的来配置Scheme:

public class AuthenticationBuilder
{
    public AuthenticationBuilder(IServiceCollection services)
        => Services = services;

    public virtual IServiceCollection Services { get; }

    public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, Action<TOptions> configureOptions)
        where TOptions : AuthenticationSchemeOptions, new()
        where THandler : AuthenticationHandler<TOptions>
        => AddScheme<TOptions, THandler>(authenticationScheme, displayName: null, configureOptions: configureOptions);

    public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
        where TOptions : AuthenticationSchemeOptions, new()
        where THandler : AuthenticationHandler<TOptions>
    {
        Services.Configure<AuthenticationOptions>(o =>
        {
            o.AddScheme(authenticationScheme, scheme => {
                scheme.HandlerType = typeof(THandler);
                scheme.DisplayName = displayName;
            });
        });
        if (configureOptions != null)
        {
            Services.Configure(authenticationScheme, configureOptions);
        }
        Services.AddTransient<THandler>();
        return this;
    }
}

在这里的AddScheme 扩展方法只是封装了对AuthenticationOptionsAddScheme的调用,如上面示例中的AddCookie便是调用该扩展方法来实现的。

AddRemoteScheme

看到 Remote 我们应该就可以猜到它是一种远程验证方式,先看一下它的定义:

public class AuthenticationBuilder
{
    public virtual AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
        where TOptions : RemoteAuthenticationOptions, new()
        where THandler : RemoteAuthenticationHandler<TOptions>
    {
        Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureSignInScheme<TOptions>>());
        return AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions: configureOptions);
    }

    private class EnsureSignInScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
    {
        private readonly AuthenticationOptions _authOptions;

        public EnsureSignInScheme(IOptions<AuthenticationOptions> authOptions)
        {
            _authOptions = authOptions.Value;
        }

        public void PostConfigure(string name, TOptions options)
        {
            options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
            if (string.Equals(options.SignInScheme, name, StringComparison.Ordinal))
            {
                throw new InvalidOperationException(Resources.Exception_RemoteSignInSchemeCannotBeSelf);
            }
        }
    }
}

首先使用PostConfigure模式(参见:Options[1]:Configure),对RemoteAuthenticationOptions进行验证,要求远程验证中指定的SignInScheme不能为自身,这是为什么呢?后文再来解释。然后便是直接调用上面介绍的 AddScheme 方法。

关于远程验证相对比较复杂,在本章中并不会太过深入的来介绍,在后续其它文章中会逐渐深入。

UseAuthentication

在上面,注册认证中间件时,我们只需调用一个UseAuthentication扩展方法,因为它会执行我们注册的所有认证Handler:

public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app)
{
    return app.UseMiddleware<AuthenticationMiddleware>();
}

咦,它的代码好简单,只是注册了一个 AuthenticationMiddleware 而已,迫不及待的想看看它的实现:

public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    public IAuthenticationSchemeProvider Schemes { get; set; }

    public async Task Invoke(HttpContext context)
    {
        context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
        {
            OriginalPath = context.Request.Path,
            OriginalPathBase = context.Request.PathBase
        });

        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);
    }
}

很简单,但是很强大,不管我们是使用Cookie认证,还是Bearer认证,等等,都只需要这一个中间件,因为它会解析所有的Handler来执行。

不过,在这里,这会先判断是否具体实现了IAuthenticationRequestHandler的Hander,优先来执行,这个是什么鬼?

查了一下,发现IAuthenticationRequestHandler是在HttpAbstractions中定义的,只是在运行原理解剖[5]:Authentication中没有介绍到它:

public interface IAuthenticationRequestHandler : IAuthenticationHandler
{
    Task<bool> HandleRequestAsync();
}

它多了一个HandleRequestAsync方法,那么它存在的意义是什么呢?其实在Cookie认证中并没有用到它,它通常在远程认证(如:OAuth, OIDC等)中使用,下文再来介绍。

继续分析上面代码,通过调用Schemes.GetDefaultAuthenticateSchemeAsync来获取到认证的Scheme,也就是上文提到的问题,我们必须指定默认的Scheme。

最后,调用AuthenticateAsync方法进行认证,认证成功后,为HttpContext.User赋值,至于如何解析身份令牌生成ClaimsPrincipal对象,则交给相应的Handler来处理。

认证Handler

上文中多次提到认证Handler,它由统一的AuthenticationMiddleware来调用,负责具体的认证实现,并分为本地认证与远程认证两种方式。

在本地验证中,身份令牌的发放与认证通常是由同一个服务器来完成,这也是我们比较熟悉的场景,对于Cookie, JwtBearer等认证来说,都属于是本地验证。而当我们使用OAuth, OIDC等验证方式时,身份令牌的发放则是由独立的服务或是第三方(QQ, Weibo 等)认证来提供,此时在我们的应用程序中获取身份令牌时需要请求远程服务器,因此称之为远程验证。

AuthenticationHandler

AuthenticationHandler是所有认证Handler的抽象基类,对于本地认证直接实现该类即可,定义如下:

public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
{
    ...

    public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
        ...

        await InitializeEventsAsync();
        await InitializeHandlerAsync();
    }

    protected virtual async Task InitializeEventsAsync() { }
    protected virtual Task<object> CreateEventsAsync() => Task.FromResult(new object());
    protected virtual Task InitializeHandlerAsync() => Task.CompletedTask;

    public async Task<AuthenticateResult> AuthenticateAsync()
    {
        var result = await HandleAuthenticateOnceAsync();

        ...
    }

    protected Task<AuthenticateResult> HandleAuthenticateOnceAsync()
    {
        if (_authenticateTask == null)
        {
            _authenticateTask = HandleAuthenticateAsync();
        }
        return _authenticateTask;
    }

    protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();


    protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 403;
        return Task.CompletedTask;
    }

    protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 401;
        return Task.CompletedTask;
    }

    ...
}

如上,它定义一个抽象方法HandleAuthenticateAsync,并使用HandleAuthenticateOnceAsync方法来保证其在每次认证只执行一次。而HandleAuthenticateAsync是认证的核心,交给具体的认证Handler负责实现。而对于 ChallengeAsync, ForbidAsync 等方法也提供了默认的实现。

而对于HandleAuthenticateAsync的实现,大致的逻辑就是从请求中获取上面发放的身份令牌,然后解析成AuthenticationTicket,并经过一系列的验证,最终返回ClaimsPrincipal对象。

RemoteAuthenticationHandler

RemoteAuthenticationHandler 便是所有远程认证的抽象基类了,它继承自AuthenticationHandler,并实现了IAuthenticationRequestHandler接口:

public abstract class RemoteAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions>, IAuthenticationRequestHandler
    where TOptions : RemoteAuthenticationOptions, new()
{

    public virtual Task<bool> ShouldHandleRequestAsync() => Task.FromResult(Options.CallbackPath == Request.Path);

    public virtual async Task<bool> HandleRequestAsync()
    {
        if (!await ShouldHandleRequestAsync())
        {
            return false;
        }

        var authResult = await HandleRemoteAuthenticateAsync();
 
        ...

        await Context.SignInAsync(SignInScheme, ticketContext.Principal, ticketContext.Properties);

        if (string.IsNullOrEmpty(ticketContext.ReturnUri)) ticketContext.ReturnUri = "/";
        Response.Redirect(ticketContext.ReturnUri);
        return true;
    }

    protected abstract Task<HandleRequestResult> HandleRemoteAuthenticateAsync();

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var result = await Context.AuthenticateAsync(SignInScheme);

        ...
    }

    protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
        => Context.ForbidAsync(SignInScheme);

    protected virtual void GenerateCorrelationId(AuthenticationProperties properties) {}
    protected virtual bool ValidateCorrelationId(AuthenticationProperties properties) {}
}

在上面介绍的AuthenticationMiddleware中,提到它会先执行实现了IAuthenticationRequestHandler 接口的Handler(远程认证),之后(若未完成认证)再执行本地认证Handler。

RemoteAuthenticationHandler中核心的认证逻辑便是 HandleRequestAsync 方法,它主要包含2个步骤:

  1. 首先执行一个抽象方法HandleRemoteAuthenticateAsync,由具体的Handler来实现,该方法返回的HandleRequestResult对象包含验证的结果(跳过,失败,成功等),在成功时会包含一个ticket对象。
  2. 若上一步验证成功,则根据返回的ticket,获取到ClaimsPrincipal对象,并调用其它认证Handler的Context.SignInAsync方法。

也就是说,远程Hander会在用户未登录时,指引用户跳转到认证服务器,登录成功后,解析认证服务器传回的凭证,最终依赖于本地Handler来保存身份令牌。当用户再次访问则无需经过远程Handler,直接交给本地Handler来处理。

由此也可以知道,远程认证中本身并不具备SignIn的能力,所以必须通过指定其它SignInScheme交给本地认证来完成 SignIn

对于其父类的HandleAuthenticateAsync抽象方法则定义了一个默认实现:“直接转交给本地验证来处理”。当我们需要定义自己的远程认证方式时,通常只需实现 HandleRemoteAuthenticateAsync 即可,而不用再去处理 HandleAuthenticateAsync 。

总结

基于声明的认证并不是微软所特有的,它在国外被广泛的使用,如微软的ADFS,Google,Facebook,Twitter等等。在基于声明的认证中,对认证和授权进行了明确的区分,认证用来颁发一个用户的身份标识,其包含这个用户的基本信息,而对于这个身份的颁发则由我们信任的第三方机构来(STS)颁发(当然,你也可以自己来颁发)。而授权,则是通过获取身份标识中的信息,来判断该用户能做什么,不能做什么。

本文对 ASP.NET Core 中认证系统的整个流程做了一个简要的介绍,可能会比较苦涩难懂,不过没关系,大致有个印象就好,下一章则详细介绍一下最常用的本地认证方式:Cookie认证,后续也会详细介绍 OIDC 的用法与实现,到时再回头来看本文或许会豁然开朗。