ASPNET-ASPNETCORE Cookie认证(非常重要)

ASPNET-ASPNETCORE Cookie认证

发布时间: 2019-06-09 02:46:39
一个小插曲

前几天去面了一家公司,跟面试官话不投机,问题犀利,态度傲慢。比如一上场就问你用过IOC框架吗?其实简历上面也有写Autofac、Unity等,我就说了几个Autofac、Unit、CastleWindsor而且自己参考Autofac也实现过轻量级简单的容器,第二句就是你真的用过IOC框架,Ninject在NET平台这么主流你都没用过?比如还有什么是webapi的基类?很多此类问题,后面我也蒙圈了。闲话就说到这吧,没有其他意思,开心就好。下面我们一起看看,ASPNET-ASPNETCORE平台上面的WEB框架有哪些主流的认证方式,以及底层实现。

话题背景

关于认证我的个人理解是,验证信息的合法性。在我们生活当中,比如门禁,你想进入一个有相对安全措施的小区或者大楼,你需要向保安或者门禁系统提供你的身份信息证明,只有确定你是小区业主,才可以进来,我这只是打个比方啊,不要纠结。对于我们计算机的安全领域,认证其实也非常类似,windows系统登陆就是一个很好的例子。今天我们主要学习的是ASPNET以及ASPNETCORE平台上面一些主流的认证方式。

正式话题-认证

我最开始接触NET平台的WEB框架是从APSNETWEBFORM开始->ASPNETMVC->ASPNETMVCCORE,下面我们就从WEBFORM开始吧(包括MVC1.x-4.x)。在MVC5之前,我们常用的认证方式有,Forms、Windows、Passport、None这三种认证方式,严格意义上说是三种,None为不认证,而在这三种认证方式当中,我们最常用的就是Forms表单认证,下面我们一起来看看Forms表单认证的实现原理。

Forms表单认证

我会以我自己的使用方式介绍再到实现原理。整个Forms认证的实现逻辑大概是,说到Forms认证我们就不得不说ASPNET处理管道,为什么这么说呢?因为ASPNET的很多基础功能都是通过相应的HttpModule实现的,比如认证、授权、缓存、Session等等。ASPNET平台的Forms认证就是基于FormsAuthenticationModule模块实现,相应的Windows认证也是一样,由WindowsAuthenticationModule实现。对于Forms认证方式登录而言。

1.匹配用户名&密码是否正确。
2.构建FormsAuthenticationTicket对象。

3.通过FormsAuthentication.Encrypt方法加密Ticker信息。
4.基于加密Ticker信息,构建HttpCookie对象。
5.写入Response,输出到客户端。
以上就是我们基于Forms表单认证方式的登录实现逻辑,下面我们来梳理一下认证的大概实现逻辑,针对每次请求而言。
1.在ASPNET管道生命周期里,认证模块FormsAuthenticationModule会接管并读取Cookie。
2.解密Cookie获取FormsAuthenticationTicket对象并且验证是否过期。
3.根据FormsAuthenticationTicket对象构造FormsIdentity对象并设置HttpContext.User。
4.完成认证。
下面我们一起看看Forms认证的具体实现,我会以我自己开发过程中使用的方式加以介绍。首先我们会在web.config文件里面定义authentication配置节点,如下。

1 <authentication mode="Forms">
2       <forms name="AUTH" loginUrl="~/login" protection="All" timeout="43200" path="/" requireSSL="false" slidingExpiration="true" />
3     </authentication>

mode属性对应了4属性值,除Forms以外还有上面我提到的三种方式。其他三种由于篇幅问题,在这里不做介绍。这些属性我相信大家应该都比较熟悉。下面我们看看关于Forms认证具体的后台代码。看代码。

 1 public virtual void SignIn(User user, // 这个user是你校验合法性之后的这么一个用户标识对象
 2 bool createPersistentCookie)
 3         {
 4             var now = DateTime.UtcNow.ToLocalTime();
 5             // 构建Ticker对象
 6             var ticket = new FormsAuthenticationTicket(
 7                 1 ,
 8                user.Username,
 9                 now,
10                 now.Add(_expirationTimeSpan),
11                 createPersistentCookie,
12                 user.Username,
13                 FormsAuthentication.FormsCookiePath);
14             // 加密ticker对象
15             var encryptedTicket = FormsAuthentication.Encrypt(ticket);
16             // 通过加密ticker对象构建HttpCookie对象
17             var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
18             cookie.HttpOnly = true;
19             if (ticket.IsPersistent)
20             {
21                 cookie.Expires = ticket.Expiration;
22             }
23             cookie.Secure = FormsAuthentication.RequireSSL;
24             cookie.Path = FormsAuthentication.FormsCookiePath;
25             if (FormsAuthentication.CookieDomain != null)
26             {
27                 cookie.Domain = FormsAuthentication.CookieDomain;
28             }
29             // 写入输出流Response
30             _httpContext.Response.Cookies.Add(cookie);
31         }

以上代码就完成了我们的Forms认证所需的Cookie信息,可能有些朋友在以往开发WebForms到4.x最常用的使用方式是FormsAuthentication.SetAuthCookie(user.UserName, true),其实SetAuthCookie里面的实现逻辑跟上面实现大同小异,只是我比较喜欢手动创建可以更多的控制一些辅助信息而已。在以上代码片段中,我着重想介绍一下FormsAuthentication.Encrypt(ticket)加密方法,因为它涉及到了Forms认证的安全机制,也好让各位朋友大概了解Forms认证到底安全不安全。FormsAuthentication该对象位于System.Web.Security名称空间下面,主要作用是安全相关辅助工具类,比如加解密等。
1.在默认情况下,ASPNETFORMS认证模块针对Ticker的加密Key是由ASPNET随机生成,并存储在本地安全机构LSA中。我们可以通过一下代码片段验证这一逻辑。

 1 private CryptographicKey GenerateCryptographicKey(string configAttributeName, string configAttributeValue, int autogenKeyOffset, int autogenKeyCount, string errorResourceString)
 2     {
 3        // 其他代码
 4       bool flag1 = false;
 5       bool flag2 = false;
 6       bool flag3 = false;
 7       if (configAttributeValue != null)
 8       {
 9         string str1 = configAttributeValue;
10         char[] chArray = new char[1]{ ',' };
11         foreach (string str2 in str1.Split(chArray))
12         {
13           if (!(str2 == "AutoGenerate"))
14           {
15             if (!(str2 == "IsolateApps"))
16             {
17               if (!(str2 == "IsolateByAppId"))
18               flag3 = true;
19             }
20             else
21               flag2 = true;
22           }
23           else
24             flag1 = true;
25         }
26       }
27       if (flag2)
28         MachineKeyMasterKeyProvider.AddSpecificPurposeString((IList<string>) stringList, "IsolateApps", this.ApplicationName);
29       if (flag3)
30         MachineKeyMasterKeyProvider.AddSpecificPurposeString((IList<string>) stringList, "IsolateByAppId", this.ApplicationId);
31     }

以上代码片段逻辑也比较简单,自己体会吧。
2.手动指定machineKey配置节点,该配置节在web.config文件里面,其中包括可支持的加密算法,加密算法支持DES,3DES,AES等。具体代码我就不贴了,我们跟踪其实现原理意在了解Forms认证其安全性。
3.通过以上两点介绍,我个人认为Forms认证相对来说很安全。
Forms认证
下面我们看看Forms的实现原理。
ASPNET的Forms认证发生在ASPNET管道的FormsAuthenticationModule对象里面,在该对象里面的Init方法里面绑定了认证事件OnEnter,具体的认证实现是OnEnter里面调用的OnAuthenticate方法。我们来看下代码。

 1 private void OnAuthenticate(FormsAuthenticationEventArgs e)
 2     {
 3          // 其他代码
 4         bool cookielessTicket = false;
 5         // 从请求cookie里面抽取ticker票据信息
 6         FormsAuthenticationTicket ticketFromCookie = FormsAuthenticationModule.ExtractTicketFromCookie(e.Context, FormsAuthentication.FormsCookieName, out cookielessTicket);
 7         // 过期或者为null直接返回
 8         if (ticketFromCookie == null || ticketFromCookie.Expired)
 9           return;
10         FormsAuthenticationTicket ticket = ticketFromCookie;
11         // 如果启用滑动过期,更新过期时间
12         if (FormsAuthentication.SlidingExpiration)
13           ticket = FormsAuthentication.RenewTicketIfOld(ticketFromCookie);
14         e.Context.SetPrincipalNoDemand((IPrincipal) new GenericPrincipal((IIdentity) new FormsIdentity(ticket), new string[0]));
15         if (!cookielessTicket && !ticket.CookiePath.Equals("/"))
16         {
17           cookie = e.Context.Request.Cookies[FormsAuthentication.FormsCookieName];
18           if (cookie != null)
19             cookie.Path = ticket.CookiePath;
20         }
21         if (ticket == ticketFromCookie)
22           return;
23         if (cookielessTicket && ticket.CookiePath != "/" && ticket.CookiePath.Length > 1)
24           ticket = FormsAuthenticationTicket.FromUtc(ticket.Version, ticket.Name, ticket.IssueDateUtc, ticket.ExpirationUtc, ticket.IsPersistent, ticket.UserData, "/");
25         string cookieValue = FormsAuthentication.Encrypt(ticket, !cookielessTicket);
26         
27         if (cookielessTicket)
28         {
29           e.Context.CookielessHelper.SetCookieValue('F', cookieValue);
30           e.Context.Response.Redirect(e.Context.Request.RawUrl);
31         }
32         else
33         {
34           if (cookie != null)
35             cookie = e.Context.Request.Cookies[FormsAuthentication.FormsCookieName];
36           if (cookie == null)
37           {
38             cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue);
39             cookie.Path = ticket.CookiePath;
40           }
41           if (ticket.IsPersistent)
42             cookie.Expires = ticket.Expiration;
43           cookie.Value = cookieValue;
44           cookie.Secure = FormsAuthentication.RequireSSL;
45           cookie.HttpOnly = true;
46           if (FormsAuthentication.CookieDomain != null)
47             cookie.Domain = FormsAuthentication.CookieDomain;
48           cookie.SameSite = FormsAuthentication.CookieSameSite;
49           e.Context.Response.Cookies.Remove(cookie.Name);
50           e.Context.Response.Cookies.Add(cookie);
51         }
52     } 

