写在2019的年末

还记得我在去年年末感慨,2018年过得飞快,如同飞鸟一般,时间就倏忽飞走了。而2019年,这鸟飞得更快,连影子没留下。

今年最重大的事件莫过于儿子出生,几乎每个周末都在围着老婆和宝宝转。用于学习的空闲时间自然是没挤出来。我以前并不觉得那些能在带娃期间备考的有多厉害,如今才体会到其中的不易。如果从外部来观察我的活动记录,最直观的表现是博客停更了,这一点非常不好。我的个人博客大多是笔记性质的文字,用于对所见所学进行记录与总结。子曰,吾日三省吾身。我今年动辄连续数月没有总结,虽说在有道云经常还有些学习笔记,但是总归没有放到博客,形成总结性的正式记录。难怪要到年末总结的时候才意识到好多计划没有落实。

年初目标与惨淡的2019

总结来说,2019年立的众多Flag里,只有寥寥数个是完成了的:

  1. 编程语言学习:我之前说,要每年尝试接触新的编程语言。2019年学了点F#皮毛,虽说没有深入研究,但在用F#写了个斗地主引擎的过程中,让我真正感受到了F#语言的超强表现力。以前老听人说F#函数式编程有多优越,当时我并未理解,甚至对模式匹配的过人之处也不屑一顾。如今自己动手实践了,才体会到模式匹配、Options、主动模式等特性的精妙之处。学了一门语言,总不免要和其他语言对比。由于都采用缩进语法,我在学习F#的过程中,经常不由自主地对Python产生鄙夷:大家都说Python简洁优雅:优雅问题不大,然而没有|>>>的编程语言,如何能称简洁?这种思想非常不好,为了矫正我的这个观念,我不停地跟自己讲:不宜过多评判编程语言本身,编程语言本身设计的优劣与否并不重要——Python远比F#流行,放弃Python就是自绝于生态。面对技术,不可有虚假的优越感。

    Read More

Authorization — (4) 自定义授权机制

自定义 Policy

通常情况下,我们可以为AuthorizationOptions配置多种Policies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
services.AddAuthorization(opts=> {
opts.AddPolicy("CanEnterSecurity", policyBuilder => policyBuilder.RequireClaim("FullName", "Itminus"));

// a function that can executed at runtime
opts.AddPolicy("CanDoRuntime",policyBuilder => policyBuilder.RequireAssertion(async context => {
await Task.Run(()=> { /* pretend doing some sth*/ });
if (context.User.Identity.Name.Contains("admin")) {
return true;
}
return false;
}));

opts.AddPolicy("ResourceOwnnershipCheck",pb=> pb.RequireAssertion(async(context) =>{
var resource = (Dictionary<string,string>)context.Resource;
return resource["P1"].Contains("World");
}));
});

有时候,这种inline风格的代码对于解决复杂问题稍显乏力。这种情况下,我们可以自定义Requirement和相应的AuthorizationHandler<TRequirement>处理器,然后把相关授权处理器注册为相关服务即可。

绝大部分需求都可以使用以上的方法解决。下面看一个自定义AuthorizationPolicyProvider的例子。

Read More

Authorization — (3) ASP.NET Core 的授权中间件和MVC授权过滤器

授权中间件

ASP.NET Core 2.1中的路由中间件不同,在3.0中新的EndPoint路由机制无需实际执行路由便可获取当前所匹配的EndPoint。正是得益于这套新引入的EndPoint路由系统,ASP.NET Core框架可在执行MVC路由之前,就可以捕捉到相应EndPoint的授权配置信息(IAuthorizeData)。基于此,ASP.NET Core 3.0中为授权机制做了重大调整,即引入了授权中间件。这意味着在3.0中我们需要在UseAuthentication()之后尽快调用UseAuthorization()方法:

1
2
3
4
5
6
7
8
9
10
app.UseRouting();
// ...
app.UseAuthentication();
app.UseAuthorization(); // 启用授权中间件
// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ChatHub>("/chat");
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
})

新引入的授权中间件的核心工作主要分成两部分:

  • 借助于IAuthorizationPolicyProvider服务和当前EndPointIAuthorizeData,构建一个Policy对象。如果没有相应的Policy,则直接调用后续中间件(跳过剩余的授权过程)。
  • 通过IPolicyEvaluator服务判断当前HttpContext是否满足Policy。如果不满足,根据授权结果决定是Challenge还是Forbid;否则,则继续调用后续中间件对请求进行处理。

    Read More

