前言
这篇文章我想带领大家了解一下 ASP.NET Core 中如何进行的身份验证,在开始之前强烈建议还没看过我写的 Identity 系列文章的同学先看一下。
Identity 入门系列文章:
名词解释
做 Web 开发的都知道 HTTP 协议是无状态的,那么服务端如果想知道此次请求的用户是哪个登录的用户,那么就需要有一种标识每次都被传递到服务端,那么这个标识就是我们都知道的 Cookie(这里我们先不考虑header中携带标识的情况),服务端根据 Cookie 中携带的信息进行识别的一个过程就是身份验证,所有基于 WEB 的服务端都是如此,无关乎语言和框架。
在整个身份验证的过程中,又分为两个部分即认证和授权,很多同学区分不出来这两个东西,因为这两个单词看起来有点像,导致经常认错,这里我教大家一个小方法,就是记住他们的发音,使用某种方法让发音和汉字对应起来,这样就记住了。
Authentication [ɔ:,θenti'keiʃən]
认证
Authorization [,ɔ:θərai'zeiʃən, -ri'z-]
授权
分享一下我的方法,认证的拼音是(renzheng),其中 zheng 包含 en ,同样的 Authentication
也包含 en,这样我就记住了这个单词是认证,那么另外一个就是授权了。
认证:确定用户身份的一个过程。 注意是一个过程。
授权:确认用户可以做哪些事情,即权限。
基于 Claims 的身份
在 ASP.NET Core 中主要是使用的基于 Claims 的身份验证,也就是说将用户的属性都抽象成证件单元来表示了,通过证件单元来表示一张身份证。
我们先来回顾一下如何制造一张身份证:
//证件单元
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name,"奥巴马"),
new Claim(ClaimTypes.NameIdentifier,"身份证号")
};
//使用证件单元创建一张身份证
var identity = new ClaimsIdentity(claims, "AuthenticationTypeXXX");
注意,在 new ClaimsIdentity
的时候第二个参数是 AuthenticationType
,我在前面文章中讲过这个是 载体类型,也就是实体形式的身份证,对吧?
那么,在使用程序创建一个身份的时候,需要就指定这个载体了,在HTTP验证中,我们将载体设置为Cookies,代码如下:
var cookie身份证 = new ClaimsIdentity(claims, "Cookies");
有了Cookie身份证,我们还需要一个携带者,看过之前文章的可能知道,我讲 ClaimsPrincipal 的时候,一张身份证就不是代表一个人了,而是不通的身份种类,比如你可以同时是一名教师,母亲,商人。如果你想证明你同时有这几种身份的时候,你可能需要出示教师证,你孩子的出生证,法人代表的营业执照证。
所以,我们还需要制造一个人,这个人来携带各种证件,我们就携带上一步制造的 cookie身份证
吧,先携带这一个好了:
var 人 = new ClaimsPrincipal(cookie身份证)
我们来看一下完整的一个代码
//证件单元
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name,"奥巴马"),
new Claim(ClaimTypes.NameIdentifier,"身份证号")
};
//使用证件单元创建一张cookie身份证
var cookie身份证 = new ClaimsIdentity(claims, "Cookies");
//创建一个人携带cookie身份证
var 人 = new ClaimsPrincipal(cookie身份证)
多重身份
当一个人有多种身份的时候,这个时候可能有人会问,什么情况下会有多种身份呢?
举个简单的例子,上面的 cookie身份证
算是一种身份,那么我可能还有比如接入 OAuth的时候使用的 bearer身份证
,接入第三方登录时候使用过的 google身份证
,facebook身份证
,microsoft身份证
等等,这就叫多重身份。
多种身份种的每一种身份都有一个 AuthenticationType
对应一个认证方式,后面我会讲到。
以上,我们理清楚了一个重要的逻辑关系就是:
一个人有多种身份,每个身份都有证件单元和一个认证方式组成。
接下来,你们可能就会认为我就开始介绍认证和授权了。 不,很多东西有时候和你想象的并不一样,比如这篇文章也是,所以接下来我要讲的东西是 IdentityModel
IdentityModel
IdentityModel 是一种基于 Claim 的 Identity 库,它提供了一组类用来标识用户身份,以及对这些东西的抽象。
有些同学可能会问,不是已经有 ClaimsIdentity 来表示用户身份了吗?为啥又还有其他的表示用户身份的东西呢?
大哥,身份认证是一整套复杂的东西,包含很多组件,协议,标准,如果很简单就学会了我还用得着写文章教你吗? 还是接着介绍吧。
最初,IdentityModel 是属于 WIF(Windows Identity Foundation) 的一部分,WIF 是微软2004年给 .NET 平台搞的一套身份验证框架(包含Claims,Configuration,Metadata,Policy,Servicesd等等),微软想把这个东西作为 .NET 标准框架的一部分,所以它的命名空间是 System.IdentityModel, 了解这个东西的人不是很多,不过不知道也没关系,反正这玩意也已经被淘汰了。
在 .NET Core 中, WIF 这些套件只有 System.IdentityModel.Tokens.Jwt
被保留了下来,其他全被扔掉了,为什么呢?
原因是只有 JWT 这部分东西有用,其他的部分更多的是为以前的 Web Servics, WCF 那套分布式东西设计的,那套分布式的东西淘汰了,自然也不必要保留了。
在没有 .NET Core 的时候,我们想实现一套标准的单点登录(SSO)系统就可以利用 System.IdentityModel
因为它已经为我们做了大量工作,并且是标准化的。在 .NET Core 中也需要一些标准的抽象东西那怎么办呢?
微软弄了一套新的 IdentityModel
的库,命名空间为 Microsoft.IdentityModel
。很多人甚至都找不到它的源码在哪里,我一开始也没找到,最后发现在 https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet 这个仓库里面。
这个库的组成部分同样都是抽象的部分,包括相关的协议对象,票据的加解密,票据存储 等等,也就是说微软给 .NET Core 的身份验证体系又定义了一套抽象的东西,任何第三方基于身份验证的实现库或者框架都要遵循(依赖)他们。
以上的关于 IdentityModel
的介绍和下面我要将的东西关系不是很大,之所以要在这里引入是因为我要为后续的文章做铺垫,在这里引入最合适不过。
接下来我们继续讲解,就开始了认证部分的讲解。
Authentication 认证
我之前讲过奥巴马去杭州旅游的故事,有些同学反映还是看不懂,所以我决定这次配合 ASP.NET Core 中 Cookie 身份认证的过程来讲解。
再次声明,如果你还没看过 Identity 入门一 这篇文章,我要求你先跳过去看一下,因为接下来的内容是这篇文章的延申。
我们假设你现在已经知道了人和身份证,然后现在人使用身份证是坐火车。
人
就是奥巴马
身份证就是 cookie身份证
我们将开始我们的认证旅程,同时结合我们最熟悉的 HTTP 登录流程。
奥马巴要去乘坐火车,那么现在他要过安检,在Web登录中就是对应的登录,登录要使用用户名密码,但是用户名密码是属于业务逻辑方面的验证,我们不考虑,因为假设是第三方登录就不需要输入用户名和密码了,所以你可以理解为我们假设用户名和密码都正确,现在奥马巴要过安检了。
对应的代码为:
//证件单元
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name,"奥巴马"),
new Claim(ClaimTypes.NameIdentifier,"身份证号")
};
//使用证件单元创建一张身份证
var identity = new ClaimsIdentity(claims,"Cookies");
//使用身份证创建一个证件当事人,也就是奥巴马
var identityPrincipal = new ClaimsPrincipal(identity);
//奥巴马开始过安检
await HttpContext.SignInAsync("Cookies", identityPrincipal);
现在,我们来运行程序,看看会发生什么。你先不用管 HttpContext.SignInAsync
是做什么用的,下面会说。
新建一个ASP.NET Core 空的 MVC 程序,然后在登录的 Action 方法中粘贴以上代码,然后按 F5 运行。
出错了,根据错误信息我们可以看出是因为我们没有注册身份验证的中间件,而且错误已经告诉了我们应该怎么做,我们尝试解决这个错误。
在 Startup.cs
文件中 ConfigureServices
方法注册服务
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication("Cookies")
.AddCookie("Cookies");
...
}
注意,AddAuthentication
这里是指定默认的认证载体类型,AddCookie
这里是注册载体类型的处理程序。
认证部分我会在下一篇中详细介绍,所以这里先大致了解下。
再次 F5 运行发现已经正常了。
我们打开浏览器的 Cookie 查看一下,可以看到多了一项 Cookie 记录
我们可以看到这个 Cookie 的 Name 为 .AdpNetCore.Cookie
,Value 为一大长串加密的字符串。
流程讲解
现在我来开始讲 HttpContext.SignIn
。
它是一个扩展方法,最终是调用的 IAuthenticationService
接口的 SignInAsync
方法。我们来看下接口的定义:
public interface IAuthenticationService
{
Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
}
有了接口,肯定有实现咯。 我们找一下实现在哪里,很容易,根据 ASP.NET Core 的 IOC 来找就行了,很明显在 AddCookie
这个扩展里面。
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication()
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
↑↑↑ 实现就在这里
...
}
我们找到了处理类 CookieAuthenticationHandler
这个对象,我们再来看具体的代码。
protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
// Process the request cookie to initialize members like _sessionKey.
await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions();
var signInContext = new CookieSigningInContext(
Context,
Scheme,
Options,
user,
properties,
cookieOptions);
await Events.SigningIn(signInContext);
var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
_sessionKey = await Options.SessionStore.StoreAsync(ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
signInContext.CookieOptions);
var signedInContext = new CookieSignedInContext(
Context,
Scheme,
signInContext.Principal,
signInContext.Properties,
Options);
await Events.SignedIn(signedInContext);
// Only redirect on the login path
var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
await ApplyHeaders(shouldRedirect, signedInContext.Properties);
Logger.SignedIn(Scheme.Name);
}
大概步骤分为:
1、创建一个SignIn Cookie 上下文对象
2、将上下文对象转换为票据(Ticket),转换为票据的目的是为了加密
3、将票据进行加密
4、将加密后的票据写入Cookie
很有意思的是第三步,我需要展开来说下,这也结束。
在第三步加密票据的过程中可以看到有一个 if 判断 if (Options.SessionStore != null)
,是做什么用的呢?
可能有些同学会有疑问,我们基于Claim的Cookie存储假如我的证件单元很多,就会生成一个非常大的cookie,每次传输是有性能影响的,并且Cookie是有最大限制的,怎么办呢?
其实解决办法就是我们就可以开启这个 SessionStore
,将Cookie存储在服务端例如Redis等缓存中。代码如下:
services.AddSingleton<ITicketStore, MyRedisTicketStore>();
services.AddOptions<CookieAuthenticationOptions>("Cookies")
.Configure<ITicketStore>((o, t) => o.SessionStore = t);
现在,浏览器中已经存储了用户的身份啦。
以上就是确认用户身份的一个过程,在这个过程中我们使用Cookie来标记用户身份并且存储到浏览器的Cookie了,这个过程就是 认证。
其实上面就是 ASP.NET Core 中的 Forms 身份验证中的认证阶段。
扩展阅读
在不使用Cookie的时候怎么确定身份呢? 比如在 WEB API 接口中使用的就是 Access Token,这也相当于Cookie中的票据了,那么在 WEB API 中如何确定身份,流程又是怎么样的呢?可以看后续文章。
总结
才把认证写完发现已经这么长了,下篇再来讲讲授权吧。
如果你对 .NET Core 有兴趣的话可以关注我,我会定期的在博客分享我的学习心得。
本文地址:http://www.cnblogs.com/savorboard/p/authentication.html
作者博客:Savorboard
本文原创授权为:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本