以上代码片段反映了Forms认证具体逻辑,逻辑比较简单,我也大概做了一些注释,以上就是ASPNET在MVC5.x之前ASPNETForms认证的实现。接下来我们对ASPNET5.X之前的版本基于Forms认证做个简单的总结。
1.用户在未登录的情况下,访问我们受保护的资源。
2.FormsAuthenticationModule模块验证用户的合法性,主要是生成Identity对象和设置IsAuthenticated属性。
3.如果未登录则endrequest阶段跳转到web.config配置的登录页或者硬编码指定的登录页。
4.用户登录。
5.匹配用户名&密码,如果合法,生成ticker票据和cookie并写入response。
6.访问受保护的资源(授权部分)。
7.FormsAuthenticationModule模块验证用户的合法性。
8.如果为以认证用户IsAuthenticated=true,授权访问相应的资源。
后续的每次请求也是6,7,8循环。
针对Forms认证就此告一段落,下面我们接着介绍MVC5的常规认证方式。
MVC5Cookies认证方式
为什么我要把MVC5的认证方式单独做一个小结讲解呢?它有什么特别之处吗?没错,ASPNETMVC5引入了新的设计理念OWin,我个人的理解是,解耦webserver容器IIS和模块化。同时NET4.5也引入了ASPNET.Identity,Identity主要是提供帮助我们管理用户、角色以及存储,当然Identity相较Membership强大多了。对于OWin和Identity我在这里不做详细介绍,自己可以去搜一些帖子看或者查看官方文档。OWin在WebServers与ASPNETWebApplication之间定义了一套标准接口,其官方的开源实现是Katana这个开源项目,我们今天要介绍的MVC5的认证就是基于Katana这个开源项目的CookieAuthenticationMiddleware中间件实现的,在介绍CookieAuthenticationMiddleware中间件之前,我想简单罗列一下MVC5的cookies认证(你也可以认为是Katana实现的新的Forms认证)和我们早期使用的Forms认证做个简单的对比。
相同点:1.基于cookie认证 2.支持滑动过期策略 3.实现令牌保护 4.重定向。
不同点:Identity结合Owin实现了声明认证Claims-based。
以上是个人的一点理解,下面我们具体看看认证中间件的实现,CookieAuthenticationMiddleware的定义。

 1 public class CookieAuthenticationMiddleware : AuthenticationMiddleware<CookieAuthenticationOptions>
 2     {
 3         // 其他成员
 4         public CookieAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, CookieAuthenticationOptions options)
 5             : base(next, options)
 6         {
 7         }
 8         // 创建具体的AuthenticationHandler
 9         protected override AuthenticationHandler<CookieAuthenticationOptions> CreateHandler()
10         {
11             return new CookieAuthenticationHandler(_logger);
12         }
13     }

CookieAuthenticationMiddleware里面就一个方法成员,通过CreateHandler方法创建了具体的CookieAuthenticationHandler对象,我们的认证核心实现就发生在这个Handler里面。接下来我们看看CookieAuthenticationHandler对象的定义。

 1 internal class CookieAuthenticationHandler : AuthenticationHandler<CookieAuthenticationOptions>
 2     {
 3         // 其他成员
 4         private const string HeaderNameCacheControl = "Cache-Control";
 5         private const string HeaderNamePragma = "Pragma";
 6         private const string HeaderNameExpires = "Expires";
 7         private const string HeaderValueNoCache = "no-cache";
 8         private const string HeaderValueMinusOne = "-1";
 9         private const string SessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
10 
11         private bool _shouldRenew;
12         private DateTimeOffset _renewIssuedUtc;
13         private DateTimeOffset _renewExpiresUtc;
14         private string _sessionKey;
15         
16         protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
17         
18         protected override async Task ApplyResponseGrantAsync()
19         
20         protected override Task ApplyResponseChallengeAsync()
21     }

从CookieAuthenticationHandler对象的定义来看,其实也能看出一二,主要是针对cookie的相关操作,在该对象成员里面我们需要了解一下其中的三个方法。
1.AuthenticateCoreAsync,代码我就不贴了,有兴趣的朋友可以自己查看Katana开源项目的源代码。该方法内部大概实现思路是:从IOWinContext对象获取cookie,如果对owin不怎么熟悉的话,这个context你可以把它理解为我们之前熟悉的HttpContext,然后通过解密出来的cookie字符串构造ClaimsIdentity对象并添加到OwinContext对象Request.User,最后返回AuthenticationTicket对象,该对象包装的就是当前用户信息以及相关辅助信息。
2.ApplyResponseGrantAsync,设置、更新或者删除cookie并写入response。
3.ApplyResponseChallengeAsync,授权失败,发生重定向。

 1 public class AuthenticationTicket
 2     {
 3         public AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties properties)
 4         {
 5             Identity = identity;
 6             Properties = properties ?? new AuthenticationProperties();
 7         }
 8         // 用户信息
 9         public ClaimsIdentity Identity { get; private set; }
10         // 辅助信息,比如会话、过期等
11         public AuthenticationProperties Properties { get; private set; }
12     }

下面我们一起看看在我们开发过程中的应用以及内部实现
Startup是Katana开源项目引入的一种新的模块初始化方式,其实也没什么特别的,就是相关中间件的注册以及一些默认上下文对象的初始化操作。下面我们具体看代码,我们的MVC5新的认证方式在Startup里面如何注册的。

 1 public partial class Startup
 2     {
 3         public void ConfigureAuth(IAppBuilder app)
 4         {
 5             // 其他代码
 6             app.UseCookieAuthentication(new CookieAuthenticationOptions
 7             {
 8                 AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
 9                 LoginPath = new PathString("/Account/Login"),
10                 Provider = new CookieAuthenticationProvider
11                 { }
12             });           
13         }
14     }

注册逻辑很简单,通过IAppBuilder的扩展方法UseCookieAuthentication实现,接下来我们看看UseCookieAuthentication扩展方法的内部实现。

 1 public static IAppBuilder UseCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options, PipelineStage stage)
 2         {
 3             if (app == null)
 4             {  }
 5             // 注册
 6             app.Use(typeof(CookieAuthenticationMiddleware), app, options);
 7             // 加入owin管道
 8             app.UseStageMarker(stage);
 9             return app;
10         }

整个注册逻辑就这么几行代码,相关方法都有注释。最后在程序初始化过程中通过Build方法完成Owin管道所有中间件的初始化工作。接下来我们看看具体的登录实现。

 1 public async Task<ActionResult> Login(LoginModel model,string returnUrl)
 2 {
 3     // 其他代码
 4     if (ModelState.IsValid)
 5     {
 6         AppUser user = await UserManager.FindAsync(model.Name, model.Password);
 7         if (user==null)
 8         {
 9             ModelState.AddModelError("","无效的用户名或密码");
10         }
11         else
12         {
13             var claimsIdentity =
14                 await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
15             AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity);
16             return Redirect(returnUrl);
17         }
18     }
19  
20     return View(model);
21 }

