概述
上两篇(asp.net core 3.x 身份验证-1涉及到的概念、asp.net core 3.x 身份验证-2启动阶段的配置)介绍了身份验证相关概念以及启动阶段的配置,本篇以cookie身份验证为例来大致说明asp.net core中的身份验证原理。如果我们的应用只考虑浏览器使用,且不考虑前后端分离,cookie是最简单的身份验证方式。虽然这样命名,但我们的用户标识并非一定要存到cookie里,asp.net core允许我们存储到任何地方,如:session、自定义基于内存的存储、redis等等。身份验证与asp.net core Identity结合会更简单,它提供了用户管理功能,以及更身份验证相关的辅助类,如:SignManager,不过暂时不将这东东。
还是以宏观上的理解和使用为主。主要涉及如下流程:
- 未登录时请求受保护的资源
- 登录
- 已登录时访问受保护的资源
- 注销
其中步骤1、3差别很小。总体流程大致如下图:
先在Startup中做如下配置:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddAuthentication().AddCookie(); 4 services.AddControllersWithViews(); 5 } 6 7 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 8 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 9 { 10 if (env.IsDevelopment()) 11 { 12 app.UseDeveloperExceptionPage(); 13 } 14 else 15 { 16 app.UseExceptionHandler("/Home/Error"); 17 } 18 app.UseStaticFiles(); 19 20 app.UseRouting(); 21 app.UseAuthentication(); 22 app.UseAuthorization(); 23 24 app.UseEndpoints(endpoints => 25 { 26 endpoints.MapControllerRoute( 27 name: "default", 28 pattern: "{controller=Home}/{action=Index}/{id?}"); 29 }); 30 }
流程1、3、访问受保护的资源
当我们在Startup.Configre中 app.UseAuthentication(); 将注册身份验证中间AuthenticationMiddleware,它负责在请求阶段尝试从请求中获取用户标识,若获取到则赋值给HttpContext.User属性,否则以匿名用户身份继续执行下一个中间件。
AuthenticationMiddleware
流程大致如下:
- 遍历程序中所有实现IAuthenticationRequestHandler身份验证处理器,调用其HandleRequestAsync方法,此方法若返回true则终止本次请求
- 通过AuthenticationSchemeProvider获取默认身份验证方案
- 调用httpContext.AuthenticateAsync(默认方案名)尝试从请求中获取当前用户
- 如果前一步获取到用户,则设置到httpContext.User属性上
- 无论是否成功获取用户请求都会进入下一个中间件继续后续执行
步骤1中允许AuthenticationHandler在特定情况下终止请求,如果我们将来自定义AuthenticationHandler时实现IAuthenticationRequestHandler可以达到这种目的
步骤3是核心,httpContext.AuthenticateAsync是扩展方法,内部从IOC容器中获取AuthenticationService并调用其同名方法
疑惑:AuthenticationMiddleware始终获取默认身份验证方案然后尝试从请求中获取用户标识,这个默认是通过Startup.ConfigreService中 services.AddAuthentication(默认方案名) 来配置的。但一个系统中可以注册多个身份验证方案(如:同时支持Cookie和JWTBearerToken方式),所以咋处理呢?我目前能想到的是要么纯网站直接用cookie,前后端分离或移动端app默认方案设置为JWTBearerToken;另一种方式再自定义一个AuthenticationMiddleware;但是感觉都很别扭,所以你有好的想法请指教下,不胜感激!
AuthenticationService.AuthenticateAsync
大致执行如下步骤:
- 通过AuthenticationHandlerProvider(注入进来的)获取指定(这里就是默认的)身份验证方案名获取关联的身份验证处理器AuthenticationHandler
- 调用AuthenticationHandler.AuthenticateAsync()尝试从请求中获取用户
- 返回前尝试将用户标识转换成另一个用户标识(且进行缓存,由于AuthenticationService是Scope注册的,所以没问题)
步骤2针对Cookie身份验证来说最终体现为CookieAuthenticationHandler.HandleAuthenticateAsync()
步骤3在将用户标识设置到httpContext.User之前允许我们添加修改用户标识的某些数据。实现方式是定义类实现IClaimsTransformation并将其(推荐单例)注册到依赖注入容器。
CookieAuthenticationHandler.HandleAuthenticateAsync
大致流程如下:
- 通过Options.CookieManager获取cookie
- 若Options.SessionStore不为空则从其获取票证,否则直接解密cookie得到票证
- 处理票证过期(如Option设置了滑动过期,在过期时响应时重新将票证写入cookie)
- 回调Events.ValidatePrincipal允许我们修改或替换用户标识
上述步骤多次用到了Options,它是CookieAuthenticationOptions类型的,专门针对基于cookie的身份验证处理器的选项对象,参考:《asp.net core 3.x 身份验证-2启动阶段的配置》,此选项对象的默认赋值行为在PostConfigureCookieAuthenticationOptions.PostConfigre中定义,我们也可以在启动配置时进行定制,
比如我们想直接将票证存储到自定义的存储中时可以参考下面的配置:
- 自定义一个类实现ITicketStore(参考)
- 配置cookie身份验证方案时修改选项 services.AddAuthentication().AddCookie(opt=> opt.SessionStore = new MySessionStore());
在CookieAuthenticationHandler的其它多个步骤中都可能使用到此选项对象。使用方式都类似。
在前一步骤AuthenticationService.AuthenticateAsync的步骤3中允许我们的代码替换/修改用户标识,在这里的步骤4也允许做类似的事,区别在于前者是针对所有身份验证方案有效的,如默认身份验证方案为JwtBeaerToken时也有效。而后者只是针对cookie身份验证有效。
AuthorizationMiddleware
由于首次请求用户尚未登录,所以context.User为空。授权中间件最终会执行 context.ChallengeAsync(); 最终会执行CookieAuthenticationHandler.HandleChallengeAsync
CookieAuthenticationHandler.HandleChallengeAsync
默认大致流程如下:
- 组织登录页地址:Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, 当前地址);
- 回调Events.RedirectToLogin,若是ajax请求则返回401状态码,否则跳转到登录页
默认登录页地址"/Account/Login",参考PostConfigureCookieAuthenticationOptions.PostConfigre
默认处理流程如下:
1 public Func, Task> OnRedirectToLogin { get; set; } = context => 2 { 3 if (IsAjaxRequest(context.Request)) 4 { 5 context.Response.Headers[HeaderNames.Location] = context.RedirectUri; 6 context.Response.StatusCode = 401; 7 } 8 else 9 { 10 context.Response.Redirect(context.RedirectUri); 11 } 12 return Task.CompletedTask; 13 };
我们也可以通过在应用启动时通过选项对象来修改。
1 services.AddAuthentication().AddCookie(opt => 2 { 3 opt.LoginPath = "account/mylogin"; 4 opt.Events.OnRedirectToLogin = context => 5 { 6 context.Response.Redirect(context.RedirectUri); 7 return Task.CompletedTask; 8 }; 9 });
流程2、登录
前端提交用户名密码,Action去数据库对比,若成功则以用户id(或一些非常常用的和与授权相关的属性)作为用户标识。然后调用HttpContext.SignInAsync(ClaimsPrincipal),所以最终会执行CookieAuthenticationHandler.HandleSignInAsync
CookieAuthenticationHandler.HandleSignInAsync
大致步骤如下:
- 准备一个CookieSigningInContext(=当前上下文+当前身份验证方案+当前身份验证方案关联的选项对象+需要登录用户的标识+cookie选项+额外数据)
- 处理过期时间等属性
- 回调Events.SigningIn,允许我们在写入cookie之前做调整
- 准备票证(=用户 + 身份验证相关属性 + 身份验证方案名 )
- 若此身份验证处理器对应的选项配置了Options.SessionStore则使用此存储,否则加密票证写入cookie
- 回调Events.SignedIn
- 最后回调Events.RedirectToReturnUrl尝试调整会最初的访问页面
流程4、注销
在Action中调用Context.SingOutAsync可以实现注销,在为传入方案名的情况下使用默认身份验证方案进行注销,假设默认是cookie身份验证,则CookieAuthenticationHandler.HandleSignOutAsync将被执行。大致流程如下:
若选项对象配置了Options.SessionStore则从其清空用户标识
回调Events.SigningOut
清除cookie(可能是含sessionKey或包含用户标识的cookie)
通过回调Events.RedirectToReturnUrl跳转到Options.ReturnUrlParameter页面
总结
本篇只是从大方向说了下基于cookie的身份验证原理,对于我们开发者来说只要记住如下几个点就行了,将来有特殊需求时再去看源码。
- 应用启动时Startup中通过AddAuthencation(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt=>{ 配置各种选项/不配置直接使用默认值 });Configure方法中app.UseRouting();后app.UseAuthentication();插入身份验证中间件
- opt是CookieAuthenticationOptions类型,我们可以通过它来参与到身份验证的过程中去,记得它有个回调函数集合Events属性。这个选项对象的默认值定义在PostConfigureCookieAuthenticationOptions.PostConfigre、CookieAuthenticationDefaults,Events属性的类型和默认值参考CookieAuthenticationEvents
- 通常我们调用身份验证方法是通过HttpContext来调用,它会从IOC容器拿到AuthenticationService调用其同名方法,最终会找到合适的AuthenticationHandler并调用其同名方法
暂时就想到这么多,以后想到再补充吧。