一、先来了解下,什么是JWT?
JWT,是英文JSON Web Tokens的简写,是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准( ),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
-
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
-
自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
JWT的主要应用场景
-
身份认证: 在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。
-
信息交换:在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
再来看看JWT的组成结构:
JWT包含了使用 .分隔的三部分:
-
Header 头部
-
Payload 负载
-
Signature 签名
其结构看起来是这样的xxxxx.xxxxxx.xxxxxx
头部Header:
{ "alg": "HS256",//加密算法 "typ": "JWT"//授权类型}
荷载Payload:
Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim: reserved , public 和 private .
-
Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有
iss(签发者)
,exp(过期时间戳)
,sub(面向的用户)
,aud(接收方)
,iat(签发时间)
。 -
Public claims:根据需要定义自己的字段,注意应该避免冲突
-
Private claims:这些是自定义的字段,可以用来在双方之间交换信息
负载使用的例子:
{ "sub": "1234567890", "name": "John Doe", "admin": true}
上述的负载需要经过 Base64Url 编码后作为JWT结构的第二部分。
Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息的发送者以及消息是没有经过篡改的。另外,官方也提供了在线调试工具
完整的JWT
JWT格式的输出是以 .
分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。
下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名:
如何使用JWT?
在身份鉴定的实现中,传统方法是在服务端存储一个session,给客户端返回一个cookie,而使用JWT之后,当用户使用它的认证信息登陆系统之后,会返回给用户一个JWT,用户只需要本地保存该token(通常使用local storage,也可以使用cookie)即可。
当用户希望访问一个受保护的路由或者资源的时候,通常应该在 Authorization
头部使用 Bearer
模式添加JWT,其内容看起来是下面这样:
Authorization: Bearer
因为用户的状态在服务端的内存中是不存储的,所以这是一种 无状态 的认证机制。服务端的保护路由将会检查请求头 Authorization
中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此减少了需要查询数据库的需要。
JWT的这些特性使得我们可以完全依赖其无状态的特性提供数据API服务,甚至是创建一个下载流服务。因为JWT并不使用Cookie的,所以你可以使用任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)。
为什么要使用JWT?
相比XML格式,JSON更加简洁,编码之后更小,这使得JWT比SAML更加简洁,更加适合在HTML和HTTP环境中传递。
在安全性方面,SWT只能够使用HMAC算法和共享的对称秘钥进行签名,而JWT和SAML token则可以使用X.509认证的公私秘钥对进行签名。与简单的JSON相比,XML和XML数字签名会引入复杂的安全漏洞。
因为JSON可以直接映射为对象,在大多数编程语言中都提供了JSON解析器,而XML则没有这么自然的文档-对象映射关系,这就使得使用JWT比SAML更方便。
第二、不得不提及Katana项目
项目地址:
Katana项目实际可以追溯到 Microsoft 外部一个名为 Open Web Interface for .NET (OWIN) 的开放源代码项目。OWIN 是一种定义 Web 服务器和应用程序组件之间的交互的规范(请参阅 )。 由于这一规范的目的是发展一个广阔且充满活力的、基于 Microsoft .NET Framework 的 Web 服务器和应用程序组件生态系统,因此它可以将服务器与应用程序之间的交互减少到一小部分类型和单个函数签名,这个函数签名被称为应用程序委托(即 AppFunc):
using AppFunc = Func, Task>;
基于 OWIN 的应用程序中的每个组件都向服务器提供应用程序委托。然后,这些组件链接成一个管道,基于 OWIN 的服务器将会向该管道推送请求。为了更有效地使用资源,管道中的所有组件都应该是异步的,这体现在返回 Task 对象的应用程序委托中。
包括应用程序状态、请求状态和服务器状态等在内的所有状态都保存在应用程序委托上指定的 IDictionary<string, object> 对象中。这种数据结构称为环境字典,随着请求通过管道时会从一个组件传递到另一个组件。虽然任何键/值数据都可以插入到环境字典中,但 OWIN 规范为某些 HTTP 核心元素定义了键。
有时间的话,建议大家多去了解下Katana的源码,下面的用法示例,也都多结合了Katana项目本身。
【解析】这里需要提及的是OWIN规范,OWIN本身定义了一种接口,Owin.dll自身并未包含实现,只是定义了名为IAppBuilder的接口,微软对此,也提供了实现,名为Microsoft.Owin.dll。其中,定义了名为OwinMiddleware(抽象类)的中间件,里面定了抽象方法:
public abstract System.Threading.Tasks.Task Invoke(Microsoft.Owin.IOwinContext context)
还有属性Next(可选)
protected OwinMiddleware Next { get; set; }
Next指定下一个要执行的中间件。 中间件可以理解为,在HTTP请求过程中,用于处理任务的组件,它可以决定是否把任务,交给下一个组件。一个应用程序,可以有很多中间件,它们之间,从上到下进行信息的传递,构成一个线性的结构,每一个组件,都有决定是否将任务传递给下一个组件的权利。在中间件的整个传递过程中,当然是有一个Context上下文充当信使,这就是OwinContext,OwinContext包含该了OwinRequest和OwinResponse两个核心成员,OwinRequest和Request,OwinResponse和Response,他们之间一定存在某种关系,在OwinRequest中基本包含了Request的所有信息,为了实现应用和服务器之间的解耦,微软对其另起炉灶。经过上面的分析,我们可以知道,在一个请求周期中,OWinContext信息在不同中间件之间进行传递,类似引用类型,y一个请求过程中,所有中间件都执行OwinContext一个实例。
【开放授权OAuth】Katana项目提供了多种开放授权协议的实现,在这里,我们模拟简单的用户登录授权。这里需要用到一个中间件:
OAuthAuthorizationServerMiddleware
这个中间件用来帮助我们实现请求的授权,中间件用的另一个对象
OAuthAuthorizationServerOptions
配置如下:
//配置OAuth中间件 app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions { //生产环境设为false AllowInsecureHttp = true, TokenEndpointPath = new PathString("/oauth2/token"),//请求token的路径 AccessTokenExpireTimeSpan = TimeSpan.FromDays(30),//令牌的过期时间 //请求获取token时,验证username, password Provider = new OAuthAuthorizationServerProvider() { OnGrantResourceOwnerCredentials = (context) => { var ticket = new AuthenticationTicket(SetClaimsIdentity(context, "1", context.UserName), new AuthenticationProperties()); context.Validated(ticket); return Task.FromResult
这样,我们在客户端,请求/oauth2/token,这里必须是POST的请求。
OnGrantResourceOwnerCredentials委托,可以用来验证请求,比如,验证用户名和密码。
我在页面上用ajax调用获取令牌:
$.ajax({ beforeSend: function (request) { console.log(request); }, type: "post", data: { client_id: "fNm0EDIXbfuuDowUpAoq5GTEiywV8eg0TpiIVnV8", username: "admin", password: "admin", grant_type: "password", client_secret: "IxrAjDoa2FqElO7IhrSrUJELhUckePEPVpaePlS_Xaw" }, url: "/oauth2/token", headers: { alg: "HS256", typ: "JWT" }, success: function (data) { var access_token = data.access_token; testApi(access_token); } });
我在Ajax请求头里,指定了加密算法(alg)和授权类型(typ)。把用户名和密码POST到/oauth2/token。
这样客户端通过ajax发起请求,GrantResourceOwnerCredentials委托进行验证逻辑,参数
OAuthGrantResourceOwnerCredentialsContext对象中,包含了用户名和密码。
【解析】OAuthAuthorizationServerOptions的Provider可以通过自定义来实现,后面我们会讲到。
还有一个需要注意的地方就是AccessTokenFormat属性,这个可以不指定,
默认会采用TicketDataFormat,这个关系到令牌的加密和解密。
JwtBearer中间件的配置
//配置JwtBearer授权中间件 app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions() { AuthenticationMode = AuthenticationMode.Active, AuthenticationType = "password", AllowedAudiences = new[] { audience }, Provider = new OAuthBearerAuthenticationProvider() { OnValidateIdentity = context => { context.Ticket.Identity.AddClaim(new System.Security.Claims.Claim(ClaimTypes.NameIdentifier, "admin")); return Task.FromResult(context); }, OnRequestToken = (context) => { if (context.Token != null) { context.Response.Headers["WWW-Authorization"] = "Bearer"; //protector IDataProtector protector = app.CreateDataProtector(typeof(OAuthAuthorizationServerMiddleware).Namespace, "Access_Token", "v1"); TicketDataFormat dataFormat = new TicketDataFormat(protector); var ticket = dataFormat.Unprotect(context.Token);//从令牌字符串中,获取授权票据 context.Request.User = new ClaimsPrincipal(ticket.Identity); } return Task.FromResult (context); } }, IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[] { new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret) } });
这个需要注意的地方是,OAuthBearerAuthenticationProvider对象的OnRequestToken委托,客户端携带访问令牌请求,经过该方法,进行验证授权,这个地方用到了AccessTokenFormat属性。这里用
默认的TicketDataFormat进行令牌的反序列化,最后会得到一个AuthenticationTicket
的票据信息,然后再为当前请求,创建身份信息,这里是基于声明的授权信息,这样在控制器或Action中指定认证特性[Authorize],授权成功,就可以访问要授权的资源了 。
第一步,ajax请求授权,成功后,返回:
{"access_token":"BLEx3eE72_P8O__wCRUEhdtMlpPq0pnswmPReBZYAVdueL1p33cfybdcd0mVBFZZFsG8OgK8LBik1ZuDADW-NHul-KM8TXbJzr_dVfs2bw9S-YUNKMkJjY7mHilbF3TEI3o54jGn11R8z2tlp_95oLNVn_D10zyAdKNGs8qtb7gMW_XUCQpkRmVNcEd58h9wREXHYnYjWYyMRYT-rttgvxU5LKU5VsUTpdIFjfx2c17ALqr7d0sqY5LZ3nDh-tNyMvNHgu9ea9G2uHE3CVbulOYb-7EVp4MkpzW8rQ0cTAI","token_type":"bearer","expires_in":2591999}
第二步,用获取到的access_token访问,指定的资源。
$.ajax({ beforeSend: function (request) { console.log(request); }, type: "post", data: { client_id: "fNm0EDIXbfuuDowUpAoq5GTEiywV8eg0TpiIVnV8", username: "admin", password: "admin", grant_type: "password", client_secret: "IxrAjDoa2FqElO7IhrSrUJELhUckePEPVpaePlS_Xaw" }, url: "/oauth2/token", headers: { alg: "HS256", typ: "JWT" }, success: function (data) { var access_token = data.access_token; testApi(access_token); } }); //用获取到的access_token请求资源/api/values/get function testApi(token) { $.ajax({ beforeSend: function (request) { }, type: "GET", url: "/api/values/get", headers: { "Authorization": "Bearer " + token }, success: function (data) { } });
最后得到:
["value1","value2"]
授权成功!
由于篇幅的原因,关于JWT的解疑,先写到这里,在后面的博客中,还会及时分享给大家。