通过以上代码片段就完成了我们系统的登录操作,在以上Login方法里面,我们需要注意这么几个方法。
1.FindAsync主要是通过Identity实现用户名和密码的匹配。
2.CreateIdentityAsync主要是创建ClaimsIdentity对象,该对象后续会写入cookie。
3.SignIn包装CreateIdentity方法创建的ClaimsIdentity以及ClaimsPrincipal对象,为cookie写入Response提供相关认证信息,只有在设置cookie阶段才会写入response。
接下来我们针对Katana里面的cookie认证做个简单的总结。
1.用户在未登录的情况下,访问我们受保护的资源。
2.CookieAuthenticationMiddleware中间件验证用户的合法性。
3.用户登录。
4.匹配用户名&密码,如果合法,包装相关认证信息。
5.创建\更新cookie写入response。
6.访问受保护的资源。
7.CookieAuthenticationMiddleware中间件解密cookie验证用户认证信息。
8.如果为以认证用户,授权访问相应的资源。
后续的每次请求也是6,7,8循环。
以上MVC5新的Cookies认证方式就此告一段落,下面我们接着介绍ASPNET.Identity三方认证。
三方认证
在我们介绍三方认证之前,我们不妨先来了解一下什么是Claim,大家把它翻译成声明,我也就这么跟着叫把。Claim所描述的是一个用户单一的某个信息,比如用户名,只有多个Claim组合才能描述一个完整的用户ClaimsIdentity对象。个人理解这是一种通用的信息存储结构,一种规范,可以很方便的基于用户数据信息驱动认证和授权并且提供独立服务,各自都不需要关心自己的实现。在我们传统的认证windows或者forms认证方式中,每个系统都有自己认证方式、授权和用户数据信息,如果是几年以前,可能没有什么问题,但是在如今飞速发展的互联网时代,就显的有很大的局限性、扩展性以及安全性。接下来我们要介绍的就是MVC5基于ASPNET.Identity结合Katana实现的三方认证,也就是我们上面说的基于Claims-based实现第三方认证,这里我们以google认证为例,由于网络问题这里我们以木宛城主大拿的实例代码做示例,我会结合实例代码分析内部实现。
首先我们需要添加google服务认证中间件。

1 app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
2 {
3      // 以下为客户端凭据,可以通过google认证服务注册
4     ClientId = "",
5     ClientSecret = "",
6 });

其二我们需要设计实现登录逻辑,通常情况下我们在登录论坛的时候,旁边可能会有基于QQ登录或者别的三方认证提供商。

 1 public ActionResult GoogleLogin(string returnUrl)
 2 {    // 创建AuthenticationProperties对象,我们可以理解为认证复制信息字典
 3     var properties = new AuthenticationProperties
 4     {    
 5         RedirectUri = Url.Action("GoogleLoginCallback",
 6         new { returnUrl = returnUrl })
 7     };
 8     // 初始化google认证相关辅助信息
 9     HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");
10     // 返回401
11     return new HttpUnauthorizedResult();
12 }

以上代码比较简单,我也做了相应的注释,其逻辑是初始化google认证的一些辅助信息,然后返回401状态码,继而重定向到google登录页。下面我们看看登录成功之后的代码逻辑。

 1 public async Task<ActionResult> GoogleLoginCallback(string returnUrl)
 2 {
 3     // 从google认证服务获取claims
 4     ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();
 5     // 检查该用户是否首次登录系统
 6     AppUser user = await UserManager.FindAsync(loginInfo.Login);
 7     if (user == null)
 8     {
 9         user = new AppUser
10         {
11             Email = loginInfo.Email,
12             UserName = loginInfo.DefaultUserName,
13             City = Cities.Shanghai,
14             Country = Countries.China
15         };
16         // 持久化用户数据
17         IdentityResult result = await UserManager.CreateAsync(user);
18         // 缓存
19         result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
20     }
21     ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
22         DefaultAuthenticationTypes.ApplicationCookie);
23     ident.AddClaims(loginInfo.ExternalIdentity.Claims);
24     // 创建用户ClaimsIdentity对象
25     AuthManager.SignIn(new AuthenticationProperties
26     {
27         IsPersistent = false
28     }, ident);
29     return Redirect(returnUrl ?? "/");
30 }

以上就是三方认证的实现方式,下面我们通过Katana源码看看三方认证的实现原理。通过上面Katana的cookies认证,我们了解到认证中间件的认证逻辑是实现在相应的AuthenticationHandler里面,我们同样以google为例,去看看内部的实现。下面我们一起来上面注册的认证中间件GoogleOAuth2AuthenticationMiddleware的定义。

 1 public class GoogleOAuth2AuthenticationMiddleware : AuthenticationMiddleware<GoogleOAuth2AuthenticationOptions>
 2     {
 3         // 其他成员
 4         public GoogleOAuth2AuthenticationMiddleware(
 5             OwinMiddleware next,
 6             IAppBuilder app,
 7             GoogleOAuth2AuthenticationOptions options)
 8             : base(next, options);
 9         // 构建认证handler
10         protected override AuthenticationHandler<GoogleOAuth2AuthenticationOptions> CreateHandler()
11         {
12             return new GoogleOAuth2AuthenticationHandler(_httpClient, _logger);
13         }
14         // 构建httpclienthandler
15         private static HttpMessageHandler ResolveHttpMessageHandler(GoogleOAuth2AuthenticationOptions options);
16     }

根据以上代码片段我们了解到,GoogleOAuth2AuthenticationMiddleware中间件似乎比我们常规的cookies认证多了一个方法ResolveHttpMessageHandler,其实这个方法没有别的套路,就是辅助创建httpclient对象,完成http请求而已,在handler的认证逻辑里面需要获取googletoken,就是通过它来获取的。
第二个方法CreateHandler返回的GoogleOAuth2AuthenticationHandler对象就是我们接下来要重点讨论的对象。

 1 internal class GoogleOAuth2AuthenticationHandler : AuthenticationHandler<GoogleOAuth2AuthenticationOptions>
 2     {
 3         private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
 4         private const string UserInfoEndpoint = "https://www.googleapis.com/plus/v1/people/me";
 5         private const string AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth";
 6 
 7         private readonly ILogger _logger;
 8         private readonly HttpClient _httpClient;
 9 
10         public GoogleOAuth2AuthenticationHandler(HttpClient httpClient, ILogger logger)
11         {
12             _httpClient = httpClient;
13             _logger = logger;
14         }
15         // 通过httpclient访问google认证服务器获取token,根据token数据包装Claim
16         protected override async Task<AuthenticationTicket> AuthenticateCoreAsync();
17         // 如果未认证,401授权失败发生重定向
18         protected override Task ApplyResponseChallengeAsync();
19 
20         public override async Task<bool> InvokeAsync()
21         {
22             return await InvokeReplyPathAsync();
23         }
24         // 调用signin,保存用户信息  
25         private async Task<bool> InvokeReplyPathAsync();
26     }

代码比较长,我把具体实现删掉了,实现逻辑我注释到了方法上面,有兴趣的朋友可以自己多看看源码。以上就是NET平台上面一些主流的认证方式和实现原理。接下来我们继续介绍ASPNETCORE的认证。

ASPNETCORE认证
熟悉微软web平台认证授权体系的朋友应该知道,不管是早期的Forms还是Katana的cookies甚至是我接下来要介绍的ASPNETCORE基于cookies认证,其实整体的设计逻辑大致都差不多,只是具体实现上的区别,尤其是OWin的设计理念,当然现在我们几乎已经模糊了OWin的慨念,但是在ASPNETCORE平台上到处都有它的缩影。下面我们一起来看看ASPNETCOREMVC的认证机制。
在这里,整个认证逻辑我就直接用一张图展示:

画图工具是网上在线编辑的,画的不好,别见怪。下面我简单解释一下认证授权流程图,以cookies认证为例。
1.认证中间件调用CookieAuthenticationHandler实现认证,如果认证成功设置HttpContext.Use对象。
2.在执行controller中的action之前,执行授权filter,如果有设置授权filter特性。
3.如果controller或者action上没有授权filter,直接执行action,呈现view。
4.如果有定义授权filter特性,授权过滤器再次检查用户是否认证,并且合并Claim,因为可以指定多个认证scheme,认证阶段使用的是默认的sheme。
5.认证失败,授权filter设置context.Result为Challenge,在后续cookie认证中间件会发生重定向到login页面。
6.认证成功,授权失败,授权filter设置context.Result为Forbid,在后续cookie认证中间件会发生重定向到权限不足页面。
7.认证、授权都通过,最后显示view。
以上就是ASPNETCOREMVC认证授权的主要执行逻辑。接下来我们一起看看,基于COREMVC的cookies认证的应用以及内部实现。
熟悉ASPNETCORE平台开发的朋友应该知道,基础功能模块的配置初始化,一般分为两部曲,注册服务、配置中间件。当然这少不了NETCORE内置DI容器的功劳,我们将要介绍的认证系统也不例外。下面我们具体看看认证系统的配置,通过Startup类型配置,关于startup的提供机制可以看看我上一篇博客,有详细介绍。
第一部曲服务配置

 1 public static void AddAuthentication(this IServiceCollection services)
 2         {
 3         
 4             // 其他代码
 5             var authenticationBuilder = services.AddAuthentication(options =>
 6             {
 7                 options.DefaultChallengeScheme = AuthenticationDefaults.AuthenticationScheme;
 8                 options.DefaultScheme = AuthenticationDefaults.AuthenticationScheme;
 9                 options.DefaultSignInScheme = AuthenticationDefaults.ExternalAuthenticationScheme;
10             })
11             .AddCookie(AuthenticationDefaults.AuthenticationScheme, options =>
12             {
13                 options.Cookie.Name = $"{CookieDefaults.Prefix}{NopCookieDefaults.AuthenticationCookie}";
14                 options.Cookie.HttpOnly = true;
15                 options.LoginPath = AuthenticationDefaults.LoginPath;
16                 options.AccessDeniedPath = AuthenticationDefaults.AccessDeniedPath;
17             })
18             .AddCookie(AuthenticationDefaults.ExternalAuthenticationScheme, options =>
19             {
20                 options.Cookie.Name = $"{CookieDefaults.Prefix}{CookieDefaults.ExternalAuthenticationCookie}";
21                 options.Cookie.HttpOnly = true;
22                 options.LoginPath = AuthenticationDefaults.LoginPath;
23                 options.AccessDeniedPath = AuthenticationDefaults.AccessDeniedPath;
24             });
25         }

