什么是JWT

JWT 简称 JSON Web Token,也就是通过 JSON 形式作为 Web 应用中的令牌,用于在各方面之间安全的将信息作为 JSON 对象传输,在数据传输过程中还可以完成数据加密,签名等相关操作

JWT能做什么

授权: 这是使用 JWT 最常见的方案,一旦用户登录,每个后续请求将包括 JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当前广泛使用 JWT 的一项功能,因为它的开销很小并且可以在不同域中轻松使用

信息交换:JSON Web Token 是在各方面之间安全地传输信息的好办法,因为可以对 JWT 进行签名(例如使用公钥 / 私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此还可以验证内容是否被篡改

为什么是JWT

基于传统的Session认证

认证方式

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

暴露问题

每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以便用户下次请求的鉴别,通常而言 Session 都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大

用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡的能力,这也意味着限制了应用的扩展能力

因为是基于 Cookie 来进行用户识别的,Cookie如果被截获,用户就会很容易受到跨域请求伪造的攻击

在前后端分离的项目中更加痛苦。在前后端分离的项目中增加了部署的复杂性,通常用户一次请求就要转发多次。如果用 Session 每次携带 sessionId 到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储在服务器内存中,给服务器增加负担。还有就是 CSRF 攻击(跨站伪造请求攻击),Session 是基于 Cookie 进行用户识别的,Cookie 如果被截获,用户就会受到跨站请求伪造的攻击。还有一个就是 sessionId 就是一个特征值,表达的信息不够丰富,不容易扩展。而且如果你后端应用是多节点部署,那么就需要实现 Session 共享机制,不方便应用集群

基于JWT认证

认证流程

首先前端通过 Web 表单将自己的用户名密码发送到后端接口,这一过程一般是一个HTTP POST请求。建议的方式是通过 SSL 加密的方式传输(https 协议),从而避免敏感信息被嗅探。后端核对用户名密码成功后,将用户 ID 等其他信息作为 JWT Payload(负载),将其与头部分别进行 Base64 编码拼接后签名,形成一个 JWT。形成的 JWT 就是一个形同 lll.zzz.xxx 的字符串。后端将 JWT 字符串作为登录成功的返回结果返回给前端,前端可以将返回结果保存在 localStorage 或者 sessionStorage,退出登录时前端删除保存的JWT即可。前端每次在请求时将 JWT 放入 HTTP Header 中的 Authorization 位(解决 XSS 和 XSRF 问题)。后端检查是否存在,如存在验证 JWT 有效性。例如检查签名是否正确,检查 token 是否过期。验证成功后后端使用 JWT 中包含的用户信息进行逻辑操作,返回相应结果

JWT优势

  • 简洁: 可以通过 URL,POST 参数或者 HTTP Header 发送。因为数据量小,传输速度也很快
  • 自包含: 负载中包含了所有用户所需要的信息,避免了多次查询数据库
  • 因为 token 是以 JSON 加密的形式保存在客户端,所以 JWT 是跨语言的,原则上任何 Web 形式都支持
  • 不需要在服务端保存会话信息,特别适合分布式服务

JWT的结构

  • 标头(Header)

标头通常由两部分组成: 令牌的类型(即 JWT)和所使用的签名算法,例如 HMAC、SHA256 或 RSA,它会使用 Base64 编码组成 JWT 结构的第一部分。注意: Base64 是一种编码,也就是说,它是可以被翻译回原来的样子来的,它并不是一种加密过程

1
2
3
4
{
"alg": "H256",
"typ": "JWT"
}
  • 有效载荷(Payload)

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明,同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

1
2
3
4
5
{
"sub": "123456",
"name": "Doe",
"admin": true
}
  • 签名(Signature)

前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(H256)进行签名,签名的作用是保证 JWT 没有被篡改过

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

最后一步签名的过程,实际上就是对头部以及负载内容进行签名,防止内容被篡改。如果有人对头部以及负载的解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的 JWT 的话,那么服务端会判断出新的头部和负载形成的签名和 JWT 附带的签名不一样。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的

Base64是一种编码,是可逆的,那我的信息不就暴露了吗?

是的。所以在 JWT 中,不应该在负载里面假如任何敏感信息。在上面的例子中,我们传输的是用户的 userId,这个值实际上不是什么敏感信息,一般情况下也是安全的。但是像密码这样的内容就不能放在 JWT 中了,如果用户的密码放在了 JWT 中,那么怀有恶意的第三方通过 Base64 解码就能很快的知道你的密码了,因此 JWT 适合用于向 Web 应用传递一些非敏感信息。JWT 还经常用于设计用户认证和授权系统,甚至实现 Web 应用的单点登录