ASP.NET没有魔法——ASP.NET MVC使用Oauth2.0实现身份验证
随着软件的不断发展,出现了更多的身份验证使用场景,除了典型的服务器与客户端之间的身份验证外还有,如服务与服务之间的(如微服务架构)、服务器与多种客户端的(如PC、移动、Web等),甚至还有需要以服务的形式开放给第三方的,身份验证这一功能已经演化为一个服务,很多大型应用中都有自己的身份验证服务器甚至集群,所以普通的身份验证方式已经不能满足需求。
在.Net领域中也有一些开源的身份验证服务器组件,如IdentityServer(http://identityserver.io/),但是这些组件对于一些规模较小的项目来说可能会感觉到比较庞大,增加了学习和维护成本,所以本章将对OAuth以及如何使用OAuth实现身份验证模式进行介绍。
本章的主要内容有:
● OAuth2.0简介
● 在.Net中使用OAuth实现基于授权码模式的身份验证
● 实现基于Access Token的身份验证
● 加入Refresh Token支持
● 实现通过用户密码模式获取Access Token
● 实现客户端模式获取Access Token
● 关于.Net中OAuth相关令牌的加密说明
注:本章内容源码下载:https://files.cnblogs.com/files/selimsong/OAuth2Demo.zip
OAuth2.0简介
在文章的开始的时候说过现代软件应用的身份验证场景越来越丰富,下图是现代应用程序的一个通信图,它描述了常见的“客户端”是如何与服务器提供的服务通信的。

该图出自IdentityServer:https://identityserver4.readthedocs.io/en/release/intro/big_picture.html
为了满足这些场景人们制定了一套标准协议,这个协议就是OAuth(Open Authorization,开放授权)协议,OAuth能够让第三方应用程序去访问受限制的HTTP服务。OAuth有两个版本分别是1.0和2.0,但是由于1.0版本过于复杂所以1.0版本被2.0版本替换了,并且两个版本是不兼容的。
接下来就对OAuth2.0相关的概念进行介绍:
1. OAuth2.0中的角色
● Resource Owner:资源拥有者,就是能够访问被限制资源的用户(注:这里的用户是个泛指,它既可以是真实用户也可以是服务程序)。
● Resource Server:资源宿主,能够接受和处理,使用访问令牌(access token)访问受保护资源的请求(如提供API的服务器)。
● Client:它泛指所有的第三方程序(无论是Web应用、桌面应用还是服务端应用),它通过资源拥有者以及它的授权来访问受保护的资源。
● Authorization Server:用来对授权成功的客户端发布令牌以及对令牌的验证授权。并对Client进行管理。
2.OAuth2.0的协议流程

A. 第三方程序向资源拥有者(用户)发送授权请求,这个过程既可以通过客户端直接向用户请求,也可以通过授权服务器作为中介来完成请求。(注:对于授权请求这个概念相当于用户登录,应用程序可以直接显示一个登录页面,也可以跳转到验证服务器的统一登录页面)
B. 用户将授权相关信息“提交”给第三方程序,在OAuth中有4种不同的权限授予方式,每种方式需要的数据不同,如基于用户密码的授权方式就需要用户名和密码。
C. 第三方程序将用户的授权信息提交到授权服务器,请求一个Access Token。
D. 授权服务器验证完成用户的授权信息后,将Access Token发放到第三方程序。
E. 第三方程序携带Access Token访问被保护的资源。
F. 资源服务器验证Access Token有效后,将资源返回到第三方程序。
3. OAuth中的授权模式(即获取Access Token的方式)
● Authorization Code(授权码模式):该模式的核心是客户端通过一个授权码来向授权服务器申请Access Token。是一种基于重定向的授权模式,授权服务器作为用户和第三方应用(Client)的中介,当用户访问第三方应用是,第三方应用跳转到授权服务器引导用户完成身份验证,生成Authorization Code并转交到第三方应用,以便于第三方应用根据这个授权码完成后续的Access Token获取。
● Implicit(简化模式):简化模式是一种简化的授权码模式,授权码模式在首次访问第三方应用时跳转到授权服务器进行身份验证返回授权码,而简化模式在跳转到授权服务器后直接返回Access Token,这种模式减少了获取Access Token的请求次数。
● Resource Owner Password Credentials(用户密码模式):通过资源拥有者(用户)的用户名和密码来直接获取Access Token的一种方法,这种方法要求第三方应用(Client)是高度可信任的,并且其它授权方式不可用的情况下使用。
● Client Credentials(客户端模式):该模式是通过第三方应用(Client)发送一个自己的凭证到授权服务器获得Access Token,这种模式的使用要求该Client已经被授权服务器管理并限制其对被保护资源的访问范围。另外这种模式下Client应该就是一个资源拥有者(用户),如微服务程序。
4. Access Token & Refresh Token
这个很好理解,第三方应用通过Access Token去获取受保护的资源,但是Access Token是存在有效期的,一旦过期就无法使用,为了避免Access Token过期后无法使用,所以加入了Refresh Token的概念,通过刷新的方式来完成Access Token的更新。
5. Client的注册
在OAuth2.0中,所有需要访问受限资源的程序都视为第三方应用(Client),为了保证这个Client是安全的、可信任的,所以OAuth需要对Client进行管理。参考:https://tools.ietf.org/html/rfc6749#section-2
6. OAuth的终结点
这里终结点代表的是HTTP资源,在OAuth授权过程中需要使用到一些终结点的支持,如Authorization code(授权码)的获取,以及Access Token的获取,终结点由授权服务器提供。参考:https://tools.ietf.org/html/rfc6749#section-3
7. Access Token Type
Access Token的类型是让Client根据具体类型来使用Access Token完成对受保护资源的请求。
OAuth2.0中有两种类型分别是Bearer和Mac,它们体现方式如下:
● Bearer:

● Mac:

参考:https://tools.ietf.org/html/rfc6750
在.Net中使用OAuth实现基于授权码模式的身份验证
OAuth2.0是一个开放标准,既然是标准那么就可以有实现,在.Net中微软基于Owin实现了OAuth2.0协议,下面就介绍如何在ASP.NET MVC程序中实现OAuth身份验证。
注:本例基于ASP.NET MVC默认带身份验证模板完成。
1. 组件安装
通过NuGet安装Microsoft.Owin.Security.OAuth组件:

注:从该组件的名称可以看出,.Net对OAuth的实现实际上是基于Owin的,所以很多内容均使用Owin中相关的身份验证概念,这些内容可参考本系列与身份验证的文章。
2. 添加OAuth授权服务器
根据上面OAuth的介绍可知,授权服务器是OAuth其中一个角色,该角色最主要的功能就是Access Token的发放以及授权,另外它还用于支持授权码模式的授权码发放以及Client的管理。
在Startup类型的Configuration方法中加入以下代码,该代码是为Owin中间件添加一个授权服务器(注:该中间件是一个Owin的身份验证中间件可参考《ASP.NET没有魔法——ASP.NET Identity 的“多重”身份验证》)。

其中OAuthAuthorizationServerOptions定义如下:

上面的定义可以分为以下几类:
● 终结点地址:AuthorizeEndpointPath、TokenEndpointPath等,它定义了访问获取授权码以及获取Token的地址信息。
● Token提供器:AuthorizationCodeProvider、AccessTokenProvider、RefreshTokenProvider负责完成对应令牌的创建和处理功能。
● Token的“加密”与“解密”:该功能是OAuth与Owin身份验证的结合,通过AccessTokenFormat等ISecureDataFormat接口的实现可以将对应的Token转换成一个AuthenticationTicket。可参考《ASP.NET没有魔法——ASP.NET Identity的加密与解密》文中TicketDataFormat的用法。
● OAuth授权服务:Provider是整个OAuth服务器的核心,它包含了终结点的处理与响应、OAuth中的4种Access Token授权方式和刷新令牌获取Access Token的方式以及请求、客户端的相关验证:

3. 为授权服务器添加终结点
上面介绍OAuth时介绍了终结点实际上就是用来获取授权码或者Access Token的,在.Net中使用Microsoft.Owin.Security.OAuth组件仅需要通过配置的形式就可以指定授权码及Token获取的终结点访问地址(注:把AllowInsecureHttp配置属性设为true,可以允许不安全的http来访问终结点,该配置仅用于开发环境):

完成后就可以通过浏览器访问这两个地址:


可以看到是可以访问,只不过是有错误的(注:请求地址的QueryString的参数参考文档)。
4. Client的管理与验证
Client在OAuth中指代了所有的第三方需要访问受限制资源的应用程序,授权服务器为了能够识别和验证Client所以需要完成Client的管理以及验证功能。(注:微软在Microsoft.Owin.Security.OAuth组件中仅仅提供了Client验证的接口,所以要自己实现Client数据的管理以及验证逻辑):
1). 添加Client实体以及对应的仓储(本例以内存的方式实现仓储,实际使用中至少应该保存数据库):