以上代码片段就完成了cookies认证的所需服务注册。其实际就是注册cookies认证所需的基础对象和辅助配置信息到DI容器,以便中间件可以通过DI容器方便获取。AddAuthentication扩展方法,主要是注册认证系统所需基础对象。AddCookie扩展方法主要是注册具体cookie认证Handler对象以及通过options模式配置辅助信息。
第二部曲中间件注册

1 public static void UseAuthentication(this IApplicationBuilder application)
2         {
3             // 其他代码
4             application.UseMiddleware<AuthenticationMiddleware>();
5         }

认证中间件的注册就这么一句代码,实际就是ASPNETCORE请求管道添加认证中间件,最后通过Build初始化到这个请求管道,后续所有的请求都会通过这个认证中间件的invoke方法处理,然后传递下一个中间件,关于中间件的原理也可以看我上一篇帖子。认证系统的配置我们已经准备完成,下面我们看看系统登录。
登录

 1   [HttpPost]
 2         public virtual IActionResult Login(LoginModel model, string returnUrl, bool captchaValid)
 3         {
 4         
 5             // 其他代码
 6             if (ModelState.IsValid)
 7             {
 8                 var loginResult = _userService.ValidateUser(model.Username, model.Password);
 9                 switch (loginResult)
10                 {
11                     case LoginResults.Successful:
12                         {
13                             var user = _userService.GetUserByUserName(model.Username);
14                             
15                             _authenticationService.SignIn(user, model.RememberMe);
16 
17                             return Redirect(returnUrl);
18                         }
19                 }
20             }
21 
22             return View(model);
23         }

以上登录代码片段比较简单,主要完成两个动作,1.收集用户输入的用户名&密码等信息,然后通过我们系统的存储介质,校验用户名&密码的合法性。2.登录到我们的认证系统,实现我们核心登录逻辑是SignIn方法里面。下面我们继续看看SignIn方法的具体实现。

 1 public virtual async void SignIn(User user, bool isPersistent)
 2         {
 3             // 其他代码
 4             // 创建身份信息集合
 5             var claims = new List<Claim>();
 6 
 7             if (!string.IsNullOrEmpty(user.Username))
 8                 claims.Add(new Claim(ClaimTypes.Name, user.Username, ClaimValueTypes.String, AuthenticationDefaults.ClaimsIssuer));
 9 
10             if (!string.IsNullOrEmpty(user.Email))
11                 claims.Add(new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.Email, AuthenticationDefaults.ClaimsIssuer));
12             
13             var userIdentity = new ClaimsIdentity(claims, AuthenticationDefaults.AuthenticationScheme);
14             var userPrincipal = new ClaimsPrincipal(userIdentity);
15             // 辅助信息
16             var authenticationProperties = new AuthenticationProperties
17             {
18                 IsPersistent = isPersistent,
19                 IssuedUtc = DateTime.UtcNow
20             };
21             // 创建cookie ticket,以备写入response输出到客户端
22             await _httpContextAccessor.HttpContext.SignInAsync(AuthenticationDefaults.AuthenticationScheme, userPrincipal, authenticationProperties);
23         }

以上代码片段就完成了我们认证系统的登录。大致逻辑是构建身份声明信息,调用HttpContext的SignInAsync方法创建ticket,在endrequest阶段创建cookie写入response。以上就是我们基于ASPNETCORE平台开发web应用对于认证的真实应用。接下来我们重点看看平台的内部实现。
Cookies认证内部实现
我们还是从服务注册开始吧,毕竟它是完成认证系统的基石。我们把视线转移到上面的AddAuthentication方法,注册服务,我们看看它到底为我们的认证系统注册了哪些基础服务,看NETCORE源代码。

1 public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
2         {
3             // 其他代码   
4             services.AddAuthenticationCore();
5             services.AddDataProtection();
6             services.AddWebEncoders();
7             services.TryAddSingleton<ISystemClock, SystemClock>();
8             return new AuthenticationBuilder(services);
9         }

从以上代码片段了解到,我们的认证服务注册是在平台AddAuthenticationCore方法里面完成的。我们一起看看AddAuthenticationCore方法的实现。

1 public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
2         {
3             services.TryAddScoped<IAuthenticationService, AuthenticationService>();
4             services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext
5             services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
6             services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
7             return services;
8         }

AddAuthenticationCore方法里面主要注册了我们NETCORE认证系统的三个基础对象,你可以把它们理解为黑帮的一个老大两个堂主,由它们吩咐下面的小弟完成任务,言归正传这三个对象也是完成我们NETCORE平台认证的三剑客,通过Provider模式实现,下面我们一个个来介绍,我们先看看IAuthenticationService接口的定义。

 1 public interface IAuthenticationService
 2     {
 3         Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
 4 
 5         Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);
 6 
 7         Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
 8 
 9         Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
10 
11         Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
12     }

IAuthenticationService接口定义了5个方法成员,它本身不实现任何认证逻辑,只是为IAuthenticationSchemeProvider 和 IAuthenticationHandlerProvider这两个Provider实现了封装,提供认证服务的统一接口。下面我大概解释一下这个5个方法在认证服务中的作用。
1.SignInAsync 登录操作,如果登录成功,生成加密ticket,用来标识用户的身份。
2.SignOutAsync 退出登录,清除Coookie等。
3.AuthenticateAsync 解密cookie,获取ticket并验证,最后返回一个 AuthenticateResult 对象,表示用户的身份。
4.ChallengeAsync 未认证,返回 401 状态码。
5.ForbidAsync 权限不足,返回 403 状态码。
下面我们一起看看它的唯一默认实现类AuthenticationService。

 1 public class AuthenticationService : IAuthenticationService
 2     {
 3     
 4         // 其他成员
 5         public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform);
 6 
 7         public IAuthenticationSchemeProvider Schemes { get; }
 8 
 9         public IAuthenticationHandlerProvider Handlers { get; }
10 
11         public IClaimsTransformation Transform { get; }
12 
13         public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
14 
15         
16         public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
17         {
18             if (scheme == null)
19             {
20                 var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
21                 scheme = defaultChallengeScheme?.Name;
22                 if (scheme == null)
23                 {
24                     throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found.");
25                 }
26             }
27 
28             var handler = await Handlers.GetHandlerAsync(context, scheme);
29             if (handler == null)
30             {
31                 throw await CreateMissingHandlerException(scheme);
32             }
33 
34             await handler.ChallengeAsync(properties);
35         }
36 
37         public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
38 
39         public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
40 
41         public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
42     }

代码比较多,我删掉了大部分,其实现逻辑都差不多。我们以ChallengeAsync方法为例,先获取相应的scheme,然后获取对应的Handler,最后执行Handler的同名方法。也就说明,真正的认证逻辑是在Handler里面完成的。从AuthenticationService的定义了解到,AuthenticationService的创建是基于Handlers和schemes创建的,下面我们看看认证的第二个基础对象IAuthenticationSchemeProvider。

 1 public interface IAuthenticationSchemeProvider
 2     {
 3         Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
 4 
 5         Task<AuthenticationScheme> GetSchemeAsync(string name);
 6 
 7         Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync();
 8 
 9         Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync();
10 
11         Task<AuthenticationScheme> GetDefaultForbidSchemeAsync();
12 
13         Task<AuthenticationScheme> GetDefaultSignInSchemeAsync();
14 
15         Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync();
16 
17         void AddScheme(AuthenticationScheme scheme);
18 
19         void RemoveScheme(string name);
20 
21         Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync();
22     }

