前言
从本篇开始将围绕asp.net core身份验证写个小系列,希望你看完本系列后,脑子里对asp.net core的身份验证原理有个大致印象。
至于身份验证是啥?与授权有啥联系?就不介绍了,太啰嗦。你如果不晓得,自己去搜搜吧。
我的学习思路是详细看源码 > 总结得出一个宏观上的印象 + 如何使用。
如果发现有啥讲错的望指正,免得误导观众
我们偶尔会思考如何设计一个牛X的软件,其实通过对asp.net core框架本身的学习更划算,一来我们熟悉了asp.net core框架,再者我们学习了微软碰到需求是如何设计的。
计划:
- 基本介绍 - 概述 + 核心类介绍
- 基于cookie/session的身份验证原理 - 适合浏览器
- 基于Token身份验证 - 适合移动端app
- 集成第三方登录原理 - 比如集成微信、支付宝登录
- IdentityServer - 目前不鸟解
- asp.net core Identity - 目前不鸟解
必备知识:asp.net core、配置、选项、依赖注入、中间件等...
参考:源码、Artech、mvc5基于owin的身份验证视频、ASP.NET Core 运行原理解剖[5]:Authentication
注意:本篇只讲涉及到的几个概念
推荐个不错的流程图/脑图工具:https://www.processon.com/i/59accdd8e4b0859febda28e3,点这个链接注册我可以获得几个文件限额,抱拳~
身份验证方式和简易流程
常见的身份验证方式:
- 基于cookie/session的身份验证 - 适合浏览器
- 基于JWTToken身份验证(OAuth2) - 适合移动端app
- 集成第三方登录(OAuth2) - 比如集成微信、支付宝登录
为了便于理解后续的概念,下面先以最简单常见的 【用户密码+cookie】 的身份验证方式说说核心流程
登录:
- 用户输入账号密码提交
- 服务端验证账号密码
- 若验证成功,则创建一个包含用户标识的票证(下面会说)
- 将票证加密成字符串写入cookie
携带cookie请求:
- 用户发起请求
- 身份验证中间件尝试获取并解密cookie,进而得到含用户标识的票证(下面会说)
- 将用户标识设置到HttpContext.User属性
注意:若身份验证中间件即使没有解析得到用户标识,请求也会继续执行,此时以匿名用户的身份在访问系统
用户标识ClaimsPrincipal
它用来表示当前登录的用户,它包含用户Id + 一些与权限检查相关的附件属性(角色、所属部门)。
当请求抵达时“身份验证中间件”将从请求中解析得到当前用户,如果获取成功则赋值给HttpContext.User属性
所以对于我们来说通常有两个场景使用它
在任意能访问HttpContext的地方获取当前用户,比如在Controller中。
如果需要自定义实现身份验证,则我们要想方设法从请求中解析得到用户,并赋值给HttpContext.User
现在你至少对用户标识这个概念有点理解了,如果要刨根问底儿就自行搜索关键字:asp.net Claims
也许你曾经做过或见过这样的设计,定义Employee表示当前系统的用户,当用户登录时会从数据库查询得到对应的Employee,若账号密码验证通过则将其放入Session或缓存中。下次访问时直接从Session/缓存中获取当前用户。个人觉得这种设计存在如下问题:
- 浪费内存:我们的业务代码访问当前用户最多的字段可能只是用户id,性别、地址、联系电话、学历....这些字段不是每个业务处理都需要的
- 抛弃了asp.net身份验证框架:从asp.net 2.0时代微软就设计了IPrincipal,后续的版本直到mvc5中基于owin的身份验证都在使用此接口,后续的权限验证微软也提供了,也是基于此接口的,但我们放弃了,反而是自己有写了一套微软本身就实现的功能,可能多数是觉得自己写的更简单。但我觉得判断哪种方式更合适是在你对两种方式都了解的情况下再做出判断。
用户票证AuthenticationTicket
既然有了上面的用户标识,何不直接在登录时加密这个标识,解析时直接解密得到呢?因为我们还需要额外的控制,比如过期时间,这个属性只是在身份验证阶段来判断是否过期,在我们(如Controller.Action中)使用用户标识的时候并不需要此字段,类似的额外字段根据不同的身份验证方式可能有很多,因此定义了“用户票证”这个概念,它包含 用户标识 + 身份验证过程中需要的额外属性(如得到用户标识的时间、过期时间等)
身份验证处理器AuthenticationHandler
参考上面的用户名密码+cookie身份验证流程我们发现有几个核心的处理步骤:
- 在登录时验证通过后将用户标识加密后存储到cookie,SignIn
- 当用户注销时,需要清楚代表用户标识的cookie,SignOut
- 在登录时从请求中获取用户标识,Authenticate
- 在用户未登录访问受保护的资源时,我们希望跳转到到登录页,Challenge
- Challenge叫做质询/挑战,意思是当发现没有从当前请求中发现用户标识是希望怎么办,可能是跳转到登录页,也可能是直接响应401,或者跳转到第三方(如QQ、微信)的登录页
- 因为某种原因(如权限验证不过),阻止方案,Forbid
身份验证处理器就是用来跟身份验证相关的步骤的,这些步骤在系统的不同地方来调用(比如在登录页对于的Action、在请求抵达时、在授权中间件中),
每个调用时都可以指定使用哪种身份验证方案,如果不提供将使用默认方案来做对应的操作。
不同的身份验证方式有不同的实现
IAuthenticationHandler接口只定义了最核心的几个步骤:Authenticate()、Challenge()、Forbid()。登录和注销这两个步骤定义了对应的子接口。当然微软还为我们定义了抽象类,参考
某个具体的身份验证方案的选项AuthenticationSchemeOptions
在上述身份验证处理的多个步骤中会用到一些选项数据,比如基于cookie的身份验证 cookeName、有效时长、再比如从请求时从cookie中解析得到用户标识后回调选项中的某个回调函数,允许我们的代码向调试中添加额外数据,或者干脆替换整个标识。
所以身份验证选项用来允许我们控制AuthenticationHandler的执行。不同的身份验证方式有不同的选项对象,它们直接或间接继承AuthenticationSchemeOptions
身份验证方案AuthenticationScheme
总结性的说:身份验证方案 = 名称 + 身份验证处理器类型,暂时可以理解一种身份验证方式 对应 一个身份验证方案,比如:
基于用户名密码+cookie的身份验证方式 对应的 身份验证方案为:new AuthenticationScheme("UIDPWDCookie",typeof(CookieAuthenticationHandler))
基于用户名密码+token 的身份验证方式 对应的 身份验证方案为:new AuthenticationScheme("JwtBearer",typeof(JwtBearerHandler))
身份验证方案在程序启动阶段配置,启动后形成一个身份验证方案列表。
程序运行阶段从这个列表中取出指定方案,得到对应的处理器类型,然后创建它,最后调用这个处理器做相应处理
比如登录操作的Action中xxx.SignIn("方案名") > 通过方案名找到方案从而得到对应的处理器类型 > 创建处理器 > 调用其SignIn方法
一种特殊的情况可能多种方案使用同一个身份验证处理器类型,这个后续的集成第三方登录来说
方案、处理器、选项、三者之间的关系
简单但不准确的理解为:方案名+处理器+选项 = 身份验证方式
身份验证方案的容器AuthenticationSchemeProvider
身份验证方案的容器(Dictionary<方案名,身份验证方案>)
默认是单例形式注册到依赖注入容器的
在应用启动时通过AuthenticationOptions添加的各种身份验证方案会被存储到这个容器中
各种GetDefaultXXX用来获取针对特定步骤的默认方案,如:GetDefaultAuthenticateSchemeAsync中间件从请求获取用户标识时用来获取针对此步骤的默认方案、GetDefaultSignInSchemeAsync获取默认用来登录的方案、GetDefaultSignOutSchemeAsync...等等,身份验证的不同步骤可以设置不同的默认方案。如果针对单独的步骤没有设置默认方案,则自动尝试获取总的默认方案,通过AuthenticationOptions设置这些默认值
身份验证过程中各个步骤都会通过此对象拿到指定方案,并通过关联的身份验证类型获得最终身份验证处理器,然后做相应处理
整个应用的身份验证选项AuthenticationOptions
AuthenticationSchemeBuilder的属性跟AuthenticationScheme几乎是一样对应的,它的Build()方法会生成一个AuthenticationScheme,所以我们可以理解为AuthenticationSchemeBuilder = AuthenticationScheme
上面说的AuthenticationSchemeOptions是指某个具体身份验证方案的选项,不同身份验证方案有对应的子类,比如:CookieAuthenticationOptions、JwtBearerOptions.. 它们都是AuthenticationSchemeOptions的子类。AuthenticationOptions则是针对整个身份验证功能的选项对象,我们需要在应用启动阶段通过它来配置身份验证功能。可以把它理解为IDictionary<方案名, AuthenticationSchemeBuilder>(方案配置容器) + 一些默认值设置。
在应用启动阶段(Startup.ConfigreService)多次调用 AddScheme以添加身份验证方案。
public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder) { var builder = new AuthenticationSchemeBuilder(name); configureBuilder(builder); _schemes.Add(builder); }
name方案名;configureBuilder允许我们提供委托对方案进行配置
添加的这些方案最终会被存储到AuthenticationSchemeProvider供其使用
另外DefaultAuthenticateScheme、DefaultSignInScheme、DefaultSignOutScheme..看名字也晓得它是说当我们调用某个步骤未指定使用那个方案是的默认选择
身份验证处理器工厂AuthenticationHandlerProvider
它是以Scope的形式注册到依赖注入容器的,所以每次请求都会创建一个实例对象。
唯一方法GetHandlerAsync从AuthenticationSchemeProvider获取指定身份验证方案,然后通过方案关联的AuthenticationHandler Type从依赖注入容器中获取AuthenticationHandler ,获取的AuthenticationHandler会被缓存,这样同一个请求的后续调用直接从缓存中拿。
所以也可以把它理解为AuthenticationHandler的运行时容器或工厂
AuthenticationService就是通过它来得到AuthenticationHandler然后完成身份验证各种功能的
身份验证服务AuthenticationService
身份验证中的步骤是在多个地方被调用的,身份验证中间件、授权中间件、登录的Action(如:AccountController.SignIn())、注销的Action(如:AccountController.SignOut()),身份验证的核心方法定义在这个类中,但它本质上还是去找到对应的身份验证处理器并调用其同名方法。其实这些方法还进一步以扩展方法的形式定义到HttpContext上了。以SignIn方法为例
HttpContext.SignIn() > AuthenticationService.SignIn() > AuthenticationHandler.SignIn()
后续
这一篇只尽量简单的说了下身份验证涉及到的几个核心概念,如果不明白的可以留言或等到下篇结合理解。下一篇将以用户名密码+cookie的身份验证方式来详细梳理下流程。