ASP.NET Core中的身份验证Authentication
1. Authentioncation vs Authorization身份验证和授权验证的区别
安全认证中有两个独立的基本面,一个是身份验证(Authentication),另一个是授权验证(Authorization)。它们的区别是:身份验证是判定你是谁的过程,而授权验证则是判定你被允许做什么的过程。显然,在判定一个用户被允许做什么之前,需要先判定该用户是谁。那么接下来,就来介绍下身份验证机制吧。
2. ASP.NET Core中的身份验证
为什么来说ASP.NET Core中的身份验证,而不是ASP.NET中的身份认证呢?一是因为.Net Core逐渐取代.Net Framework,成为流行的.Net生态技术平台,二是因为身份验证从ASP.NET到ASP.NET Core中是逐渐升级和平滑过渡的,其基础机制并没有显著改变。比如,在ASP.NET 4.x中,在HttpContext对象中有一个User属性(它的类型是IPrincipal,表征着某个请求request的当前用户)。在ASP.NET Core中有一个类似的属性User,差别在于其类型是ClaimsPrincipal(实现了IPrincipal接口)。
ASP.NET 4.x之前,授权验证通常都是Role-based,一个用户可能属于一个或多个角色,而且在应用程序的不同部分可能需要用户拥有特定的角色才能访问。在ASP.NET Core中仍然可以继续采用这种role-based授权验证,但这主要是为了保留向后兼容的原因。在ASP.NET Core中建议采用claims-based的方式。
3. Claims-based身份验证
基于声明的授权,这种概念可能初听起来令人困惑,但这种方式在实践中可能是最接近你想要的方式。你可以把声明(cliams)看作某特定身份(Identity)的一个属性,由名称和值组成。例如,你可以有一个生日声明,姓名声明,邮箱声明或VIP声明。注意这些声明只是关于这个身份是谁或是什么的说法,与它们能做什么无关。
这个Identity可以和多个claims关联起来,比如拿驾驶证来说,这个Identity就包含了很多项claims:姓名,出生年月,地址,以及可以驾驶何种类型的车辆等信息。你的护照也是一种包含多项声明的Identity。
让我们看下Identity在ASP.NET Core中的代码(具体类型为ClaimsIdentity),实际代码较为复杂,下面的代码是经过简化后的版本:
public class ClaimsIdentity: IIdentity { public string AuthenticationType { get; } public bool IsAuthenticated { get; } public IEnumerable<Claim> Claims { get; } public Claim FindFirst(string type) { /*...*/ } public Claim HasClaim(string type, string value) { /*...*/ } }
简化代码中显示了最重要的属性,即该Identity所拥有的所有Claims。针对Claims的操作由很多methods,上面代码中只列举了其中最常用的两个,当你做授权验证以及判断某特定Identity是否拥有一个特定claim时特别有用。
AuthenticationType属性可以从字面意思看出是何作用,在ASP.NET中这个字符串可能是Cookie,Bearer或Google等。IsAuthenticated这个属性表示一个Identity是否已经身份验证过了,这看起来有些冗余-怎么可能存在一个没有经过身份验证的Identity呢?这样一个场景就需要:当你想要guest访客未登录时,仍然能保存其在购物车中放入的物品。这样就需要一个未经身份验证的Identity,并且关联着若干claims。在ASP.NET Core中作为附加补充,如果你创建了一个ClaimsIdentity并且在构造函数中提供了AuthenticationType,那么IsAuthenticated属性就永远为真。反之同理的说法是,不会存在一个未经身份验证的用户拥有非空的AuthenticationType。
4. Multiple Identities
前面提过,HttpContext对象上的User属性的类型是ClaimsPrincipal,而不是ClaimsIdentity。所以我们来看下简化后的ClaimsPrincipal
public class ClaimsPrincipal :IPrincipal { public IIdentity Identity { get; } public IEnumerable<ClaimsIdentity> Identities { get; } public IEnumerable<Claim> Claims { get; } public bool IsInRole(string role) { /*...*/ } public Claim FindFirst(string type) { /*...*/ } public Claim HasClaim(string type, string value) { /*...*/ } }
其中重要的是Identities属性,其返回的是IEnumerable<ClaimsIdentity>。这就是说,一个ClaimsPricipal可以包含多个Identities。同时,类型中还存在一个Identity属性,它的目的是为了继承IPrincipal接口-在ASP.NET Core中它仅仅只选择Identities中的第一个Identity。
回到我们之前提到的护照和驾驶证的例子,多个identities实际是很有作用的-护照和驾驶证都是一纸identity,各自包含若干claims。在这个例子中,你就是当事人,你拥有这两份身份证明;因此你便继承了这两个Identity所有的Claims。
考虑另外一个实际的例子 - 你要乘坐飞机。首先,你会被服务人员问到你的姓名,你可以直接提供护照来证明你的姓名claim,因而你会拿到你的登机票,然后可以进行下一步。在安检门时,你被要求证明某个claim来声明你已经订了航班,这时,你需要另外一份identity即你刚刚拿到手的登机票,上面有航班号claim,所以你被允许继续下一步。最后,当你通过安检门后,你想要进入VIP贵宾室,并且被要求证明你是VIP会员且提供VIP号。这可能是VIP卡,或者其他方式的identity。如果你没有VIP卡,你就不能证明所要求的claim,你就会被拒绝进入。
重新梳理一下,这里面的关键点是一个principal可以拥有多个identities,这些identities可以拥有多个claims,然后ClaimsPrincipal继承了这些identites所拥有的claims。
正如前面所说,role-based授权验证更多的只是为了保留向后兼容的原因,所以IsInRole这个method在当你想用claims-based身份验证时就没有用处了,虽然其底层实现也是用到了claims(其中,claim的类型默认是RoleClaimType或ClaimType.Role)。
再次考虑到ASP.NET Core,多identities和claims可以在你的应用的不同部分中起到作用,就像在机场一样。例如,你可能用用户名和密码来登录,然后基于identity被授予一系列关联的claims,这些claims允许你在网站中浏览。但是如果说你在应用中有块特别敏感的区域,你想要特别保护起来;这就有可能要求你提供另外一个额外的Identity,关联着额外的claims;比如采用双因素认证或者要求你再次输入你的密码。这样来允许当前用户同时拥有多个identities,并且采纳所有identities的claims。
5. 创建principal
既然我们已经知道了在ASP.NET Core中principal时如何工作的,何不实际创建一个试试呢?
下面是一个简单的例子,
public async Task<IActionResult> Login(string returnUrl = null) { const string Issuer = "https://gov.uk"; var claims = new List<Claim> { new Claim(ClaimTypes.Name, "Andrew", ClaimValueTypes.String, Issuer), new Claim(ClaimTypes.Surname, "Lock", ClaimValueTypes.String, Issuer), new Claim(ClaimTypes.Country, "UK", ClaimValueTypes.String, Issuer), new Claim("ChildhoodHero", "Ronnie James Dio", ClaimValueTypes.String) }; var userIdentity = new ClaimsIdentity(claims, "Passport"); var userPrincipal = new ClaimsPrincipal(userIdentity); await HttpContext.Authentication.SignInAsync("Cookie", userPrincipal, new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(20), IsPersistent = false, AllowRefresh = false }); return RedirectToLocal(returnUrl); }
这个方法中实际上将claims硬编码了,实际中不会这么做,实际上你可能会从数据库或其他数据源中获取claim。代码中所做的首先就是创建一系列claims,每个都有自己的name,value,以及可选的Issuer和ClaimValueType。ClaimType类是一个帮助类,暴漏出一些常用的claim类型。它们是一些url,比如http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
,但是你没必要一定用url,正如最后添加的那个claim所示。一旦你创建完claims后,你就可以创建ClaimsIdentity,传入claims列表,并且指定AuthenticationType(为了确保你的identity的IsAuthenticated=true)。最终,使用此identity创建了一个ClaimsPricipal。在这个例子中,我们告知了AuthenticationManager去使用"Cookie"验证机制(前提是必须先在管道中配置好此中间件)
参考:
https://andrewlock.net/introduction-to-authentication-with-asp-net-core/