scheme其实际就是提供认证方案标识,我们知道,NETCORE的认证系统所支持的认证方案非常丰富,比如openid、bearer、cookie等等。下面我们一起看看它的默认实现AuthenticationSchemeProvider对象。

 1 public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
 2     {
 3         public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
 4             : this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
 5         {
 6         }
 7 
 8         protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
 9         {
10             _options = options.Value;
11 
12             _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
13             _requestHandlers = new List<AuthenticationScheme>();
14 
15             foreach (var builder in _options.Schemes)
16             {
17                 var scheme = builder.Build();
18                 AddScheme(scheme);
19             }
20         }
21 
22         private readonly AuthenticationOptions _options;
23         private readonly object _lock = new object();
24         private readonly IDictionary<string, AuthenticationScheme> _schemes;
25         private readonly List<AuthenticationScheme> _requestHandlers;
26         private IEnumerable<AuthenticationScheme> _schemesCopy = Array.Empty<AuthenticationScheme>();
27         private IEnumerable<AuthenticationScheme> _requestHandlersCopy = Array.Empty<AuthenticationScheme>();
28 
29         private Task<AuthenticationScheme> GetDefaultSchemeAsync()
30             => _options.DefaultScheme != null
31             ? GetSchemeAsync(_options.DefaultScheme)
32             : Task.FromResult<AuthenticationScheme>(null);
33 
34         public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
35             => _options.DefaultAuthenticateScheme != null
36           
一个小插曲

前几天去面了一家公司,跟面试官话不投机,问题犀利,态度傲慢。比如一上场就问你用过IOC框架吗?其实简历上面也有写Autofac、Unity等,我就说了几个Autofac、Unit、CastleWindsor而且自己参考Autofac也实现过轻量级简单的容器,第二句就是你真的用过IOC框架,Ninject在NET平台这么主流你都没用过?比如还有什么是webapi的基类?很多此类问题,后面我也蒙圈了。闲话就说到这吧,没有其他意思,开心就好。下面我们一起看看,ASPNET-ASPNETCORE平台上面的WEB框架有哪些主流的认证方式,以及底层实现。

话题背景

关于认证我的个人理解是,验证信息的合法性。在我们生活当中,比如门禁,你想进入一个有相对安全措施的小区或者大楼,你需要向保安或者门禁系统提供你的身份信息证明,只有确定你是小区业主,才可以进来,我这只是打个比方啊,不要纠结。对于我们计算机的安全领域,认证其实也非常类似,windows系统登陆就是一个很好的例子。今天我们主要学习的是ASPNET以及ASPNETCORE平台上面一些主流的认证方式。

正式话题-认证

我最开始接触NET平台的WEB框架是从APSNETWEBFORM开始->ASPNETMVC->ASPNETMVCCORE,下面我们就从WEBFORM开始吧(包括MVC1.x-4.x)。在MVC5之前,我们常用的认证方式有,Forms、Windows、Passport、None这三种认证方式,严格意义上说是三种,None为不认证,而在这三种认证方式当中,我们最常用的就是Forms表单认证,下面我们一起来看看Forms表单认证的实现原理。

Forms表单认证

我会以我自己的使用方式介绍再到实现原理。整个Forms认证的实现逻辑大概是,说到Forms认证我们就不得不说ASPNET处理管道,为什么这么说呢?因为ASPNET的很多基础功能都是通过相应的HttpModule实现的,比如认证、授权、缓存、Session等等。ASPNET平台的Forms认证就是基于FormsAuthenticationModule模块实现,相应的Windows认证也是一样,由WindowsAuthenticationModule实现。对于Forms认证方式登录而言。

1.匹配用户名&密码是否正确。
2.构建FormsAuthenticationTicket对象。

3.通过FormsAuthentication.Encrypt方法加密Ticker信息。
4.基于加密Ticker信息,构建HttpCookie对象。
5.写入Response,输出到客户端。
以上就是我们基于Forms表单认证方式的登录实现逻辑,下面我们来梳理一下认证的大概实现逻辑,针对每次请求而言。
1.在ASPNET管道生命周期里,认证模块FormsAuthenticationModule会接管并读取Cookie。
2.解密Cookie获取FormsAuthenticationTicket对象并且验证是否过期。
3.根据FormsAuthenticationTicket对象构造FormsIdentity对象并设置HttpContext.User。
4.完成认证。
下面我们一起看看Forms认证的具体实现,我会以我自己开发过程中使用的方式加以介绍。首先我们会在web.config文件里面定义authentication配置节点,如下。

1 <authentication mode="Forms">
2       <forms name="AUTH" loginUrl="~/login" protection="All" timeout="43200" path="/" requireSSL="false" slidingExpiration="true" />
3     </authentication>

mode属性对应了4属性值,除Forms以外还有上面我提到的三种方式。其他三种由于篇幅问题,在这里不做介绍。这些属性我相信大家应该都比较熟悉。下面我们看看关于Forms认证具体的后台代码。看代码。

 1 public virtual void SignIn(User user, // 这个user是你校验合法性之后的这么一个用户标识对象
 2 bool createPersistentCookie)
 3         {
 4             var now = DateTime.UtcNow.ToLocalTime();
 5             // 构建Ticker对象
 6             var ticket = new FormsAuthenticationTicket(
 7                 1 ,
 8                user.Username,
 9                 now,
10                 now.Add(_expirationTimeSpan),
11                 createPersistentCookie,
12                 user.Username,
13                 FormsAuthentication.FormsCookiePath);
14             // 加密ticker对象
15             var encryptedTicket = FormsAuthentication.Encrypt(ticket);
16             // 通过加密ticker对象构建HttpCookie对象
17             var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
18             cookie.HttpOnly = true;
19             if (ticket.IsPersistent)
20             {
21                 cookie.Expires = ticket.Expiration;
22             }
23             cookie.Secure = FormsAuthentication.RequireSSL;
24             cookie.Path = FormsAuthentication.FormsCookiePath;
25             if (FormsAuthentication.CookieDomain != null)
26             {
27                 cookie.Domain = FormsAuthentication.CookieDomain;
28             }
29             // 写入输出流Response
30             _httpContext.Response.Cookies.Add(cookie);
31         }

以上代码就完成了我们的Forms认证所需的Cookie信息,可能有些朋友在以往开发WebForms到4.x最常用的使用方式是FormsAuthentication.SetAuthCookie(user.UserName, true),其实SetAuthCookie里面的实现逻辑跟上面实现大同小异,只是我比较喜欢手动创建可以更多的控制一些辅助信息而已。在以上代码片段中,我着重想介绍一下FormsAuthentication.Encrypt(ticket)加密方法,因为它涉及到了Forms认证的安全机制,也好让各位朋友大概了解Forms认证到底安全不安全。FormsAuthentication该对象位于System.Web.Security名称空间下面,主要作用是安全相关辅助工具类,比如加解密等。
1.在默认情况下,ASPNETFORMS认证模块针对Ticker的加密Key是由ASPNET随机生成,并存储在本地安全机构LSA中。我们可以通过一下代码片段验证这一逻辑。

 1 private CryptographicKey GenerateCryptographicKey(string configAttributeName, string configAttributeValue, int autogenKeyOffset, int autogenKeyCount, string errorResourceString)
 2     {
 3        // 其他代码
 4       bool flag1 = false;
 5       bool flag2 = false;
 6       bool flag3 = false;
 7       if (configAttributeValue != null)
 8       {
 9         string str1 = configAttributeValue;
10         char[] chArray = new char[1]{ ',' };
11         foreach (string str2 in str1.Split(chArray))
12         {
13           if (!(str2 == "AutoGenerate"))
14           {
15             if (!(str2 == "IsolateApps"))
16             {
17               if (!(str2 == "IsolateByAppId"))
18               flag3 = true;
19             }
20             else
21               flag2 = true;
22           }
23           else
24             flag1 = true;
25         }
26       }
27       if (flag2)
28         MachineKeyMasterKeyProvider.AddSpecificPurposeString((IList<string>) stringList, "IsolateApps", this.ApplicationName);
29       if (flag3)
30         MachineKeyMasterKeyProvider.AddSpecificPurposeString((IList<string>) stringList, "IsolateByAppId", this.ApplicationId);
31     }

以上代码片段逻辑也比较简单,自己体会吧。
2.手动指定machineKey配置节点,该配置节在web.config文件里面,其中包括可支持的加密算法,加密算法支持DES,3DES,AES等。具体代码我就不贴了,我们跟踪其实现原理意在了解Forms认证其安全性。
3.通过以上两点介绍,我个人认为Forms认证相对来说很安全。
Forms认证
下面我们看看Forms的实现原理。
ASPNET的Forms认证发生在ASPNET管道的FormsAuthenticationModule对象里面,在该对象里面的Init方法里面绑定了认证事件OnEnter,具体的认证实现是OnEnter里面调用的OnAuthenticate方法。我们来看下代码。

 1 private void OnAuthenticate(FormsAuthenticationEventArgs e)
 2     {
 3          // 其他代码
 4         bool cookielessTicket = false;
 5         // 从请求cookie里面抽取ticker票据信息
 6         FormsAuthenticationTicket ticketFromCookie = FormsAuthenticationModule.ExtractTicketFromCookie(e.Context, FormsAuthentication.FormsCookieName, out cookielessTicket);
 7         // 过期或者为null直接返回
 8         if (ticketFromCookie == null || ticketFromCookie.Expired)
 9           return;
10         FormsAuthenticationTicket ticket = ticketFromCookie;
11         // 如果启用滑动过期,更新过期时间
12         if (FormsAuthentication.SlidingExpiration)
13           ticket = FormsAuthentication.RenewTicketIfOld(ticketFromCookie);
14         e.Context.SetPrincipalNoDemand((IPrincipal) new GenericPrincipal((IIdentity) new FormsIdentity(ticket), new string[0]));
15         if (!cookielessTicket && !ticket.CookiePath.Equals("/"))
16         {
17           cookie = e.Context.Request.Cookies[FormsAuthentication.FormsCookieName];
18           if (cookie != null)
19             cookie.Path = ticket.CookiePath;
20         }
21         if (ticket == ticketFromCookie)
22           return;
23         if (cookielessTicket && ticket.CookiePath != "/" && ticket.CookiePath.Length > 1)
24           ticket = FormsAuthenticationTicket.FromUtc(ticket.Version, ticket.Name, ticket.IssueDateUtc, ticket.ExpirationUtc, ticket.IsPersistent, ticket.UserData, "/");
25         string cookieValue = FormsAuthentication.Encrypt(ticket, !cookielessTicket);
26         
27         if (cookielessTicket)
28         {
29           e.Context.CookielessHelper.SetCookieValue('F', cookieValue);
30           e.Context.Response.Redirect(e.Context.Request.RawUrl);
31         }
32         else
33         {
34           if (cookie != null)
35             cookie = e.Context.Request.Cookies[FormsAuthentication.FormsCookieName];
36           if (cookie == null)
37           {
38             cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue);
39             cookie.Path = ticket.CookiePath;
40           }
41           if (ticket.IsPersistent)
42             cookie.Expires = ticket.Expiration;
43           cookie.Value = cookieValue;
44           cookie.Secure = FormsAuthentication.RequireSSL;
45           cookie.HttpOnly = true;
46           if (FormsAuthentication.CookieDomain != null)
47             cookie.Domain = FormsAuthentication.CookieDomain;
48           cookie.SameSite = FormsAuthentication.CookieSameSite;
49           e.Context.Response.Cookies.Remove(cookie.Name);
50           e.Context.Response.Cookies.Add(cookie);
51         }
52     } 

