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

时间:2022-08-28 23:49:52
  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客户端程序的身份验证配置:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  下图为基于OpenIDConnect客户端程序的身份验证及授权中间件配置:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  以上代码并没有额外的配置授权策略,但是可以通过Authorize特性来提供最基础的授权(授权通过身份验证的用户)。另外需要注意的是Authorize特性是需要搭配Authorization中间件来使用的,如下图所示:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  另外基于Identity组件的身份验证代码中没有出现AddAuthentication及AddCookie方法,而是通过AddDefaultIdentity就可以完成身份验证,是因为AddDefaultIdentity方法中包含了相关方法调用:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  AddDefaultIndeity方法代码:
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  完成配置后就可以在应用程序中使用身份验证及授权功能了。
  关于asp.net core官方提供的身份验证方式,我们可以直接看看GitHub上的代码:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  从图中可以看到有基于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验证添加的服务配置代码吗。
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  在这段代码中设置了身份验证的默认Scheme以及默认ChallengeScheme,关于Scheme的作用请往下看。
  注:asp.net 与asp.net core中的身份验证机制有共同点也有区别,总体来说asp.net core基于scheme的身份验证管理机制逻辑上和性能上会更好(毕竟是最新的产物)。
  关于身份验证处理器,它实际上就是一个实现IAuthenticationHandler接口的类型,它提供了身份验证所需的具体实现逻辑:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权

三个方法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中
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  授权中间件(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):
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  注:如果所访问的资源没有授权相关的限制,那么请求将跳过授权步骤直接往下访问。

三个对象HttpContext、ClaimsIdentity、AuthenticationProperties

  首先我们来看看ClaimsIdentity,它实际上是一组Claim的集合,每个Claim代表用户身份的一个属性的键值对,一组Claim可以表示某一方面的用户信息特性,除此之外它还包含是否通过验证(IsAuthenticated)以及验证方式(AuthenticationType)等信息。
  下图为通过oidc身份验证的ClaimsIdentity信息,HttpConext对象中包含的User是ClaimsPrincipal(声明的主体),一个主体里面包含多个ClaimsIdentity信息:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  这里可以这么理解这些对象:
  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命名空间下的拓展方法定义:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  下图为IAuthenticationService的方法定义,HttpContext通过容器获取IAuthenticationService的实例进行调用,而IAuthenticationService最终实际上调用的是指定或默认身份验证处理器的相关方法:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权

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的登录功能:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  设置断点后,直接访问登录页面进行登录,在登录信息提交后我们可以看到User信息是空的:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  登录之后仍然没有用户信息:
  从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  但是在ResponseHeader的HeaderSetCookie信息中我们找到了如下信息:
从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
  看到它即将写入cookie中带有它创建的身份信息载体。这个就是登录生成身份信息载体的过程,至于登陆后即可访问保护内容,是因为登录完成后做了跳转,跳转后将携带身份信息发起请求后既可以完成身份验证,从而可以访问受保护内容。
  注:Identity提供的登录功能最终也是通过HttpContext的拓展方法通过IAuthenticationService来完成的,具体可参考相关源码,这里不在赘述。

自主登录与外部登录

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

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

  前面内容详细介绍了Asp.net core身份验证相关的一些基础原理,下面就通过一个流程图来介绍一下完整的身份验证和授权流程:
  从零搭建一个IdentityServer——聊聊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)?
   从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权

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仍要对钥匙进行验证,看清楚钥匙上贴了张三的名字,但很有可能这把钥匙是李四拿着。
 
参考:
以及文章中涉及的相关源代码

