从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权

  OpenIDConnect是一个身份验证服务,而Oauth2.0是一个授权框架,在前面几篇文章里通过IdentityServer4实现了基于Oauth2.0的客户端证书(Client_Credentials)、用户名密码(Password)的授权流程,同时也实现OpenIDConnect的授权码(Authorization Code)、隐式流程(Implicit)的身份验证。
  ???啥?一会儿是授权一会儿是身份验证,身份验证与授权傻傻分不清楚??本文就来聊一聊Asp.net core中的身份验证与授权。
  本文主要内容有:

身份验证与授权

  以前写过一篇asp.net identity的文章(//www.cnblogs.com/selimsong/p/7828326.html)已经提到过身份验证与授权的概念,简单来说身份验证就是“是谁”的问题,而授权就是“能不能”的问题,一般来说首先需要知道“是谁”,然后再判断“能不能”。
  这里举个生活中常见的小栗子,锁是门用来保护门内财产的工具,而随着科技发展现在有了指纹锁,指纹锁的特征是它既可以通过指纹来开锁,也可以通过钥匙开锁,对于指纹开锁时首先需要录入指纹并指定一个指纹身份,比如保姆阿姨,首先需要的就是给她录入指纹,然后允许该指纹在上午6点至晚上10点可以开门,那么最终保姆阿姨在开门时,授权识别指纹,通过指纹匹配到或者说知道是保姆,这里就是身份验证,如果陌生人进行指纹匹配那么将匹配不到任何身份,但是能否开门还得根据设定的规则,那就是开门时间是否在规定的时间范围内,满足条件才能开门,这就是授权
  当然在开门这个问题上还有一个Bug,那就是钥匙,只要拥有钥匙,不管是谁都能开门,获得钥匙就是获得授权
  在软件系统中通常使用的用户名密码登录实际上就是身份验证功能,用户登录后系统就记住这一状态,后续访问系统时系统就知道“是谁”在访问系统,然后因为已经知道是谁,那么就可以根据具体访问条件来判断用户“能不能”访问资源,这就是授权。

Asp.net core中的身份验证与授权

  首先需要再次明确一下Asp.net core是一个Web框架,它本身就具有一些特性,这其中就包括了身份验证和授权。
  在Asp.net core中的身份验证和授权是通过中间件完成的,而把一个中间件添加到asp.net core的应用程序中一般只需要两个步骤,第一是对相关中间件所需参数及服务进行配置,第二就是将相应的中间件添加到请求管道中即可。
  下图为基于OpenIDConnect客户端程序的身份验证配置:
  
  下图为基于OpenIDConnect客户端程序的身份验证及授权中间件配置:
  
  以上代码并没有额外的配置授权策略,但是可以通过Authorize特性来提供最基础的授权(授权通过身份验证的用户)。另外需要注意的是Authorize特性是需要搭配Authorization中间件来使用的,如下图所示:
  
  另外基于Identity组件的身份验证代码中没有出现AddAuthentication及AddCookie方法,而是通过AddDefaultIdentity就可以完成身份验证,是因为AddDefaultIdentity方法中包含了相关方法调用:
  
  AddDefaultIndeity方法代码:
  完成配置后就可以在应用程序中使用身份验证及授权功能了。
  关于asp.net core官方提供的身份验证方式,我们可以直接看看GitHub上的代码:
  
  从图中可以看到有基于Cookie、Jwt Bearer、Oauth、OpenIdConnect也有基于Facebook、Google、MicrosoftAccount、Twitter的,如果非官方的话应该还能找到基于微信、支付宝等账号的登录开源库。
  总的来说asp.net core的身份验证可以支持现有的大部分常用方式或协议,同时也支持第三方的账户登录。

Asp.net core身份验证及授权的基本原理

Scheme与身份验证处理器

  Scheme和处理器可以简单的理解为一个键值对,处理器是用于实际处理身份验证逻辑的代码,Scheme就是这个处理器的标识,通过Scheme可以直接获取到相应的处理器,然后通过处理器来完成身份验证。
  Scheme是一个重要的概念,因为在asp.net core中它可以添加多个身份验证处理器,在Asp.net版本中,或者准确来讲Owin中我们就提到过一个多重身份验证的概念(ASP.NET没有魔法——ASP.NET Identity 的“多重”身份验证)实际上也就是在一个应用里面添加了多个身份验证处理器,换句话说就是一个应用程序支持多种身份验证(登录)方式。asp.net core中管理多个身份验证处理器的核心就是基于Scheme,还记得本文上面oidc验证添加的服务配置代码吗。
  
  在这段代码中设置了身份验证的默认Scheme以及默认ChallengeScheme,关于Scheme的作用请往下看。
  注:asp.net 与asp.net core中的身份验证机制有共同点也有区别,总体来说asp.net core基于scheme的身份验证管理机制逻辑上和性能上会更好(毕竟是最新的产物)。
  关于身份验证处理器,它实际上就是一个实现IAuthenticationHandler接口的类型,它提供了身份验证所需的具体实现逻辑:
  

三个方法Authenticate、Challenge、Forbid

  这三个方法是asp.net core身份验证/授权中的基础,它们分别代表身份验证、质疑和禁止,每一个身份验证处理器都需要实现这三个方法,下面简单介绍一下这三个方法:
  Authenticate:
  • 身份验证调用和核心逻辑,换句话就是证明“是谁”的方法。
  • 拟人化来说就是检查身份证同时与持有人是否匹配的过程。
  • 在程序中就是检查cookie、jwt token、id token等是否有效,以及信息载体中标记的用户“是谁”
  Challenge:
  • 可翻译为“怀疑/质疑”,实际上就是身份验证没有成功后调用的方法。
  • 拟人化来说就是“我”不知道你“是谁”,但“我”需要知道,所以“我”会问“你是谁?把你的身份证给我看一下?”
  • 在程序中一般的过程就是重定向到登录页面,通过登录方式告诉系统“是谁”。对于Api一类没有UI的程序时,就返回401状态码告知未通过身份验证。
  Forbid:
  • 这个方法用于授权,授权失败时调用该方法。
  • 这个方法相对简单,当程序存在UI时,通过UI告知用户无权限禁止访问即可,对于Api一类没有UI的程序时,通过返回403状态码告知无权限。

两个中间件AuthenticationMiddleware、AuthorizationMiddleware

  身份验证中间件(AuthenticationMiddleware),只做三件事:
  1. 处理身份验证请求,如oidc的由身份验证服务器完成id_token生成跳转的/signi-oidc。
  2. 处理默认scheme的身份验证流程。
  3. 如果身份验证通过后将验证结果的主体信息(Principal)放到HttpContext中
  
  授权中间件(AuthorizationMiddleware)主要是通过一系列终结点授权信息获取、执行后根据授权执行结果来决定是challenge、forbidden还是拥有权限可进入资源访问(参考:  //github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs  //github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddlewareResultHandler.cs):
  
  注:如果所访问的资源没有授权相关的限制,那么请求将跳过授权步骤直接往下访问。

三个对象HttpContext、ClaimsIdentity、AuthenticationProperties

  首先我们来看看ClaimsIdentity,它实际上是一组Claim的集合,每个Claim代表用户身份的一个属性的键值对,一组Claim可以表示某一方面的用户信息特性,除此之外它还包含是否通过验证(IsAuthenticated)以及验证方式(AuthenticationType)等信息。
  下图为通过oidc身份验证的ClaimsIdentity信息,HttpConext对象中包含的User是ClaimsPrincipal(声明的主体),一个主体里面包含多个ClaimsIdentity信息:
  
  这里可以这么理解这些对象:
  1. 我们每个人都有身份证、护照、户口册、驾照等可以证明我们身份的东西,这相当于一个ClaimsPrincipal可以拥有多个ClaimsIdentity。
  2. 身份证上面有姓名、身份证号等属性,相当于一个ClaimsIdentity包含多个Claim。
  3. 关于Claim它代表一个用户信息属性,并且一些属性名称是有相关定义的,具体参考://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes?view=net-5.0
  4. 每个身份证明它的识别方法不一样,比如身份证可以通过身份证识别器识别、户口册可以在公安局识别,这个相当于每个ClaimsIdentity中的AuthenticationType。
 
  AuthenticationProperties:它是一个用来存储身份验证会话数据的字典,oidc流程中IdentityServer返回的Id_token及access_token等信息就是存储到AuthenticationProperties中。
 
  HttpContext:Http上下文对象,是整个请求的核心,包含了Http请求及响应的所有内容,但是在身份验证/授权方面,它有另一个角色——身份验证服务代理,通过HttpContext我们可以调用身份验证服务的相关方法,包括身份验证和授权中间件的Challenge等方法调用都是通过HttpContext完成的。
  下图为HttpContext在Authentication命名空间下的拓展方法定义:
  
  下图为IAuthenticationService的方法定义,HttpContext通过容器获取IAuthenticationService的实例进行调用,而IAuthenticationService最终实际上调用的是指定或默认身份验证处理器的相关方法:
  

Signin与身份信息载体

  前面文章详细讲解了身份验证的相关细节,但唯独没说的就是登录。登录到底是做了什么事情?在了解登录之前我们先来了解一个概念“身份信息载体”,其实也就字面意思,承载身份信息的物体,在现实生活中我们的身份信息载体是“身份证”等等实际物品,而在信息系统中信息载体就是一段数据,这段数据为了能让相关程序或者广大程序所理解,它应该按照具体的协议来创建,信息系统中常用的身份信息载体有Cookie以及Jwt(Json web token)。
  Cookie:
  我们都知道http是一个无状态协议,但是大部分时候我们需要它“有”状态,Cookie作为一项浏览器数据存储技术,它经常用于存储一些状态信息,用于下一次发起请求的时候服务器能够了解当前请求的状态。所以Cookie非常适合作为身份信息载体,当然asp.net core的基于Cookie身份验证是这样做的,将用户信息(ClaimsIdentity)加密后存储到Cookie中,下次从Cookie中获取数据,解密后获得用户信息并完成身份验证。
  Jwt:
  Jwt是一种基于Json的安全信息传输标准,Jwt因为带有数字签名的,可以保证数据完整性,就想我们的身份证一样不能伪造,所以也很适合作为身份信息载体。
 
  Cookie和Jwt各有特点,可适用于不同的应用场景,如Cookie它本身有域特性,现在的单页应用程序它会存在跨域问题,而Jwt虽然能保证数据完整,但是它本身不是加密的(但是传输过程可以加密,并且生产一般必须加密,如https),所以Jwt中的身份信息很容易泄漏,所以它比较适合更封闭的客户端,如服务端与服务端通讯、手机App等。
 
  现在我们再回来聊登录,登录实际上就是将身份信息写到身份信息载体的过程。基于Cookie的就写Cookie,基于Jwt的就颁发Jwt,但是需要注意的是一般jwt由第三方身份验证服务器颁发,所以应用程序本身是不需要关注的,所以这里主要讲讲基于Cookie的登录。
  下面我们做一个基于Cookie登录的小实验,首先做一个简单的基于Identity的登录功能:
  
  设置断点后,直接访问登录页面进行登录,在登录信息提交后我们可以看到User信息是空的:
  
  登录之后仍然没有用户信息:
  
  但是在ResponseHeader的HeaderSetCookie信息中我们找到了如下信息:
  看到它即将写入cookie中带有它创建的身份信息载体。这个就是登录生成身份信息载体的过程,至于登陆后即可访问保护内容,是因为登录完成后做了跳转,跳转后将携带身份信息发起请求后既可以完成身份验证,从而可以访问受保护内容。
  注:Identity提供的登录功能最终也是通过HttpContext的拓展方法通过IAuthenticationService来完成的,具体可参考相关源码,这里不在赘述。

自主登录与外部登录

  自主登录指的是应用程序本身提供了用户身份核对(用户名+密码登录),然后拥有用户信息自主权(应用程序保存了与用户相关的信息),最后根据用户信息来生成用户信息载体的登录方式。如Asp.net core Identity提供的就是一种自主登录方式。
  外部登录指的是由第三方程序来对用户身份核对,并提供相关用户信息交由程序本身来生成用户信息载体的,或者直接由第三方程序生成用户信息载体的方式。
  如本系列文章介绍的oidc的身份验证就是由IdentityServer提供用户身份核对并提供用户信息(UserInfo EndPoint),然后交由客户端程序来生成身份信息载体Cookie。
  而如果通过IdentityServer直接通过Oauth2.0流程获得Access Token的方式就相当于由第三方程序生成用户信息载体,客户端直接验证用户信息载体即可完成后续的身份验证。

Asp.net core身份验证及授权流程

  前面内容详细介绍了Asp.net core身份验证相关的一些基础原理,下面就通过一个流程图来介绍一下完整的身份验证和授权流程:
  
  从图中我们可以找到3个主体分别是:浏览器、Asp.net core应用程序以及第三方验证服务。
  整个流程的开始可能是通过访问受保护资源、自主登录系统或者外部登录系统开始,但是登录的目的在于访问受保护资源,下面就简单对访问受保护资源流程进行梳理:
  1. 浏览器发起受保护资源访问请求(没有Cookie).
  2. 服务器对请求进行身份验证,因为没有Cookie返回一个失败结果。
  3. 因为验证结果为失败,所以没有ClamsIdentity信息,赋值到HttpContext.User也为空。
  4. 进行授权判断,因为没有经过身份验证,所以调用质疑操作(Challenge),由默认的ChallengeScheme决定是自主登录还是外部登录。
  5. 如果是自主登录,那么跳转到应用登录页面完成登录,并根据用户信息生成ClaimsIdentity。
  5. 如果是外部登录,那么跳转到第三方登录页面完成登录,并回到自主应用的回调地址对第三方返回的code、id_token及access_token进行处理,并获取用户信息,根据获取的用户信息生成ClaimsIdentity。
  6. 系统将ClaimsIdentity信息生成身份信息载体(Cookie)并重定向回之前访问的资源。
  7. 重定向后携带身份信息载体访问受保护资源,如果用户有权限,那么可访问资源,如果没有权限返回403禁止访问。
 
  小提示:为什么asp.net core identity生成的UI代码中,外部登录执行的核心代码为ChallegeResult + (provider 和returnUrl)?
   

Asp.net core中的授权

  前面详细介绍了Asp.net core中的身份验证,授权仅仅是其中的一环来帮助完成身份验证。那么Asp.net core中提供了哪些授权机制或者说要如何进行授权呢?
  Asp.net core及Identity组件提供了简单的(只要通过身份验证)、基于角色的、基于声明(Claim)的、基于策略的授权机制,具体使用方式参考文档://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-5.0
  另外还给了一个如何实现数据增删改权限控制的例子:
  上面这个例子告诉我们授权机制不仅仅局限于授权特性和中间件,我们可以把授权机制融入到我们的业务逻辑中。

小结

  本篇文章从Asp.Net core介绍了身份验证和授权的基本概念和原理,通过流程图的方式展现了Asp.net core身份验证和授权的流程,最后简单介绍了授权的相关机制。
  现在我们回到文章开头问的问题为什么IdentityServer4提供的功能中一会儿是身份验证,一会儿是授权??
  这个问题需要根据主体来看,首先我们看Oauth2.0,它的最终结果是一个Jwt的Bearer Token,这相当于给了你一把钥匙,使用这个钥匙你可以打开指定的门,所以它是一个授权。
  然后来看看OIDC的授权码流程,它除了Access Token外实际上关键的是Id_token,证实了用户的身份,这相当于告诉你,用户是保姆阿姨,解决了“是谁”的问题,所以是身份验证。知道了是谁,至于开不开门,那是你(客户端程序)的授权问题。
  最后来看看Asp.net core应用程序,在Asp.net core应用程序中不存在独立的授权,换句话就是没法单独使用授权功能,需要身份验证和授权功能联合使用,比如Oauth给了一把钥匙,但是Asp.net core仍要对钥匙进行验证,看清楚钥匙上贴了张三的名字,但很有可能这把钥匙是李四拿着。
 
参考:
以及文章中涉及的相关源代码