以上代码片段反映了Forms认证具体逻辑,逻辑比较简单,我也大概做了一些注释,以上就是ASPNET在MVC5.x之前ASPNETForms认证的实现。接下来我们对ASPNET5.X之前的版本基于Forms认证做个简单的总结。
1.用户在未登录的情况下,访问我们受保护的资源。
2.FormsAuthenticationModule模块验证用户的合法性,主要是生成Identity对象和设置IsAuthenticated属性。
3.如果未登录则endrequest阶段跳转到web.config配置的登录页或者硬编码指定的登录页。
4.用户登录。
5.匹配用户名&密码,如果合法,生成ticker票据和cookie并写入response。
6.访问受保护的资源(授权部分)。
7.FormsAuthenticationModule模块验证用户的合法性。
8.如果为以认证用户IsAuthenticated=true,授权访问相应的资源。
后续的每次请求也是6,7,8循环。
针对Forms认证就此告一段落,下面我们接着介绍MVC5的常规认证方式。
MVC5Cookies认证方式
为什么我要把MVC5的认证方式单独做一个小结讲解呢?它有什么特别之处吗?没错,ASPNETMVC5引入了新的设计理念OWin,我个人的理解是,解耦webserver容器IIS和模块化。同时NET4.5也引入了ASPNET.Identity,Identity主要是提供帮助我们管理用户、角色以及存储,当然Identity相较Membership强大多了。对于OWin和Identity我在这里不做详细介绍,自己可以去搜一些帖子看或者查看官方文档。OWin在WebServers与ASPNETWebApplication之间定义了一套标准接口,其官方的开源实现是Katana这个开源项目,我们今天要介绍的MVC5的认证就是基于Katana这个开源项目的CookieAuthenticationMiddleware中间件实现的,在介绍CookieAuthenticationMiddleware中间件之前,我想简单罗列一下MVC5的cookies认证(你也可以认为是Katana实现的新的Forms认证)和我们早期使用的Forms认证做个简单的对比。
相同点:1.基于cookie认证 2.支持滑动过期策略 3.实现令牌保护 4.重定向。
不同点:Identity结合Owin实现了声明认证Claims-based。
以上是个人的一点理解,下面我们具体看看认证中间件的实现,CookieAuthenticationMiddleware的定义。

 1 public class CookieAuthenticationMiddleware : AuthenticationMiddleware<CookieAuthenticationOptions>
 2     {
 3         // 其他成员
 4         public CookieAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, CookieAuthenticationOptions options)
 5             : base(next, options)
 6         {
 7         }
 8         // 创建具体的AuthenticationHandler
 9         protected override AuthenticationHandler<CookieAuthenticationOptions> CreateHandler()
10         {
11             return new CookieAuthenticationHandler(_logger);
12         }
13     }

CookieAuthenticationMiddleware里面就一个方法成员,通过CreateHandler方法创建了具体的CookieAuthenticationHandler对象,我们的认证核心实现就发生在这个Handler里面。接下来我们看看CookieAuthenticationHandler对象的定义。

 1 internal class CookieAuthenticationHandler : AuthenticationHandler<CookieAuthenticationOptions>
 2     {
 3         // 其他成员
 4         private const string HeaderNameCacheControl = "Cache-Control";
 5         private const string HeaderNamePragma = "Pragma";
 6         private const string HeaderNameExpires = "Expires";
 7         private const string HeaderValueNoCache = "no-cache";
 8         private const string HeaderValueMinusOne = "-1";
 9         private const string SessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
10 
11         private bool _shouldRenew;
12         private DateTimeOffset _renewIssuedUtc;
13         private DateTimeOffset _renewExpiresUtc;
14         private string _sessionKey;
15         
16         protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
17         
18         protected override async Task ApplyResponseGrantAsync()
19         
20         protected override Task ApplyResponseChallengeAsync()
21     }

从CookieAuthenticationHandler对象的定义来看,其实也能看出一二,主要是针对cookie的相关操作,在该对象成员里面我们需要了解一下其中的三个方法。
1.AuthenticateCoreAsync,代码我就不贴了,有兴趣的朋友可以自己查看Katana开源项目的源代码。该方法内部大概实现思路是:从IOWinContext对象获取cookie,如果对owin不怎么熟悉的话,这个context你可以把它理解为我们之前熟悉的HttpContext,然后通过解密出来的cookie字符串构造ClaimsIdentity对象并添加到OwinContext对象Request.User,最后返回AuthenticationTicket对象,该对象包装的就是当前用户信息以及相关辅助信息。
2.ApplyResponseGrantAsync,设置、更新或者删除cookie并写入response。
3.ApplyResponseChallengeAsync,授权失败,发生重定向。

 1 public class AuthenticationTicket
 2     {
 3         public AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties properties)
 4         {
 5             Identity = identity;
 6             Properties = properties ?? new AuthenticationProperties();
 7         }
 8         // 用户信息
 9         public ClaimsIdentity Identity { get; private set; }
10         // 辅助信息,比如会话、过期等
11         public AuthenticationProperties Properties { get; private set; }
12     }

下面我们一起看看在我们开发过程中的应用以及内部实现
Startup是Katana开源项目引入的一种新的模块初始化方式,其实也没什么特别的,就是相关中间件的注册以及一些默认上下文对象的初始化操作。下面我们具体看代码,我们的MVC5新的认证方式在Startup里面如何注册的。

 1 public partial class Startup
 2     {
 3         public void ConfigureAuth(IAppBuilder app)
 4         {
 5             // 其他代码
 6             app.UseCookieAuthentication(new CookieAuthenticationOptions
 7             {
 8                 AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
 9                 LoginPath = new PathString("/Account/Login"),
10                 Provider = new CookieAuthenticationProvider
11                 { }
12             });           
13         }
14     }

注册逻辑很简单,通过IAppBuilder的扩展方法UseCookieAuthentication实现,接下来我们看看UseCookieAuthentication扩展方法的内部实现。

 1 public static IAppBuilder UseCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options, PipelineStage stage)
 2         {
 3             if (app == null)
 4             {  }
 5             // 注册
 6             app.Use(typeof(CookieAuthenticationMiddleware), app, options);
 7             // 加入owin管道
 8             app.UseStageMarker(stage);
 9             return app;
10         }

整个注册逻辑就这么几行代码,相关方法都有注释。最后在程序初始化过程中通过Build方法完成Owin管道所有中间件的初始化工作。接下来我们看看具体的登录实现。

 1 public async Task<ActionResult> Login(LoginModel model,string returnUrl)
 2 {
 3     // 其他代码
 4     if (ModelState.IsValid)
 5     {
 6         AppUser user = await UserManager.FindAsync(model.Name, model.Password);
 7         if (user==null)
 8         {
 9             ModelState.AddModelError("","无效的用户名或密码");
10         }
11         else
12         {
13             var claimsIdentity =
14                 await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
15             AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity);
16             return Redirect(returnUrl);
17         }
18     }
19  
20     return View(model);
21 }

通过以上代码片段就完成了我们系统的登录操作,在以上Login方法里面,我们需要注意这么几个方法。
1.FindAsync主要是通过Identity实现用户名和密码的匹配。
2.CreateIdentityAsync主要是创建ClaimsIdentity对象,该对象后续会写入cookie。
3.SignIn包装CreateIdentity方法创建的ClaimsIdentity以及ClaimsPrincipal对象,为cookie写入Response提供相关认证信息,只有在设置cookie阶段才会写入response。
接下来我们针对Katana里面的cookie认证做个简单的总结。
1.用户在未登录的情况下,访问我们受保护的资源。
2.CookieAuthenticationMiddleware中间件验证用户的合法性。
3.用户登录。
4.匹配用户名&密码,如果合法,包装相关认证信息。
5.创建\更新cookie写入response。
6.访问受保护的资源。
7.CookieAuthenticationMiddleware中间件解密cookie验证用户认证信息。
8.如果为以认证用户,授权访问相应的资源。
后续的每次请求也是6,7,8循环。
以上MVC5新的Cookies认证方式就此告一段落,下面我们接着介绍ASPNET.Identity三方认证。
三方认证
在我们介绍三方认证之前,我们不妨先来了解一下什么是Claim,大家把它翻译成声明,我也就这么跟着叫把。Claim所描述的是一个用户单一的某个信息,比如用户名,只有多个Claim组合才能描述一个完整的用户ClaimsIdentity对象。个人理解这是一种通用的信息存储结构,一种规范,可以很方便的基于用户数据信息驱动认证和授权并且提供独立服务,各自都不需要关心自己的实现。在我们传统的认证windows或者forms认证方式中,每个系统都有自己认证方式、授权和用户数据信息,如果是几年以前,可能没有什么问题,但是在如今飞速发展的互联网时代,就显的有很大的局限性、扩展性以及安全性。接下来我们要介绍的就是MVC5基于ASPNET.Identity结合Katana实现的三方认证,也就是我们上面说的基于Claims-based实现第三方认证,这里我们以google认证为例,由于网络问题这里我们以木宛城主大拿的实例代码做示例,我会结合实例代码分析内部实现。
首先我们需要添加google服务认证中间件。