从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权的更多相关文章

  1. 从零搭建一个IdentityServer——集成Asp.net core Identity

    前面的文章使用Asp.net core 5.0以及IdentityServer4搭建了一个基础的验证服务器,并实现了基于客户端证书的Oauth2.0授权流程,以及通过access token访问被保护 ...

  2. 从零搭建一个IdentityServer——目录(更新中...)

    从零搭建一个IdentityServer--项目搭建 从零搭建一个IdentityServer--集成Asp.net core Identity 从零搭建一个IdentityServer--初识Ope ...

  3. asp.net core 3.x 身份验证-3cookie身份验证原理

    概述 上两篇(asp.net core 3.x 身份验证-1涉及到的概念.asp.net core 3.x 身份验证-2启动阶段的配置)介绍了身份验证相关概念以及启动阶段的配置,本篇以cookie身份 ...

  4. 从零搭建一个IdentityServer——会话管理与登出

    在上一篇文章中我们介绍了单页应用是如何使用IdentityServer完成身份验证的,并且在讲到静默登录以及会话监听的时候都提到会话(Session)这一概念,会话指的是用户与系统之间交互过程,反过来 ...

  5. 从零搭建一个IdentityServer——资源与访问控制

    IdentityServer作为授权服务器它的最终目的是用于对资源进行管控,这里所说的资源有两种,其一是API资源,实际上也就是OIDC协议中客户端(RP)所需要访问的一系列受保护的资源(API),授 ...

  6. 从零搭建一个IdentityServer——单页应用身份验证

    上一篇文章我们介绍了Asp.net core中身份验证的相关内容,并通过下图描述了身份验证及授权的流程: 注:改流程图进行过修改,第三方用户名密码登陆后并不是直接获得code/id_token/acc ...

  7. 从零搭建一个IdentityServer——项目搭建

    本篇文章是基于ASP.NET CORE 5.0以及IdentityServer4的IdentityServer搭建,为什么要从零搭建呢?IdentityServer4本身就有很多模板可以直接生成一个可 ...

  8. 从零搭建一个IdentityServer——初识OpenIDConnect

    上一篇文章实现了IdentityServer4与Asp.net core Identity的集成,可以使用通过identity注册功能添加的用户,以Password的方式获取Access token, ...

  9. 聊聊ASP.NET Core中的配置

    ​作为软件开发人员,我们当然喜欢一些可配置选项,尤其是当它允许我们改变应用程序的行为而无需修改或编译我们的应用程序时.无论你是使用新的还是旧的.NET时,可能希望利用json文件的配置.在这篇文章中, ...

随机推荐

  1. 最好的.NET开源免费ZIP库DotNetZip(.NET组件介绍之三)

    在项目开发中,除了对数据的展示更多的就是对文件的相关操作,例如文件的创建和删除,以及文件的压缩和解压.文件压缩的好处有很多,主要就是在文件传输的方面,文件压缩的好处就不需要赘述,因为无论是开发者,还是 ...

  2. height:100% 布局

    常常会碰到需要填满整个浏览器,并且自适应高度的需求.首先肯定会想到给容器设定height:100%,但是会没有效果.原因是body没有高度,所以百分比无法生效. 解决方案:给html,body,标签都 ...

  3. Java EE开发平台随手记4——Mybatis扩展3

    接着昨天的Mybatis扩展——IDaoTemplate接口. 扩展9:批量执行 1.明确什么是批量执行 首先说明一下,这里的批量执行不是利用<foreach>标签生成一长串的sql字符串 ...

  4. Android中的SQLite使用学习

    Android中的SQLite使用学习 SQLite是非常流行的嵌入式关系型数据库,轻载, 速度快,而且是开源.在Android中,runtime提供SQLite,所以我们可以使用SQLite,而且是 ...

  5. C primer plus 读书笔记第六章和第七章

    这两章的标题是C控制语句:循环以及C控制语句:分支和跳转.之所以一起讲,是因为这两章内容都是讲控制语句. 第六章的第一段示例代码 /* summing.c --对用户输入的整数求和 */ #inclu ...

  6. android 1&period;6 launcher研究之自定义ViewGroup &lpar;转 2011&period;06&period;03(二)——— android 1&period;6 launcher研究之自定义ViewGroup &rpar;

    2011.06.03(2)——— android 1.6 launcher研究之自定义ViewGroup2011.06.03(2)——— android 1.6 launcher研究之自定义ViewG ...

  7. MySQL索引统计信息更新相关的参数

    MySQL统计信息相关的参数: 1. innodb_stats_on_metadata(是否自动更新统计信息),MySQL 5.7中默认为关闭状态 仅在统计信息配置为非持久化的时候生效. 也就是说在i ...

  8. SharePoint REST API - 使用REST API和jQuery上传一个文件

    博客地址:http://blog.csdn.net/FoxDave 本篇主要通过两个代码示例来展示如何应用REST API和jQuery上传文件到SharePoint. 示例会使用REST接口和j ...

  9. PAM unable to dlopen&lpar;&sol;lib&sol;security&sol;pam&lowbar;limits&period;so&rpar;&colon; &sol;lib&sol;security&sol;pam&lowbar;limits&period;so&colon; wrong ELF class&colon; ELFCLASS32

    systemctl status sshd● sshd.service - OpenSSH server daemon Loaded: loaded (/usr/lib/systemd/system/ ...

  10. 易混点总结--JS

    1.defer与 async 的区别是: defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行:async一旦下载完,渲染引擎就会中断渲染,执行这个脚本 ...