JSON Web Tokens(JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON 的开放标准(RFC 7519)。该 Token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 Token 也可直接被用于认证,也可被加密。

一、为什么要使用JWT

1、传统的鉴权机制

HTTP 协议是一种无状态的协议,这就意味着如果用户向服务端提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据 HTTP 协议,我们无法知道是哪个用户发出的请求,所以为了让服务端能识别是哪个用户发出的请求,只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,并将其保存在 cookie 中,以便下次请求时发送给服务端,这样服务端就能识别请求来自哪个用户,这就是基于 session 的认证。

但是这种基于 session 的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,这时候基于 session 认证的服务端就会出现问题。

2、传统机制所存在的问题

  • 服务端开销大: 每当用户经过服务端认证之后都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言 session 都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
  • 拓展性差:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
  • 易受攻击:因为是基于 cookie 来进行用户识别的,cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击。

3、基于Token的鉴权机制

基于Token的鉴权机制类似于HTTP协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于Token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

注:当采用Token鉴权机制时,服务端需要支持CORS(跨域资源共享)策略,一般在响应头中加上Access-Control-Allow-Origin: *即可。

二、JWT的使用场景

1、授权

这是使用JWT最常见的场景。一旦用户登录之后,每个后续请求将包括JWT,用户便可访问该令牌允许的路由、服务和资源。单点登录(Single-Sign-On)是目前广泛使用JWT的一个特性,因为它的开销很小,而且能够轻松地跨不同的域使用。

2、信息交换

JSON Web令牌是在各方之间安全传输信息的一种好方法。因为JWT可以使用公钥/私钥对进行签名,因此可以确保发送者是所指定的人。另外,由于签名是使用头部和有效负载计算的,所以还可以验证内容是否被篡改。

三、JWT的组成

JSON Web Tokens由头部(Header)、载荷(Payload)、签名(Signature)这三部分组成,每个部分之间用.分隔,JWT的表示形式如下:

1
<Header>.<Payload>.<Signature>

下面我们将详细介绍每个部分:

1、头部

头部通常由两部分组成:令牌的类型(JWT)和正在使用的签名算法,如HMAC SHA256或RSA。

当完整的头部如下面这样的JSON时:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

然后,这个JSON被Base64Url编码,形成JWT的第一部分,编码的结果如下:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2、载荷

令牌的第二部分是有效负载,它包含声明。声明是关于实体(通常是用户)和附加数据的语句。有三种类型的声明:登记声明(Registered claims)、公有声明(Public claims)和私有声明(Private claims)。

  • 登记声明:这些是一组预定义的声明,这些声明不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。其中包括:iss(发行者)、exp(到期时间)、sub(主题)、aud(受众)和其它。

    注意:声明名称只有三个字符长,因为JWT是紧凑的。

  • 公有声明:这些可以由使用JWT的用户随意定义。但是为了避免冲突,应该在IANA JSON Web Token注册表中定义它们,或者将它们定义为包含防冲突命名空间的URI。

  • 私有声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,这些声明既不是注册的,也不是公开的。

举个例子,一个有效的载荷可以为如下形式:

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

然后对有效负载进行Base64Url编码,以形成JSON Web令牌的第二部分,编码结果如下:

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

注意:对于已签名的令牌,此信息虽然受到保护以防篡改,但任何人都可以阅读。不要将机密信息放入JWT的有效负载或头元素中,除非它是加密的。

3、签名

要创建签名部分,您必须获取编码的头部、编码的载荷、一个Secret,然后用头部中指定的算法对其进行签名。

例如,如果要使用HMAC SHA256算法,将按以下方式创建签名:

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

secretfrank时生成的签名如下:

1
KhrURQUSujDUfvesmBrRnihuHOFusehQw5ZFcFkzGhc

签名用于验证消息在整个过程中没有被更改,并且在使用私钥签名的令牌的情况下,还可以验证JWT的发送者是所指定的人。

若想自己尝试生成一个JWT,可以在jwt.io.Debugger进行尝试。

四、JWT的工作流程

JWT完整的工作流程如下图所示:

1607673606249.png

下面将详细介绍这几个流程:

1、客户端获取服务端授权

首先客户端向服务端获取授权,通常是登录操作。

2、服务端生成JWT并返回

在身份验证中,当用户使用其凭据成功登录时,服务端将生成并返回一个JSON Web Token。

3、客户端访问受保护的内容

每当用户想要访问受保护的路由或资源时,用户代理(如:浏览器)应该发送JWT,通常在Authorization请求头中使用Bearer模式。请求头的内容应如下所示:

1
Authorization: Bearer <token>

4、服务端返回结果

服务器的受保护路由将在授权头中检查有效的JWT,如果JWT存在,则允许用户访问受保护的资源。如果JWT包含必要的数据,那么查询数据库以执行某些操作的需求可能会减少。

五、总结

1、JWT是无状态的

因为JWT无需在服务端保存会话信息,也无需考虑 用户在哪一台服务器上进行登录,因此是无状态的。

2、JWT是易于拓展的

因为JWT无需在服务端保存会话信息,因此是易于扩展的。

3、JWT是便于传输的

JWT的构成非常简单,字节占用很小,所以它是非常便于传输的。

4、JWT是不安全的

因为JWT采用Base64Url进行编码,所以JWT中包含的所有信息都是易于获取的,即使他们无法更改这些信息。这意味着不能将机密信息放在JWT中。

若需获取更多关于JSON Web Tokens 的知识,请访问jwt.io