1 app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
2 {
3      // 以下为客户端凭据,可以通过google认证服务注册
4     ClientId = "",
5     ClientSecret = "",
6 });

其二我们需要设计实现登录逻辑,通常情况下我们在登录论坛的时候,旁边可能会有基于QQ登录或者别的三方认证提供商。

 1 public ActionResult GoogleLogin(string returnUrl)
 2 {    // 创建AuthenticationProperties对象,我们可以理解为认证复制信息字典
 3     var properties = new AuthenticationProperties
 4     {    
 5         RedirectUri = Url.Action("GoogleLoginCallback",
 6         new { returnUrl = returnUrl })
 7     };
 8     // 初始化google认证相关辅助信息
 9     HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");
10     // 返回401
11     return new HttpUnauthorizedResult();
12 }

以上代码比较简单,我也做了相应的注释,其逻辑是初始化google认证的一些辅助信息,然后返回401状态码,继而重定向到google登录页。下面我们看看登录成功之后的代码逻辑。

 1 public async Task<ActionResult> GoogleLoginCallback(string returnUrl)
 2 {
 3     // 从google认证服务获取claims
 4     ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();
 5     // 检查该用户是否首次登录系统
 6     AppUser user = await UserManager.FindAsync(loginInfo.Login);
 7     if (user == null)
 8     {
 9         user = new AppUser
10         {
11             Email = loginInfo.Email,
12             UserName = loginInfo.DefaultUserName,
13             City = Cities.Shanghai,
14             Country = Countries.China
15         };
16         // 持久化用户数据
17         IdentityResult result = await UserManager.CreateAsync(user);
18         // 缓存
19         result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
20     }
21     ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
22         DefaultAuthenticationTypes.ApplicationCookie);
23     ident.AddClaims(loginInfo.ExternalIdentity.Claims);
24     // 创建用户ClaimsIdentity对象
25     AuthManager.SignIn(new AuthenticationProperties
26     {
27         IsPersistent = false
28     }, ident);
29     return Redirect(returnUrl ?? "/");
30 }

以上就是三方认证的实现方式,下面我们通过Katana源码看看三方认证的实现原理。通过上面Katana的cookies认证,我们了解到认证中间件的认证逻辑是实现在相应的AuthenticationHandler里面,我们同样以google为例,去看看内部的实现。下面我们一起来上面注册的认证中间件GoogleOAuth2AuthenticationMiddleware的定义。

 1 public class GoogleOAuth2AuthenticationMiddleware : AuthenticationMiddleware<GoogleOAuth2AuthenticationOptions>
 2     {
 3         // 其他成员
 4         public GoogleOAuth2AuthenticationMiddleware(
 5             OwinMiddleware next,
 6             IAppBuilder app,
 7             GoogleOAuth2AuthenticationOptions options)
 8             : base(next, options);
 9         // 构建认证handler
10         protected override AuthenticationHandler<GoogleOAuth2AuthenticationOptions> CreateHandler()
11         {
12             return new GoogleOAuth2AuthenticationHandler(_httpClient, _logger);
13         }
14         // 构建httpclienthandler
15         private static HttpMessageHandler ResolveHttpMessageHandler(GoogleOAuth2AuthenticationOptions options);
16     }

根据以上代码片段我们了解到,GoogleOAuth2AuthenticationMiddleware中间件似乎比我们常规的cookies认证多了一个方法ResolveHttpMessageHandler,其实这个方法没有别的套路,就是辅助创建httpclient对象,完成http请求而已,在handler的认证逻辑里面需要获取googletoken,就是通过它来获取的。
第二个方法CreateHandler返回的GoogleOAuth2AuthenticationHandler对象就是我们接下来要重点讨论的对象。

 1 internal class GoogleOAuth2AuthenticationHandler : AuthenticationHandler<GoogleOAuth2AuthenticationOptions>
 2     {
 3         private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
 4         private const string UserInfoEndpoint = "https://www.googleapis.com/plus/v1/people/me";
 5         private const string AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth";
 6 
 7         private readonly ILogger _logger;
 8         private readonly HttpClient _httpClient;
 9 
10         public GoogleOAuth2AuthenticationHandler(HttpClient httpClient, ILogger logger)
11         {
12             _httpClient = httpClient;
13             _logger = logger;
14         }
15         // 通过httpclient访问google认证服务器获取token,根据token数据包装Claim
16         protected override async Task<AuthenticationTicket> AuthenticateCoreAsync();
17         // 如果未认证,401授权失败发生重定向
18         protected override Task ApplyResponseChallengeAsync();
19 
20         public override async Task<bool> InvokeAsync()
21         {
22             return await InvokeReplyPathAsync();
23         }
24         // 调用signin,保存用户信息  
25         private async Task<bool> InvokeReplyPathAsync();
26     }

代码比较长,我把具体实现删掉了,实现逻辑我注释到了方法上面,有兴趣的朋友可以自己多看看源码。以上就是NET平台上面一些主流的认证方式和实现原理。接下来我们继续介绍ASPNETCORE的认证。

ASPNETCORE认证
熟悉微软web平台认证授权体系的朋友应该知道,不管是早期的Forms还是Katana的cookies甚至是我接下来要介绍的ASPNETCORE基于cookies认证,其实整体的设计逻辑大致都差不多,只是具体实现上的区别,尤其是OWin的设计理念,当然现在我们几乎已经模糊了OWin的慨念,但是在ASPNETCORE平台上到处都有它的缩影。下面我们一起来看看ASPNETCOREMVC的认证机制。
在这里,整个认证逻辑我就直接用一张图展示:

画图工具是网上在线编辑的,画的不好,别见怪。下面我简单解释一下认证授权流程图,以cookies认证为例。
1.认证中间件调用CookieAuthenticationHandler实现认证,如果认证成功设置HttpContext.Use对象。
2.在执行controller中的action之前,执行授权filter,如果有设置授权filter特性。
3.如果controller或者action上没有授权filter,直接执行action,呈现view。
4.如果有定义授权filter特性,授权过滤器再次检查用户是否认证,并且合并Claim,因为可以指定多个认证scheme,认证阶段使用的是默认的sheme。
5.认证失败,授权filter设置context.Result为Challenge,在后续cookie认证中间件会发生重定向到login页面。
6.认证成功,授权失败,授权filter设置context.Result为Forbid,在后续cookie认证中间件会发生重定向到权限不足页面。
7.认证、授权都通过,最后显示view。
以上就是ASPNETCOREMVC认证授权的主要执行逻辑。接下来我们一起看看,基于COREMVC的cookies认证的应用以及内部实现。
熟悉ASPNETCORE平台开发的朋友应该知道,基础功能模块的配置初始化,一般分为两部曲,注册服务、配置中间件。当然这少不了NETCORE内置DI容器的功劳,我们将要介绍的认证系统也不例外。下面我们具体看看认证系统的配置,通过Startup类型配置,关于startup的提供机制可以看看我上一篇博客,有详细介绍。
第一部曲服务配置

 1 public static void AddAuthentication(this IServiceCollection services)
 2         {
 3         
 4             // 其他代码
 5             var authenticationBuilder = services.AddAuthentication(options =>
 6             {
 7                 options.DefaultChallengeScheme = AuthenticationDefaults.AuthenticationScheme;
 8                 options.DefaultScheme = AuthenticationDefaults.AuthenticationScheme;
 9                 options.DefaultSignInScheme = AuthenticationDefaults.ExternalAuthenticationScheme;
10             })
11             .AddCookie(AuthenticationDefaults.AuthenticationScheme, options =>
12             {
13                 options.Cookie.Name = $"{CookieDefaults.Prefix}{NopCookieDefaults.AuthenticationCookie}";
14                 options.Cookie.HttpOnly = true;
15                 options.LoginPath = AuthenticationDefaults.LoginPath;
16                 options.AccessDeniedPath = AuthenticationDefaults.AccessDeniedPath;
17             })
18             .AddCookie(AuthenticationDefaults.ExternalAuthenticationScheme, options =>
19             {
20                 options.Cookie.Name = $"{CookieDefaults.Prefix}{CookieDefaults.ExternalAuthenticationCookie}";
21                 options.Cookie.HttpOnly = true;
22                 options.LoginPath = AuthenticationDefaults.LoginPath;
23                 options.AccessDeniedPath = AuthenticationDefaults.AccessDeniedPath;
24             });
25         }