上图是Client最基础的属性(注:如果还需要对Client的访问范围进行限制,那么还应该加入一个Scope的列表,本例不再加入Scope的限制)。
2). Client的仓储:

3). 实现授权服务器对Client的验证:
由于授权服务器对客户端验证的接口位于OAuthAuthorizationServerProvider类型中,所以首先要继承该类型,并重载相应的验证方法:

上面代码做了以下几件事:
● 尝试从Http请求header或者请求body中获取Client信息,包含Id和密码。
● 如果没有Client的Id信息,那么直接判断为不通过验证,如果有Client的密码信息则保存到Owin上下文中,供后续处理使用。
● 使用获得的ClientId在Client仓储中查询,判断是否是一个合法的Client,如不是则判断为不通过验证。
4). 验证完成后设置该Client的重定向Url(注:该方法仍旧是重载OAuthAuthorizationServerProvider类型中的方法):

5. 添加授权码提供器
授权码的生成是授权服务器终结点的一项功能,当使用授权码模式时,用户访问Client会被引导跳转到授权服务器完成身份验证(登录),随后又携带授权码跳转回Client,Client使用该授权码获取Access Token。在OAuth的.Net实现中,需要通过在配置中配置一个类型为IAuthenticationTokenProvider的令牌提供器,该提供器用于创建和解析令牌,这里的创建实际就是用户完成登录后授权码的生成以及授权码和用户登录身份信息的关联,而解析实际就是根据授权码获得对应用户身份信息并生成Access Token的过程。
下面就通过实现IAuthenticationTokenProvider的方式实现一个自定义授权码提供器:

从上面代码可以看出这个提供器的核心功能是以Guid的方式生成一个键值(授权码)保存了当前用户的信息,当解析时通过该键值(即授权码)获取用户身份信息。(注:AuthenticationTokenCreateContext对象用于对当前用户身份信息AuthenticationTicket对的的序列化和反序列化)
完成后将该提供器配置到授权服务器中间件中:

6. 为授权服务器添加用户授权提示页面
当用户访问授权码终结点时理应让用户知道Client需要他的授权,为此在ASP.NET MVC程序中需要添加一个路由与授权码终结点地址匹配的Controller、Action以及View:
1). Controller及Action(注:该Action需要通过身份验证,如果没有需要跳转到登录页面完成身份验证后才可访问):

2). View:显示授权提示

7. 运行程序
1).访问授权码终结点获取授权码:http://localhost:59273/oauth2/authorize?response_type=code&client_id=test1
由于没有登录,所以先跳转到登录页面。

完成登录后跳转回授权页面:

点击授权按钮后,携带授权码跳转到test1这个client的重定向Url(注:此处test1这个Client设置的Url就是授权服务器本身,所以看上去没有做重定向)

得到授权码后,携带授权码访问Access Token终结点获取Access Token(注:这里使用Chrome浏览器的Postman拓展来实现请求的模拟):

注:上面响应信息中的access_token包含了加密后用户的身份信息,其加密过程可参考基于Cookie的用户信息加密过程。ASP.NET没有魔法——ASP.NET Identity的加密与解密
实现基于Access Token的身份验证
上面介绍了如何基于授权码模式获得Access Token,接下来将介绍如何使用Access Token来访问受限制的资源(注:本例中的资源服务器与授权服务器位于同一实例中,所以当资源服务器对access token解密时,能够保证与授权服务器用于生成access token所用密钥一致,能够正常解密,这里的Access Token和基于Cookie的身份验证中的身份验证Cookie性质是相同的,都是将用户的身份信息序列化后的加密字符串)
1. 在Startup类中添加基于Bearer的OAuth身份验证中间件:

2. 添加访问受限制的资源:

3. 访问受限资源:
未添加授权信息直接跳转到登录页面。

添加Access Token后可正常访问资源:

加入Refresh Token支持
上面使用授权码模式生成的Access Token是存在过期时间的(实际上无论什么方式生成的Access Token都存在过期时间),Token过期后又不可能让用户再授权一次,所以需要使用Refresh Token来定期刷新Access Token,.Net中实现Refresh Token的方式与授权码类似,在生成Refresh Token的同时会关联用户的身份信息,后续可以使用这个Refresh Token来生成新的Access Token。
1. 创建Refresh Token提供器(实现方式与授权码提供器基本一致):

2. 为授权服务器配置Refresh Token提供器:

3. 再次获取到授权码后,根据该授权码获取Access Token,返回信息中将携带Refresh Token:

4. 根据Refresh Token刷新Access Token:

实现通过用户密码模式获取Access Token
上面介绍了授权码模式的实现方式,但这种方式的核心实际上是建立了一个授权码和用户信息的映射(包括刷新令牌方式也是建立了刷新令牌与用户信息的映射),后续的Access Token实际上是使用这个了用户信息生成的。换句话用户信息才是核心,.Net中用户信息的体现从底到高分别是:IIdentity->ClaimsIdentity-> AuthenticationTicket,关于用户的身份信息可参考:《ASP.NET没有魔法——ASP.NET Identity与授权》,在基于授权码的模式时通过在授权服务器的登录功能获得了用户信息,而基于用户名密码模式时没有这个跳转登录环节,所以需要直接通过用户名密码来获取用户信息,其实现如下重载了OAuthAuthorizationServerProvider类型的GrantResourceOwnerCredentials方法:

该方法从Owin环境中获取Identity中的UserManager对象,通过UserManager来验证用户是否存在,如果存在则将使用用户信息来创建一个ClaimsIdentity对象(注:此处是省略的实现,正常实现可根据需求参考Cookie验证方式将Scope或者Role等信息也添加到Identity对象中)。另UserManager是通过以下代码添加到Owin上下文中的,它的Key值是"AspNet.Identity.Owin:" + typeof(ApplicationUserManager).AssemblyQualifiedName。

使用用户名密码获取Access Token:

实现客户端模式获取Access Token
客户端模式和用户名密码模式是类似的,它是通过Client的Id以及密码来进行授权,使用的是Client相关的信息,它的实现方式如下,重载GrantClientCredentials方法,通过客户端验证后的id和密码信息来验证改Client是否合法,对于合法的Client为其创建Identity对象(注:此处可以根据实际需求在Identity中添加相应的属性):

使用Client信息获取Access Token:

以上就是.Net中对于OAuth的实现,另外.Net中没有提供简化模式的接口,但是提供了一个GrantCustomExtension,也就是说授权模式是可拓展的。
关于.Net中OAuth相关令牌的加密说明
本例中除了授权码以及刷新令牌是2个Guid连接外,访问令牌(包括所有授权模式生成的令牌)以及授权码对应的用户信息、刷新令牌对应的用户信息都是经过加密的,其加解密对象创建过程如下,具体内容可参考《ASP.NET没有魔法——ASP.NET Identity的加密与解密》

小结
本章内容介绍了OAuth2.0协议相关的内容,并通过一个ASP.NET MVC程序基于微软的Microsoft.Owin.Security.OAuth组件实现了该协议中的大部分功能。使用OAuth来实现身份验证可以让我们的应用程序从Web拓展至任意的平台上运行,但这样的实现仍旧是存在一些问题的,在下一篇文章中将对这些问题进一步的讨论和介绍。
PS.这一章内容比较多,如有问题可以在评论区留言,另外最近事情比较多,所以更新慢了,感谢大家的支持。
参考:
https://stackoverflow.com/questions/39909419/jwt-vs-oauth-authentication
http://www.cnblogs.com/linianhui/p/oauth2-authorization.html
http://www.c-sharpcorner.com/UploadFile/4b0136/openid-connect-availability-in-owin-security-components/
https://docs.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server
https://security.stackexchange.com/questions/94995/oauth-2-vs-openid-connect-to-secure-api
http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/
本文链接:http://www.cnblogs.com/selimsong/p/8037717.html
ASP.NET没有魔法——目录