Authorization — (2) 授权处理器、授权服务、和Policy Evaluator

授权处理器与Provider

AuthorizationHandler

AuthorizationHandler表示针对具体Requirement的处理器:

1
2
3
4
public interface IAuthorizationHandler
{
Task HandleAsync(AuthorizationHandlerContext context);
}

抽象类AuthorizationHandler<TRequirement>的默认逻辑是针对所有的TRequirement都进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

注意,尽管AuthorizatoinHandler<TRequirement>类包含了为一个TRequirement类型,但是一种类型的TRequirement,可以被应用于多种类型的Handler——在HandleAsync()方法中,这些Handler会被逐一调用。

特别地,根据所要授权的目标的不同,授权处理器还分化出了针对RequirementResource的抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler
where TRequirement : IAuthorizationRequirement
{
public virtual async Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.Resource is TResource)
{
foreach (var req in context.Requirements.OfType<TRequirement>())
{
await HandleRequirementAsync(context, req, (TResource)context.Resource);
}
}
}

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

Read More

Authorization — (1) 授权选项与Policy获取

Authentication一样,Authorization机制也有一个对应的AuthorizationOptions供开发者进行配置。更进一步地,和AuthenticationOptions.AddScheme(name,configureBuilder)类似,AuthorizationOptions也提供了一个名为AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)方法来配置授权策略。

不过和Authentication机制不同的是,AddAuthentication()返回的是一个AuthenticationBuilder实例,这样开发者就可以链式构建AuthenticationOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services.AddAuthentication(options =>{
...
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})

但是AddAuthorization()并没有返回一个Builder之类东西来供开发者链式构建AuthorizationOptions,这可能是基于向后兼容考虑。在Authorization中,开发者直接使用AddPolicy()等实例方法来配置授权策略:

Read More

Authorization — (0) 基础概念 Policy的组合与构建

ASP.NET Core中,AuthorizationAuthentication机制有很大的相似之处:

  1. 一个WebApp中,可以有多种AuthenticationScheme;类似的,也可以指定多种AuthorizationPolicy
  2. Authentication通过AuthentionOptions来配置认证行为;类似的,Authorization通过AuthorizationOptions来配置授权行为。
  3. AuthenticationOptions提供AddScheme(name,func)方法来注册认证模式,并提供通过AuthenticationBuilder来构建AuthenticationOptions;而AuthorizationOptions提供AddPolicy(name,func)方法来添加授权策略,AuthorizationBuilder 则负责多个Requirements/Policy的组合,从而构建出最终的AuthorizationOptions
  4. 认证处理器AuthenticationHandler<TSchemeOptions>依据某种具体模式选项进行认证;而授权处理器AuthorizationHandler<TRequirement>则依据某种具体的TRequirement进行授权。
  5. Authentication提供了一个中间件来自动认证;Authorization也提供了一个MVC Filter来授权,在3.0之后,甚至还添加了一个AuthorizationMiddleware来做授权工作。

当然,在细节上,二者的实现还有很大的不同。比如Authorization并没有向Authentication那样,提供一个方法自动为用户注册自定义的AuthorizationHandler<TRequirement>(需要开发者手工注册)。这些具体的细节会后续几篇源码分析笔记中讲述。

PolicyRequirement

Requirement只是表达“要求”这个概念的一个空接口:

1
public interface IAuthorizationRequirement { }

一个授权Policy由多条要求组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AuthorizationPolicy
{
public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> authenticationSchemes)
{
if (requirements == null) { /* throw */ }
if (authenticationSchemes == null){ /* throw */ }
if (requirements.Count() == 0){ /* throw */ }

Requirements = new List<IAuthorizationRequirement>(requirements).AsReadOnly();
AuthenticationSchemes = new List<string>(authenticationSchemes).AsReadOnly();
}

public IReadOnlyList<IAuthorizationRequirement> Requirements { get; }

public IReadOnlyList<string> AuthenticationSchemes { get; }
}

Read More

Authentication — (5) 如何自定义认证处理器

设想有这样一个场景,我们开发了一个SaaS服务,正如微软在暴露Azure的某些服务那样,我们要求开发者提供相应的订阅(SubScriptionKey)才能访问我们的资源。我们约定,开发者需要在HTTP请求中添加如下形式的报头:

1
Authorization: subscription-key {KEY}

此外,服务提供商还会定期公布一些供新用户试用的订阅,使用这些试用订阅也能通过认证。

