ASP.NET没有魔法——ASP.NET OAuth、jwt、OpenID Connect

  上一篇文章介绍了OAuth2.0以及如何使用.Net来实现基于OAuth的身份验证,本文是对上一篇文章的补充,主要是介绍OAuth与Jwt以及OpenID Connect之间的关系与区别。html

  本文主要内容有:
  ● Jwt简介
  ● .Net的Jwt实现
  ● OAuth与Jwt
  ● .Net中使用Jwt Bearer Token实现OAuth身份验证
  ● OAuth与OpenID Connectweb

  注:本章内容源码下载:https://files.cnblogs.com/files/selimsong/OAuth2Demo_jwt.zip算法

Jwt简介

  Jwt(Json Web Token)它是一种基于Json用于安全的信息传输标准,Jwt具备如下几个特色:
  ● 紧凑:Jwt因为是为Web准备的,因此就须要让数据尽量小,可以在Url、Post参数或者Http Header中携带Jwt,同时因为数据小,因此也增长了数据传输的速度。
  ● 自包含:在Jwt的playload部分包含了全部应该包含的信息,特别是在Jwt用于身份验证时playload中包含了用户必要的身份信息(注:不该该包含敏感信息),这样在进行身份验证时就无需去数据库中查询用户信息。
  ● 可信:Jwt是带有数字签名的,能够知道Jwt在传输过程当中是否被篡改,保证数据是完整的,可用的签名算法有RS256(RSA+SHA-256)、HS256(HMAC+SHA-256)等。数据库

  Jwt有两个用途,其一是用于数据交互,由于Jwt是被签名的,能够保证数据的完整性。另外就是用来携带用户信息进行身份验证json

  Jwt包含三个部分:
  ● Header:包含了签名算法以及令牌类型(默认为JWT)。如:api

  

  注:alg以及typ均是缩写,其目的就是为了减少jwt的大小。数组

  ● Playload:包含Jwt所携带的信息内容,Playload中包含了3种类型的Claim(声明)定义,分别是标准的,如iss(issuer,Jwt的发行者)、sub(subject,Jwt所表明的用户)、aud(audience,Jwt的接收者)、exp(expiration time,Jwt的过时时间),还有一些是公共约定的如: http://www.iana.org/assignments/jwt/jwt.xhtml,另外就是私有自定义的,这些用来存放具体的信息。
  Playload的结构以下:安全

  

  ● Signature:包含了Header以及Playload的base64Url编码后的签名结果,其计算过程以下:服务器

  

  最终三个部分均使用Base64Url的方式进行编码后使用符号“.”进行分隔,如下是一个完整Jwt的例子:app

  

  注:Jwt中的数据是透明的,既任何人拿到数据都能Base64Url反编码的形式看到内容,签名仅仅是保证内容不被纂改,因此不能在Jwt中包含敏感数据。以上例子均来自https://jwt.io/introduction/ 

.Net的Jwt实现

  Jwt是一个标准,在https://jwt.io/上能够看到不少不一样语言对Jwt的实现,而.Net的其中一个实现是System.IdentityModel.Tokens.Jwt组件,该组件是由微软实现的,它有两个重要的类型分别是:
  注:从名称(IdentityModel)均可以看出,微软的这个实现主要是用于身份验证的,若是使用Jwt的目的不是身份验证能够选择其它的组件或自定义实现。
  ● JwtSecurityToken:这个类型是Jwt的一个封装,它除了包含Jwt的三个要素(Header、Playload、Signature)外,还拓展了一些如Subject、Iusser、Audiences、有效期、签名算法、签名密钥等重要属性。
  下图是JwtSecurityToken的部分定义:

  

  ● JwtSecurityTokenHandler:该对象用来对Jwt进行操做,如Jwt的建立、验证( 包含发布者、接收者、签名等验证)、Jwt的序列化与反序列化(字符串形式与对象形式之间的转换)
  下图是JwtSecurityTokenHandler的部分定义:

  

