前面有说到UserAuthentication()UserAuthorization(),这两个的差别在于:前者用于验证登录者是谁,后者则决定登录者可以做什么。

举例来说,一个员工要登录员工系统,他必须输入帐号(如员工ID、姓名或是email)、密码,系统才能知道是谁登录了,这就是Authentication(验证),处理Authentication 的方式有CookieToken第三方验证(OAuth 或API-token)OpenIdSAML

而当员工登录系统后,一般员工通常不会有跨部门或是管理权限,他只能看到他自己或所属部门的信息,例如生产部员工看不到会计部的财务,不过会计部为了计算成本却看得到生产部的原料价格,这就是Authorization (授权)

在决定登录者可以做什么前,必须先知道登录者是谁,所以UserAuthentication()必须放在UserAuthorization()前面。

ASP.NET Core Identity 使用的是基于Claim 的验证,要了解Claim,必须先了解ClaimClaimsIdentityClaimsPrincipal 是什么。

Claim 就是关于使用者的一些信息,Claim TypeClaim Value(可以不用给)就组成一个ClaimClaim 可以是姓名电话角色Email甚至是角色等等。Authorization 就是用Claim 判断使用者有无授权。

new System.Security.Claims.Claim(ClaimTypes.Role, role.Name)

ClaimsIdentity 则是多个Claim 的集合,像是驾照上面记录了姓名生日电话驾照就是一个ClaimsIdentity

ClaimsPrincipal 是多个ClaimsIdentity 的集合,台湾人都有身分证跟健保卡,有些人还有驾照,身分证、健保卡跟驾照都是ClaimsIdentity,持有它们的人就是ClaimsPrincipal

而每一个HTTP request 都会产生HttpContext 对象,该对象就存有目前request 的信息,其下可以找到一个类型为ClaimsPrincipal 的Property 名为User,这个Property 就是由UseAuthentication()引入的Authentication Middleware 产生的。

登录机制可以用Cookie 或是JWT 实现,但Authentication Middleware 怎么知道要用哪个方法产生User Property?那就要看Authentication Scheme 跟Authentication Handlers。

Authentication Handlers

Authentication Handlers 就是处理验证的方式,ASP.NET Core Identity 可以调用AuthenticateAsync()API 去验证使用者已登录,如验证失败就调用ChallengeAsync()将使用者导回登录页面,如授权失败则用ForbidAsync()禁止使用者访问,当然也可以自己实现这些行为。下面例子中用了JWTCookie 的验证方式,如果用了前者,就必须验证JWT token 并产生ClaimsPrincipal 回传到HttpContext.User 中;使用后者则会检查当前requestcookie并产生ClaimsPrincipal

builder.Services.AddAuthentication()
	.AddJwtBearer()
	.AddCookie();

Authentication Scheme

用了任何一种方式注册Authentication Handlers 就称为Authentication Scheme,每个Authentication Scheme 都有一个独特的名字以识别,且可以自己设定Authentication Handlers,下面的程序结果跟上面会是一样,因为它们都有预设的Scheme Name。

builder.Services.AddAuthentication()
    .AddJwtBearer("Bearer")
    .AddCookie("Cookies");

Blazor Authentication

Blazor 用的验证方式跟ASP.NET Core 一样,不过Blazor WebAssemblyBlazor Server 又有不同,前者的验证就像任何前端网站一样可以被绕过,因为使用者一端的程序可以被使用者改动,因此发送数据的API 端一定也需要验证;后者则可用内建的AuthenticationStateProvider 取得前面说的HttpContext.User,笔者此前就是自己继承并重写这项Service 实现JWT 验证的。

AuthenticationStateProvider 就是昨天说到的<AuthorizeView><CascadingAuthenticationState>可以取得当前验证状态的原因,但如果没有要重写预设验证机制的话,建议不要自己在Component 注入一个AuthenticationStateProvider 出来,直接使用的缺点很明显,若当前request 的验证状态有改动,因为你改动了这个Component 的验证机制,该Component 就不会被告知。

可以看到下图,ApiAuthenticationStateProvider继承了AuthenticationStateProvider,并重写了Task<AuthenticationState>,这个Property 可以取得当前的验证状态,MarkUserAsAuthenticated()MarkUserAsLoggedOut()则是笔者自己写的方法用以标示使用者通过验证及注销系统,NotifyAuthenticationStateChanged()顾名思义会通知各个Component 当前验证状态,这就是自己继承并重写的案例。

下图则是在Component 取得当前requestHttpContext.UserClaims 的作法,不过这里是先利用服务取得User 再取得其下Claims,其实是多此一举了。

如果只是要取得HttpContext.User,只要如下图般就可以了,因为 Task<AuthenticationState>会以[CascadingParameter]的方式层层传递下去。

引用:

  1. Introduction to Authentication in ASP.NET Core
  2. ASP.NET Core Blazor authentication and authorization

注:本文代码通过 .NET 6 + Visual Studio 2022重构,可点击原文链接与重构后代码比较学习,谢谢阅读,支持原作者