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

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

身份验证与受权

  之前写过一篇asp.net identity的文章( https://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仍是拥有权限可进入资源访问(参考:   https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs   https://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它表明一个用户信息属性,而且一些属性名称是有相关定义的,具体参考: https://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)的、基于策略的受权机制,具体使用方式参考文档: https://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仍要对钥匙进行验证,看清楚钥匙上贴了张三的名字,但颇有可能这把钥匙是李四拿着。
 
参考:
以及文章中涉及的相关源代码
相关文章
相关标签/搜索