漫谈Web--JWT

“The most amazing achievement of the computer software industry is its continuing cancellation of the steady and staggering gains made by the computer hardware industry.” —Henry Petroski

前言

随着技术的发展,分布式web应用的普及,通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息,随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是所有数据都保存在客户端,每次请求都发回服务器,随后服务器直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。

这篇文章中主要讲解了关于JSON Web Tokens(JWT)的基础概念,以及解释JWT为什么会被广泛应用。JWT是确保应用程序信任和安全的重要部分,它允许以诸如用户数据之类的安全的方式声明表示。

为了JWT的工作原理,我们先来看一看关于它的抽象定义:

JSON Web Token(JWT)是一种JSON对象,在RFC 7519中定义为表示双方之间的一组信息的安全方式;它由头部、有效负载和签名三部分组成。

我们完全可以把JWT简单地看成仅仅是符合以下格式的字符串

header.payload.signature

Notes: 合法的JSON必须是使用双引号的字符串

为了理解JWT是如何被使用的,我们将使用User(用户)、Application Server(应用服务器)、Authentication Server(授权服务器)3个简单的实体加以解释说明。其中Authentication Server将提供JWT给用户,用户拿到JWT后就可以和应用(app/website)安全地通信。

How an application uses JWT to verify the authenticity of a user.

上图中,User携带用户名/密码等可证明身份的内容去授权服务器获取JWT信息,每次服务都携带该Token内容与应用服务器进行交互,由应用服务器来验证Token是否是授权系统发放的有效Token,来验证当前业务是否请求是否合法。

接下来我们深入理解JWT是如何构造以及如何验证的。

第一步 创建头部(header)

JWT的头部包含了JWT自身的签名算法信息,它是一个具有如下格式的JSON对象:

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

这个JSON中, typ属性表示令牌的类型,JWT令牌统一写为JWT。alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256)

第二部 创建有效载荷(payload)

JWT的有效载荷即是JWT的主体内容部分,包含了需要传递的数据(这些数据也被称为JWT的声明)。在我们的🌰中,JWT中存储了用户ID信息。

1
2
3
{
"userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
}

上面的🌰中,我们的JWT仅仅存储了一个信息。然而JWT 规定了7个官方字段,供选用

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

当然除了官方字段,你还可以在这个部分定义私有字段,诸如:

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

值得注意的是有效载荷的存储数据的大小会影响整个JWT的大小,一般来说对JWT的大小没有限制但是过大的JWT可能会影响性能或造成潜在的问题。

第三步 创建签名(signature)

签名是为了防止数据被篡改。

JWT的签名使用以下伪代码实现:

1
2
3
4
// signature algorithm
data = base64urlEncode( header ) + “.” + base64urlEncode( payload )
hashedData = hash( data, secret )
signature = base64urlEncode( hashedData )

该算法所做的使用base64urlEncode对在第一步创建的头部和第二部中的有效负载分别进行编码并用”.”号拼接成字符串。随后使用JWT头中指定的散列算法对字符串使用密钥进行散列。最后再使用base64urlEncode对生成的散列数据进行编码以产生JWT签名。(密钥只有服务器才知道,不能泄露给用户)

这篇文章中,使用base64urlEncode对header和payload进行编码后得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
// header
base64urlEncode({
"typ": "JWT",
"alg": "HS256"
})
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
// payload
// base64urlEncode({
"userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
})
eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ

然后将结果代入上面的伪代码会得到如下签名:

1
2
// signature
-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

第四步 生成JWT

现在我们已经创建完成了JWT的header、payload、signature3个部分,那么再按照

header.payload.signature

的格式将3个部分组合起来就是所谓的JWT了。如下就是我们🌰中的JWT

1
2
// JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

JWT官方网站,你可以尝试创建自己的JWT。


回到我们的案例,这个Authentication Server 已经创建了一个JWT,并且发给了用户。

JWT是怎么保护数据的

注意,这里经常会有一个误区,JWT本身和安全没关系,它就仅仅只是一个字符串,使用它来做安全远不如类似于RSA2这样的非对称加密的形式来的实在,由于客户端的程序对用户几乎完全透明,验签的过程对于他们来讲也是透明的,所以安全性肯定不会靠这个来实现,如果实在怕JWT的被盗取,可以考虑在Payload部分加入一些客户端独有的非敏感信息,用于在服务端来进行核验,比如使用MAC-Message Authentication Code、或者公钥之类的等等; 或者干脆就把生效时间设置的短一些,也可以减少暴露的风险。

JWT的数据被编码和被签名,但是没有被加密。编码的目的是转化数据结构,签名是为了数据的接收者验证数据的权威性。所以编码和签名并不会保护数据的安全性。而加密的主要目的才是为了保护数据安全防止未授权访问。对于详细描述编码与加密的区别,可以参考这篇文章

Since JWT are signed and encoded only, and since JWT are not encrypted, JWT do not guarantee any security for sensitive data.

由于JWT仅仅被编码和签名而没有被加密过的,所以它无法保证任何敏感信息的安全性。

第五步 验证JWT

第四步中我们已经知道JWT的签名生成需要用密钥(secret),这个密钥只有授权服务器知道。应与服务器开启认证处理时,需要从授权服务器获取此密钥。此后当用户携带JWT访问应用服务器时,应用服务器拿到JWT后可以
通过类似第三步的方法生成一个签名,然后与用户携带的JWT的签名进行对比从而验证用户JWT是否有效。

结束语

本文中描述的JWT认证设置使用对称密钥算法(HS256)。您也可以使用非对称算法(例如RS256),即授权服务器具有密钥,并且应用程序服务器具有公钥。了解使用对称和非对称算法之间差异的详细分类。

还应该注意,JWT应该通过HTTPS连接(而不是HTTP)发送。HTTPS有助于防止未经授权的用户窃取所发送的JWT,从而无法拦截服务器和用户之间的通信。

同时,JWT应该设置有效期,并且有效期不要太长。