近几年,前后端分离大行其道,在典型的前后端分离的应用架构中,后端主要作为 Model 层,为前端提供数据访问的 API,前后端之间的通信需要在不可信(Zero Trust)的异构网络之间进行,为了保证数据安全可靠地在客户端与服务端之间传输,实现客户端认证就显得非常重要。而 HTTP 协议本身是无状态的,实现服务端的客户端认证的基础是记录客户端和服务端的对话状态。

我们最熟悉的认证客户端的方式就是基于 session/cookie 的状态记录方式,基本流程是:

  1. 客户端向服务器端发送用户名和密码
  2. 服务器验证通过后,创建新的对话(session)并保存保存相关数据,比如用户角色、登录时间等
  3. 服务器向客户端返回一个 SESSIONID,写入客户端的 Cookie
  4. 客户端随后的每一次请求,都会通过 Cookie,将 SESSIONID 传回服务器
  5. 服务器收到 SESSIONID,取出相应 session 并与保存的 session 信息进行对比,由此得知用户的身份

这种认证方式最大的问题是,没有分布式架构,无法支持横向扩展。如果使用一个服务器,该模式完全没有问题。但是,如果它是服务器群集或面向服务的跨域体系结构,则需要一个统一的 session 数据库库来保存会话数据实现共享,这样负载均衡下的每个服务器才可以正确的验证用户身份。

举例来说,某企业同时有两个不同的网站 A 和网站 B 提供服务,如何做到用户只需要登录其中一个网站,然后它就会自动登录到另一个网站?

一种解决方案是使用持久化 session 的基础设施,例如 Redis,写入 session 数据到持久层。收到新的客户端请求后,服务端从持久层查找对应的 session 信息。这种方案的优点在于架构清晰,而缺点是架构修改比较费劲,整个服务的验证逻辑层都需要重写,工作量相对较大。而且由于依赖于持久层的数据库或者类似系统,会有单点故障风险,如果持久层失败,整个认证体系都会挂掉。

有没有别的方案呢?这就是我们即将要学习的 JWT(JSON Web Token) 认证方式。

什么是 JWT

JWT 另辟蹊径,基于令牌(token)认证客户端,也就是说只需要在每次客户端请求的HTTP头部附上对应的 token,服务器端负责去检查 token 的签名来确保 token 没有被篡改,这样通过客户端保存数据,而不是服务器保存会话数据,每个请求都被发送回服务器端认证。

根据官方的定义,JWT 是一套开放的标准(RFC 7519),它定义了一套简洁且安全的方案,可以在客户端和服务器之间传输 JSON 格式的 token 信息。

JWT 的工作原理

JWT 服务端认证的基本原理是在服务器身份验证之后,将生成一个 JSON 对象并将其发送回客户端,如下所示:

{
    "username": "morvencao",
    "role": "Admin",
    "expire": "2017-02-08 12:45:43"
}

之后,当客户端与服务器端通信时,客户端需要在请求中发回这个 JSON 对象。服务器仅依赖于这个 JSON 对象的内容来认证客户端。为了防止中间人篡改数据,服务器将在生成 JSON 对象时添加签名。但服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。

JWT 的数据格式

一个典型的 JWT 的数据结构看起来如下图所示:

JWT 对象为一个很长的字符串,字符之间通过 . 分隔符分为三个子串,各字串之间也没有换行符,每一个子串表示了一个功能块,总共有以下三个部分:

  1. JWT Header

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示:

{
    "alg": "HS256",
    "typ": "JWT"
}

在上面的代码片段中,alg 属性表示签名使用的算法,默认为 HMAC SHA256typ 属性表示令牌的类型,JWT 令牌统一写为JWT;最后,使用 Base64URL 算法将上述 JSON 对象转换为字符串保存。

  1. JWT Payload

有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。JWT 指定七个默认字段供选择:

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例所示:

{
    "sub": "xxxxxxxxx",
    "username": "morvencao",
    "role": "Admin",
    "expire": "2016-04-20 12:45:43"
}

Note: 默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建敏感信息字段,例如存放保密信息,以防止信息泄露。

JSON 对象也使用 Base64URL 算法转换为字符串保存。

  1. Signature

签名哈希部分是对以上两部分数据签名,通过指定的算法生成哈希值,以确保数据不会被篡改。

首先,需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向客户端公开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名:

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

在计算出签名哈希后,<Header>.<Payload>.<Signature> 三个部分使用 . 组合成一个字符串,就构成整个 JWT 对象。

优点

  • 体积小:一串字符串,传输速度快
  • 传输方式多样:可以通过 HTTP Header/URL/POST 参数等方式传输
  • 严谨的结构化:它自身(在有效载荷部分中)就包含了所有与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且有效载荷部分支持应用定制
  • 支持跨域验证:多应用于单点登录

其实,除了以上很容易看得见的优点之外,相对于传统的服务端认证,JWT 还有以下优点:

  1. 充分依赖无状态 API,契合 RESTful 设计原则

JWT 的设计契合无状态原则:用户登录之后,服务器会返回一串 token 并保存在本地,在这之后的服务器访问都要带上这串 token,来获得访问相关路由、服务及资源的权限。比如单点登录就比较多地使用了 JWT,因为它的体积小,并且经过简单处理(使用 HTTP 请求的头信息 Authorization 字段里面带上 Authorization: Bearer <token>)就可以支持跨域操作。

  1. 易于实现 CDN,将静态资源分布式管理

在传统的 session/cookie 认证方式中,服务端必须保存 SESSIONID,用于与用户传过来的 cookie 进行验证。而一开始 session 只会保存在一台服务器上,所以只能由一台服务器应答,就算其他服务器有空闲也无法应答,无法充分利用到分布式服务器的优点。JWT 依赖的是在客户端本地保存认证信息,不需要利用服务器保存的信息来验证,所以任意一台服务器都可以应答,服务器的资源也能被较好地利用。

  1. 认证解耦,随处生成

无需使用特定的身份验证方案,只要拥有生成 token 所需的认证信息,在何处都可以调用相应接口生成 token,无需繁琐的耦合的认证操作,可谓是一次生成,永久使用。