以上代码片段就完成了cookies认证的所需服务注册。其实际就是注册cookies认证所需的基础对象和辅助配置信息到DI容器,以便中间件可以通过DI容器方便获取。AddAuthentication扩展方法,主要是注册认证系统所需基础对象。AddCookie扩展方法主要是注册具体cookie认证Handler对象以及通过options模式配置辅助信息。
第二部曲中间件注册

1 public static void UseAuthentication(this IApplicationBuilder application)
2         {
3             // 其他代码
4             application.UseMiddleware<AuthenticationMiddleware>();
5         }

认证中间件的注册就这么一句代码,实际就是ASPNETCORE请求管道添加认证中间件,最后通过Build初始化到这个请求管道,后续所有的请求都会通过这个认证中间件的invoke方法处理,然后传递下一个中间件,关于中间件的原理也可以看我上一篇帖子。认证系统的配置我们已经准备完成,下面我们看看系统登录。
登录

 1   [HttpPost]
 2         public virtual IActionResult Login(LoginModel model, string returnUrl, bool captchaValid)
 3         {
 4         
 5             // 其他代码
 6             if (ModelState.IsValid)
 7             {
 8                 var loginResult = _userService.ValidateUser(model.Username, model.Password);
 9                 switch (loginResult)
10                 {
11                     case LoginResults.Successful:
12                         {
13                             var user = _userService.GetUserByUserName(model.Username);
14                             
15                             _authenticationService.SignIn(user, model.RememberMe);
16 
17                             return Redirect(returnUrl);
18                         }
19                 }
20             }
21 
22             return View(model);
23         }

以上登录代码片段比较简单,主要完成两个动作,1.收集用户输入的用户名&密码等信息,然后通过我们系统的存储介质,校验用户名&密码的合法性。2.登录到我们的认证系统,实现我们核心登录逻辑是SignIn方法里面。下面我们继续看看SignIn方法的具体实现。

 1 public virtual async void SignIn(User user, bool isPersistent)
 2         {
 3             // 其他代码
 4             // 创建身份信息集合
 5             var claims = new List<Claim>();
 6 
 7             if (!string.IsNullOrEmpty(user.Username))
 8                 claims.Add(new Claim(ClaimTypes.Name, user.Username, ClaimValueTypes.String, AuthenticationDefaults.ClaimsIssuer));
 9 
10             if (!string.IsNullOrEmpty(user.Email))
11                 claims.Add(new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.Email, AuthenticationDefaults.ClaimsIssuer));
12             
13             var userIdentity = new ClaimsIdentity(claims, AuthenticationDefaults.AuthenticationScheme);
14             var userPrincipal = new ClaimsPrincipal(userIdentity);
15             // 辅助信息
16             var authenticationProperties = new AuthenticationProperties
17             {
18                 IsPersistent = isPersistent,
19                 IssuedUtc = DateTime.UtcNow
20             };
21             // 创建cookie ticket,以备写入response输出到客户端
22             await _httpContextAccessor.HttpContext.SignInAsync(AuthenticationDefaults.AuthenticationScheme, userPrincipal, authenticationProperties);
23         }

以上代码片段就完成了我们认证系统的登录。大致逻辑是构建身份声明信息,调用HttpContext的SignInAsync方法创建ticket,在endrequest阶段创建cookie写入response。以上就是我们基于ASPNETCORE平台开发web应用对于认证的真实应用。接下来我们重点看看平台的内部实现。
Cookies认证内部实现
我们还是从服务注册开始吧,毕竟它是完成认证系统的基石。我们把视线转移到上面的AddAuthentication方法,注册服务,我们看看它到底为我们的认证系统注册了哪些基础服务,看NETCORE源代码。

1 public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
2         {
3             // 其他代码   
4             services.AddAuthenticationCore();
5             services.AddDataProtection();
6             services.AddWebEncoders();
7             services.TryAddSingleton<ISystemClock, SystemClock>();
8             return new AuthenticationBuilder(services);
9         }

从以上代码片段了解到,我们的认证服务注册是在平台AddAuthenticationCore方法里面完成的。我们一起看看AddAuthenticationCore方法的实现。

1 public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
2         {
3             services.TryAddScoped<IAuthenticationService, AuthenticationService>();
4             services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext
5             services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
6             services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
7             return services;
8         }

AddAuthenticationCore方法里面主要注册了我们NETCORE认证系统的三个基础对象,你可以把它们理解为黑帮的一个老大两个堂主,由它们吩咐下面的小弟完成任务,言归正传这三个对象也是完成我们NETCORE平台认证的三剑客,通过Provider模式实现,下面我们一个个来介绍,我们先看看IAuthenticationService接口的定义。

 1 public interface IAuthenticationService
 2     {
 3         Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
 4 
 5         Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);
 6 
 7         Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
 8 
 9         Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
10 
11         Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
12     }

IAuthenticationService接口定义了5个方法成员,它本身不实现任何认证逻辑,只是为IAuthenticationSchemeProvider 和 IAuthenticationHandlerProvider这两个Provider实现了封装,提供认证服务的统一接口。下面我大概解释一下这个5个方法在认证服务中的作用。
1.SignInAsync 登录操作,如果登录成功,生成加密ticket,用来标识用户的身份。
2.SignOutAsync 退出登录,清除Coookie等。
3.AuthenticateAsync 解密cookie,获取ticket并验证,最后返回一个 AuthenticateResult 对象,表示用户的身份。
4.ChallengeAsync 未认证,返回 401 状态码。
5.ForbidAsync 权限不足,返回 403 状态码。
下面我们一起看看它的唯一默认实现类AuthenticationService。

 1 public class AuthenticationService : IAuthenticationService
 2     {
 3     
 4         // 其他成员
 5         public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform);
 6 
 7         public IAuthenticationSchemeProvider Schemes { get; }
 8 
 9         public IAuthenticationHandlerProvider Handlers { get; }
10 
11         public IClaimsTransformation Transform { get; }
12 
13         public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
14 
15         
16         public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
17         {
18             if (scheme == null)
19             {
20                 var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
21                 scheme = defaultChallengeScheme?.Name;
22                 if (scheme == null)
23                 {
24                     throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found.");
25                 }
26             }
27 
28             var handler = await Handlers.GetHandlerAsync(context, scheme);
29             if (handler == null)
30             {
31                 throw await CreateMissingHandlerException(scheme);
32             }
33 
34             await handler.ChallengeAsync(properties);
35         }
36 
37         public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
38 
39         public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
40 
41         public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
42     }

代码比较多,我删掉了大部分,其实现逻辑都差不多。我们以ChallengeAsync方法为例,先获取相应的scheme,然后获取对应的Handler,最后执行Handler的同名方法。也就说明,真正的认证逻辑是在Handler里面完成的。从AuthenticationService的定义了解到,AuthenticationService的创建是基于Handlers和schemes创建的,下面我们看看认证的第二个基础对象IAuthenticationSchemeProvider。

 1 public interface IAuthenticationSchemeProvider
 2     {
 3         Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
 4 
 5         Task<AuthenticationScheme> GetSchemeAsync(string name);
 6 
 7         Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync();
 8 
 9         Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync();
10 
11         Task<AuthenticationScheme> GetDefaultForbidSchemeAsync();
12 
13         Task<AuthenticationScheme> GetDefaultSignInSchemeAsync();
14 
15         Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync();
16 
17         void AddScheme(AuthenticationScheme scheme);
18 
19         void RemoveScheme(string name);
20 
21         Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync();
22     }

scheme其实际就是提供认证方案标识,我们知道,NETCORE的认证系统所支持的认证方案非常丰富,比如openid、bearer、cookie等等。下面我们一起看看它的默认实现AuthenticationSchemeProvider对象。

 1 public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
 2     {
 3         public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
 4             : this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
 5         {
 6         }
 7 
 8         protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
 9         {
10             _options = options.Value;
11 
12             _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
13             _requestHandlers = new List<AuthenticationScheme>();
14 
15             foreach (var builder in _options.Schemes)
16             {
17                 var scheme = builder.Build();
18                 AddScheme(scheme);
19             }
20         }
21 
22         private readonly AuthenticationOptions _options;
23         private readonly object _lock = new object();
24         private readonly IDictionary<string, AuthenticationScheme> _schemes;
25         private readonly List<AuthenticationScheme> _requestHandlers;
26         private IEnumerable<AuthenticationScheme> _schemesCopy = Array.Empty<AuthenticationScheme>();
27         private IEnumerable<AuthenticationScheme> _requestHandlersCopy = Array.Empty<AuthenticationScheme>();
28 
29         private Task<AuthenticationScheme> GetDefaultSchemeAsync()
30             => _options.DefaultScheme != null
31             ? GetSchemeAsync(_options.DefaultScheme)
32             : Task.FromResult<AuthenticationScheme>(null);
33 
34         public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
35             => _options.DefaultAuthenticateScheme != null
36           

猜你喜欢

转载自www.cnblogs.com/bwdblogs/p/11226961.html
今日推荐