为了巩固前几篇源码分析笔记的相关知识,我们通过自定义一个新的认证处理器来解决这个问题。

自定义认证处理器

首先,我们新建一个类来表示与此认证相关的配置项:

1
2
3
4
5
public class SubsKeyAuthNSchemeOptions : AuthenticationSchemeOptions
{
public string SubscriptionKeyPrefix { get; set; } = "subscription-key";
public string TrialKey { get; set; } = "42 is the answer";
}

认证处理器需要首先从报头中提取Token(也即订阅的Key);然后判断当前Key是否为试用的订阅,然后从数据库中检索该Key是否有效;如果有效,则生成认证成功凭证、认证票据,最后返回认证成功结果。

下面给出认证处理器的完整实现:

Read More

Authentication — (4) Authentication服务的配置与构建

世界观

ASP.NET Core可以配置多种认证,让各个认证模式协同工作。这种配置主要体现在两个方面:

  1. 需要对DI容器中的 AuthenticationOptions 进行设置: 其中包含了每一种认证模式对应的<scheme>-<handlerType>的映射关系、以及默认的相关scheme名等信息。
  2. 需要把各个scheme对应的 handler 作为服务注册 到DI容器中。

AuthenticationBuilder

事实上,ASP.NET Core暴露了一个AuthenticationBuilder来帮助开发者对认证进行设置。由于认证除了简单地new一个AuthenticationBuilder实例之外,还需要注册一些其他服务(比如编码解码功能等),ASP.NET CoreIServiceCollection提供了一个AddAuthentication()扩展方法,封装了上述过程,调用后返回一个AuthenticationBuilder实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

services.AddAuthenticationCore();
services.AddDataProtection();
services.AddWebEncoders();
services.TryAddSingleton<ISystemClock, SystemClock>();
return new AuthenticationBuilder(services);
}

除了.AddAuthentication()这种核心方式,还有两种重载形式:

Read More

Authentication — (3.3) 认证处理器的实现之RemoteAuthenticationHandler

设想我们要写一个支持OAuth2.0的认证处理器,它支持使用GoogleMicrosoftFacebook等账号登陆。由于它们共用一套认证逻辑OAuth2.0,所以我们不希望为每一个网站都写一遍处理认证的方法,而是希望针对每个网站的一些特性部分进行简单填充。一个合理的方式是让处理认证的方法接受一个如何登陆的字符串(比如Google),然后通过认证服务去自动调用对应具体的认证处理器(比如GoogleHandler)。
更一般的,除了OAuth,OIDC也是一种常见的远程认证方式。

为了抽象这种利用远程服务器进行认证的方式,ASP.NET Core提供了RemoteAuthenticationHandler<TOptions>抽象类。该类继承自AuthenticationHandler<TOptions>抽象基类,并且实现了IAuthenticationRequestHandler接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class RemoteAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions>, IAuthenticationRequestHandler
where TOptions : RemoteAuthenticationOptions, new()
{
protected string SignInScheme => Options.SignInScheme;

protected new RemoteAuthenticationEvents Events
{
get { return (RemoteAuthenticationEvents)base.Events; }
set { base.Events = value; }
}

// ...
}

由于在继承自AuthenticationHandler<TOptions>的同时,还实现了IAuthenticationRequestHandler接口,这个RemoteAuthenticationHandler<TOptions>类就有两套处理认证的机制。一套是AuthenticationHandler<TOptions>的认证、质询、禁止等方法;另一套是IAuthenticationRequestHandler的接口方法HandleRequestAsync()用于直接对请求进行中间件级别的处理,并中断后续请求处理过程。

Read More

Authentication — (3.2) 认证处理器的实现之JwtBearerHandler

仅就认证处理器的工作机理而言,JwtBearer认证模式是最为简单的一种认证。所以,我们选择JwtBearer认证处理器作为本系列源码分析中关于认证处理器第一个具体实现的来讲述。

HandleAuthenticateAsync()的基本逻辑是:

  1. 触发接收到消息事件,事件处理程序通常可以设置新的token——这在使用WebSocket/SignalR认证中尤其有用,因为难以传递Authorization: Bearer {token}报头。事件处理程序甚至可以直接设置messageReceivedContext.Result来截断后续处理。
  2. 如果消息处理事件没有设置Token,则从Authorization: Bearer {jwt-token} 中获取
  3. 获取令牌校验参数
  4. 校验令牌,给出认证成功/失败结果

由于这部分相对简单,这里直接贴出相关源码(具体过程参见我的注释):

Read More