1.简介
JWT(JSON Web Token)是目前最流行的跨域认证解决方案,是一种基于Token的认证授权机制。从JWT的全称可以看出,JWT本身也是Token,一种规范化之后的JSON结构的Token。
JWT自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储Session信息,这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
可以看出,JWT更符合设计RESTFUL API时的无状态原则。
无状态原则:
授权服务器不需要维护任何状态;令牌本身就是验证令牌持有者授权所需的全部内容。
同时JWT认证可以有效的避免cookie的CSRF攻击,因为JWT一般是存储在localStorage中,使用JWT进行身份验证的过程是不会涉及到Cookie的。
2.JWT的组成部分
JWT 本质上就是一组字串,通过(.
)切分成三个为 Base64 编码的部分:
Header(头部):描述JWT的元数据,定义了生成签名的算法以及Token的类型。Header被Base64Url编码后成为JWT的第一部分。
同时Header通常由两部分组成
1.Type : 令牌类型
2. Algorithm:签名算法
PayLoad(载荷):用来存放实际需要传输的数据。包含声明(Claims),如
sub
(subject,主题)、jti
(JWT ID)。Payload 被 Base64Url 编码后成为 JWT 的第二部分。PayLoad也是JSON格式的数据,其中包含了Claims(声明,包含JWT的相关信息)
Claims分为三种类型:
1.Registered Claims (注册声明):预定义的一些声明,建议使用,但不是强制性的
2.Public Claims (公有声明) :JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 IANA JSON Web Token Registryopen in new window 中定义它们。
3.Private Claims (私有声明):JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。
下面是一些常见的注册声明:
iss
(issuer):JWT 签发方。
iat
(issued at time):JWT 签发时间。
sub
(subject):JWT 主题。
aud
(audience):JWT 接收方。
exp
(expiration time):JWT 的过期时间。
nbf
(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
jti
(JWT ID):JWT 唯一标识。
Signature
Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。
这个签名的生成需要用到:
Header + Payload。
存放在服务端的密钥(一定不要泄露出去)。
签名算法。
签名的计算公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
3.如何基于JWT进行身份验证
在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。
步骤为:
1.用户向服务器发送用户名、密码以及验证码用于登陆系统。
2.如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
3.用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
4.服务端检查 JWT 并从中获取用户相关信息。
4. JWT身份认证的一些问题
实际上JWT并不是万金油,也有很多缺陷,就比如注销登录场景下JWT还有效的问题和JWT的续签问题、JWT体积太大问题。接下来就讲一下这两三个问题
4.1.注销登录下JWT还有效
这个问题不存在Session认证的场景中,因为在Session认证方式中,遇到这种情况的话服务端删除对应的session记录即可。可是,使用JWT认证的方式就不好解决了。
那我们如何解决这个问题呢?查阅了很多资料,我简单总结了下面 4 种方案:
1. 将JWT存入数据库
将有效的 JWT 存入数据库中,更建议使用内存数据库比如 Redis。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 都要先从 Redis 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。
2. 黑名单机制
和上面的方式类似,使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。
这两种方案的核心都是借助存储,其本质已经违背了JWT的无状态原则,但是一般实际项目中我们通常还是会使用这两种方案的。
3. 修改密钥
我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大:
1. 如果服务是分布式的,则每次发出新的 JWT 时都必须在多台机器同步密钥。为此,你需要将密钥存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。
2. 如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
4.保持令牌的有效期限短并经常轮换
很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。
另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。说一种我觉得比较好的方式:使用用户的密码的哈希值对 JWT 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。
4.2 JWT的续签问题
JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认证,如何实现动态刷新 JWT,避免用户经常需要重新登录?
我们先来看看在 Session 认证中一般的做法:假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。
那么如何解决续签的问题呢?下面有4种方法
1. 类似于Session认证中的做法(不推荐)
这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。
2. 每次请求都返回新的JWT(不推荐)
这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
3. JWT有效期设置到半夜(不推荐)
这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
4.用户登录返回两个JWT(推荐)
第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。refreshJWT 只用来获取 accessJWT,不容易被泄露。
客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。
但是这种方案也有不足
1、需要客户端来配合;
2.用户注销的时候需要同时保证两个 JWT 都无效;
3.重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT);
4.存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。不过,由于 refreshJWT 只用来获取 accessJWT,不容易被泄露。
4.3 JWT体积太大
JWT 结构复杂(Header、Payload 和 Signature),包含了更多额外的信息,还需要进行 Base64Url 编码,这会使得 JWT 体积较大,增加了网络传输的开销。
解决办法:
1.尽量减少 JWT Payload(载荷)中的信息,只保留必要的用户和权限信息。
2、在传输 JWT 之前,使用压缩算法(如 GZIP)对 JWT 进行压缩以减少体积。
3.在某些情况下,使用传统的 Token 可能更合适。传统的 Token 通常只是一个唯一标识符,对应的信息(例如用户 ID、Token 过期时间、权限信息)存储在服务端,通常会通过 Redis 保存。