OAuth与Jwt

  OAuth与Jwt前者是一个受权协议后者是一个信息安全传输标准,看起来它们之间并无什么关系,但其实OAuth的Access Token有一种实现方式就是Jwt。
为何要使用Jwt来做为OAuth的Access Token?首先来看一下上一篇文章中生成的Access Token:

  

  它是一个加密后的字符串,该字符串包含了用户的相关信息,可是该字符串只可以被使用Microsoft.Owin.Security.OAuth组件的应用程序解密(不包括参照源码的实现),而且还要保证加解密的密钥是相同的。可是OAuth不少时候是用于一些分布式的场景中,甚至还会使用不一样语言来编写不一样的应用、服务。这样的话上面这种Token的实现方式就没法知足需求。
  因此须要使用Jwt Bearer Token来解决不一样应用中的Token识别问题

.Net中使用Jwt Bearer Token实现OAuth身份验证

  在上一篇文章中提到了Microsoft.Owin.Security.OAuth组件中Access Token的生成其实是对一个AuthenticationTicket对象序列化并加密后的字符串,而Access Token的验证则是对加密后的字符串解密并反序列化得到AuthenticationTicket对象的过程。
  而对于Access Token来讲不管是Microsoft.Owin.Security.OAuth组件的实现方式仍是Jwt,甚至是自定义格式,它的核心都在于如何将用户信息包含到一个字符串令牌中,而且可以经过这个字符串令牌还原出正确的用户信息。对于这一个过程在.Net的Owin身份验证解决方案中将其抽象为一个ISecureDataFormat<TData>接口,其中身份验证的泛型TData类型为AuthenticationTicket。下图是ISecureDataFormat接口的定义,它的两个方法就是用于进行字符串加密令牌与用户信息对象之间的转换,可参考《ASP.NET没有魔法——ASP.NET Identity的加密与解密

  

  上一篇文章中也给出了Microsoft.Owin.Security.OAuth组件中,默认对Access Token加解密对象是TicketDataFormat,该对象实际上就是一个实现了ISecureDataFormat接口的类型,用于经过数据保护器来完成数据对象的序列化与加解密的工做,可参考《ASP.NET没有魔法——ASP.NET Identity的加密与解密》:

  

  能够这样理解要在.Net中实现基于Jwt Bearer Token的OAuth身份验证,仅须要在Microsoft.Owin.Security.OAuth组件的基础上自定义一个ISecureDataFormat<AuthenticationTicket>类型便可

Jwt主要属性的说明

  实现以前再次对Jwt的一些重要属性进行说明:
  ● Issuer:发布者,Jwt里面包含而且会进行验证的信息,Token的发布者,该发布者实际上就是身份验证服务器自己。
  ● Audience:观众,发布者生成一个Token是根据观众来生成的,由于整个验证体系是以发布者为中心的分布式的包含多种应用的,为了保证数据安全一个Token只应该针对其中一个应用有效,因此在验证Jwt时还要对Audience进行验证。
  ● Subject:主题,在身份验证中通常用于保存用户信息,如用户名。

  它们三的关系以下图:

  

  User表明的就是Subject,在OAuth中有Client的概念,OAuth的Client就至关于Audience。以前已经实现了Client的管理,如今为每个Client添加一个用来数字签名的密钥,该密钥是一个32位byte数组的Base64编码字符串。另外这里是使用HMAC算法来完成对Token的摘要计算。

  

实现一个基于Jwt的ISecureDataFormat<AuthenticationTicket>

  下面就开始介绍如何来实现这个ISecureDataFormat:
  1. 经过Nuget安装Microsoft.Owin.Security.Jwt组件:
  注:微软实现了一个用于解析Jwt Bearer Token的组件,可是该组件只实现了Unprotect方法,使用这个组件开发能够减小一些工做量。

  

  2. 了解Microsoft.Owin.Security.Jwt中JwtFormat类型:
  Microsoft.Owin.Security.Jwt中实现了一个JwtFormat的对象,该对象正好实现了须要的ISecureDataFormat接口:

  

  可是从源码中得知该对象没有实现Protect方法:

  

  而它的UnProtect方法的实现主要工做以下:

  

  ● 对发布者以及Token的签名、过时时间等进行验证(注:验证操做是由System.IdentityModel.Tokens.Jwt组件中的JwtSecurityTokenHandler类型提供的)。
  ● 验证成功后获取Token中包含的用户信息。

  3. 实现Jwt的Protect方法:

  

  完整代码:

 1     public class MyJwtFormat :  ISecureDataFormat<AuthenticationTicket>
 2     {
 3         //用于从AuthenticationTicket中获取Audience信息
 4         private const string AudiencePropertyKey = "aud";
 5 
 6         private readonly string _issuer = string.Empty;
 7         //Jwt的发布者和用于数字签名的密钥
 8         public MyJwtFormat(string issuer)
 9         {
10             _issuer = issuer;
11         }
12 
13         public string Protect(AuthenticationTicket data)
14         {
15             if (data == null)
16             {
17                 throw new ArgumentNullException("data");
18             }
19             //获取Audience名称及其信息
20             string audienceId = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ?
21                 data.Properties.Dictionary[AudiencePropertyKey] : null;
22             if (string.IsNullOrWhiteSpace(audienceId)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience");
23             var audience = ClientRepository.Clients.Where(c => c.Id == audienceId).FirstOrDefault();
24             if (audience == null) throw new InvalidOperationException("Audience invalid.");
25             //根据密钥建立用于数字签名的SigningCredentials,该对象在JwtSecurityToken中使用
26             var keyByteArray = TextEncodings.Base64Url.Decode(audience.Secret);
27             var signingKey = new InMemorySymmetricSecurityKey(keyByteArray);
28             var signingCredentials = new SigningCredentials(signingKey,
29                 SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.Sha256Digest);
30             //获取发布时间和过时时间
31             var issued = data.Properties.IssuedUtc;
32             var expires = data.Properties.ExpiresUtc;
33             //建立JwtToken对象
34             var token = new JwtSecurityToken(_issuer,
35                 audienceId, 
36                 data.Identity.Claims,
37                 issued.Value.UtcDateTime,
38                 expires.Value.UtcDateTime,
39                 signingCredentials);
40             //使用JwtSecurityTokenHandler将Token对象序列化成字符串
41             var handler = new JwtSecurityTokenHandler();
42             var jwt = handler.WriteToken(token);
43             return jwt;
44         }
45 
46         public AuthenticationTicket Unprotect(string protectedText)
47         {
48             throw new NotImplementedException();
49         }
50     }
View Code 

  上面代码作了如下几件事:
  ● 从AuthenticationTicket中获取Audience信息(注:AuthenticationTicket是.Net中用来保存用户信息的对象,它除了用户信息,如用户名以及用户Claims以外还携带了身份验证的有效期等附加信息,见下图。AuthenticationTicket的建立方式有两种,其一是登陆时,在判断登陆信息无误后,从数据库中获取相应的用户信息以及从配置(或者默认)获取身份验证信息,若有效期等。另外就是经过反序列化身份Token获取。这里的Protect方法实际上就是序列化Token的方法,因此它获得的AuthenticationTicket是经过第一总方式建立的)

  

  ● 建立用于数字签名的SignatureCredentials对象,该对象表明了用于数字签名的算法及其密钥,建立该对象的缘由仅仅是JwtSecurityToken对象须要它来完成Token建立。
  ● 经过JwtSecurityToken对象建立Token,该对象的建立须要发布者(issuer)、观众(audience)、用户Claims信息、发布时间、有效期以及数字签名须要的算法及密钥等。
  ● 经过JwtSecurityTokenHandler完成对Token的序列化。

  3. 在AuthenticationTicket中加入Audience信息。
  上面在建立Token时提到了须要Audience信息,而Token是经过AuthenticationTicket建立的,因此须要在建立AuthenticationTicket时加入Audience信息,另外上面也提到AuthenticationTicket的两种建立方法,这里使用的方法就是在“登陆”时建立的,而OAuth的“登陆”是经过不一样类型的“受权”方式实现的,因此要加入Audience信息,只须要在相应方式的受权代码中添加便可(以基于用户名、密码的模式为例,其它方法复制代码便可):

  

  4. 为Audience(Client)添加用于解析Token的JwtBearerAuthentication中间件:

  

  Audience或者说Client包含了受限制的资源,当要访问这些资源时就须要解析Token完成身份验证。而Audience之间或者是Client之间是相对独立的,因此它应该限制可访问的Audience以及拥有本身的加密密钥,甚至还须要验证发布者以肯定token的安全性。(注:本例将身份验证服务器和Client都包含在同一个应用中,实际应用可将其分开,这样就是一个简单的单点登陆系统)。

  5. 运行程序

  

  使用该Token可以正常访问受限资源:

  

  下面是将Token Base64解码后的结果,能够看到Jwt包含的信息:

  

  若是使用test2这个Client获取的Token,将没法访问test1保护的资源:

  

  身份验证失败,跳转登陆页面:

  

OAuth与OpenID Connect

  OAuth与OpenID Connect是常常一块儿出现的两个名词,前者在本系列文章中已经进行过介绍,OAuth是一个受权协议,可是有点矛盾的就是身份验证和受权其实是两个概念,前面文章也提到过的,身份验证的目的是知道“你”是谁,而受权则是判断“你”是否有权限访问资源。可是从上一篇文章开始介绍的OAuth相关的内容都是用来作身份验证。受权协议用来作身份验证,因此说是矛盾的。
  OpenID Connect就是为了弥补OAuth协议的缺陷,而在OAuth协议基础上进行补充拓展的一个身份验证协议。它包含了如发现服务、动态注册、Session管理、注销机制等新的高级特性。
  使用OAuth来作身份验证,只是由于OAuth相对简单,适合小型项目,这个与OAuth是受权协议仍是身份验证协议无关,它关注的是可否知足需求,包括app.UseOAuthBearerAuthentication方法名称都是Authentication而不是Authorization,经过添加OAuth Bearer身份验证中间件来实现身份验证。OpenID Connect更适合于大型项目,在这里就再也不深刻介绍。

  关于OAuth与OpenID Connect的内容可参考 blackheart的博客。感谢 blackheart给我提的意见。^_^

小结

  本章介绍了Jwt以及Jwt在.Net中的实现,并介绍了在.Net中如何使用Jwt Token实现基于OAuth的身份验证。使用Jwt Token最主要的是为了解决不一样应用的Token识别问题。
  最后简单的说明了OAuth与OpenID Connect的区别,它们取舍的关键点在于需求,对于小型应用来讲OAuth就可以知足,而因为OpenID Connect很是复杂,若是有需求时也能够先考虑使用如IdentityServer这些开源组件。

  

  与身份验证相关的内容暂时到此,关于.Net安全相关内容能够参考下面的博客,很是全面包含了身份验证以及.Net中的加解密等内容:https://dotnetcodr.com/security-and-cryptography/ 

参考:

  https://dzone.com/articles/whats-better-oauth-access-tokens-or-json-web-token
  https://stackoverflow.com/questions/32964774/oauth-or-jwt-which-one-to-use-and-why
  http://openid.net/specs/draft-jones-oauth-jwt-bearer-03.html
  https://tools.ietf.org/html/rfc7523
  https://auth0.com/learn/json-web-tokens/
  https://stackoverflow.com/questions/39239051/rs256-vs-hs256-whats-the-difference
  https://stackoverflow.com/questions/18677837/decoding-and-verifying-jwt-token-using-system-identitymodel-tokens-jwt
  http://www.c-sharpcorner.com/UploadFile/4b0136/openid-connect-availability-in-owin-security-components/
  https://security.stackexchange.com/questions/94995/oauth-2-vs-openid-connect-to-secure-api
  https://www.cnblogs.com/linianhui/archive/2017/05/30/openid-connect-core.html

本文连接:http://www.cnblogs.com/selimsong/p/8184904.html 

ASP.NET没有魔法——目录

相关文章
相关标签/搜索