JWT详解
JWT详解
什么是JWT
JWT的全称是Json Web Token。是基于RFC 7519开放标准的,它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。此信息可以用作验证和相互信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
JWT由三部分组成:Header(标头),Payload(有效载荷),Signature(签名),中间用点分开,即
1 | Header.Payload.signature |
我们可以使用jwt.io.Debugger去解码,验证和生成JWT令牌.
Header(标头)
Header通常由两部分组成:令牌的类型(JWT)和所使用的签名算法(如HMAC SHA256或RSA)。
1 | { |
然后,这个JSON被Base64Url编码,形成JWT的第一部分。
Payload(有效载荷)
Payload包含声明。声明是关于实体(通常是用户)和附加数据的声明(claims),一般存放一些不敏感的信息,比如用户名、权限、角色等
声明(claims)也分为三种:Registered claim,Public claims和Private claims.
-
Registered claim(已注册声明):其中包含了一组官方定义好的推荐的声明(共7个),有
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
-
Public claims(公开声明):声明名称可以由使用JWT的人员随意定义.然而,为了防止冲突,任何新的声明名称都应该要么在IANA“JSON Web令牌声明”注册中心注册,要么得包含防撞(冲突)名称.
-
Private claims(私有声明):JWT的生产者和消费者一致同意使用的不是已注册声明和公开声明的声明.私有声明可能会发生冲突,应该小心使用.
例如:
1 | { |
然后,这个JSON被Base64Url编码,形成JWT的第二部分。
应当注意的是,对于已签名的令牌,这些信息虽然受到保护,不会被篡改,但任何人都可以读取。除非经过加密,否则不要将机密信息放入JWT的有效载荷或标头元素中。
Signature(签名)
要创建签名部分,您必须获取编码的标头、编码的有效载荷、秘钥、标头中指定的算法,并对其进行签名。
例如,如果要使用HMAC SHA256算法,则将以以下方式创建签名:
1 | HMACSHA256( |
签名用于验证消息在发送过程中没有更改,在使用私钥签名的令牌的情况下,它还可以验证JWT的发送者是否就是它所说的那个人。
JWT是如何工作的?
在认证的时候,当用户用他们的凭证成功登录以后,一个JWT将会被返回。此后,token 就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上 JWT,典型的,通常放在 Authorization header 中,用 Bearer schema。
header 应该看起来是这样的:
1 | Authorization: Bearer JWT |
服务器上的受保护的路由将会检查 Authorization header 中的 JWT 是否有效,如果有效,则用户可以访问受保护的资源。如果 JWT 包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果 token 是在授权头(Authorization header)中发送的,那么跨源资源共享 (CORS) 将不会成为问题,因为它不使用 cookie。
JWT认证流程如下:
- 用户登录账号,客户端发送 POST 请求,将用户名和密码发送到服务器。
- 服务器会验证用户的登录信息(用户名、密码),校验成功的时候会使用 JWT 算法,生成一个 token (已签名的 token)。
- 服务器返回给客户端 HTTP 200 状态码,并且会将生成的 token 放在请求头中(header),并不能放在请求体(body)中。客户端一般会将接收到的 token 存储在浏览器的 localstorage,cookie 或者 sessionstorage 。
- 当客户端之后再向服务器发送请求的时候,都会携带上 token (当然也可以将 token 放在 cookie 中自动发送,但是这样不能跨域发送)。最好的做法是将 token 放在 http 的头信息中的 Authorization 字段中(这是官方文档建议的做法)。
- 当服务器接收到请求的时候,接收到了 token 信息,这时候 JWT 进行反向验证,验证对应的 token 是否正确。
- 当服务器交验完毕后,会产生两种情况:第一种情况就是校验成功,返回给客户端 HTTP 200 状态码和客户端所需要的数据;第二种情况就是校验失败,这个时候会返回给客户端 HTTP 401 状态码,Not authorized。
JWT的优缺点
-
无状态:JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
-
单点登录友好:使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 JWT 进行认证的话, JWT 被保存在客户端,不会存在这些问题。
-
有效避免了 CSRF 攻击:CSRF 攻击需要依赖 Cookie ,Session 认证中 Cookie 中的 SessionID 是由浏览器发送到服务端的,只要发出请求,Cookie 就会被携带。借助这个特性,即使黑客无法获取你的 SessionID,只要让你误点攻击链接,就可以达到攻击效果.JWT一般会选择存放在 localStorage 中。前端的每一个请求后续都会附带上这个 JWT,整个过程压根不会涉及到 Cookie。
-
不可控:我们想要在JWT有效期内废弃一个JWT或者更改它的权限的话,并不会立即生效,通常需要等到有效期过后才可以。
JWT身份认证常见问题及解决办法
注销登录等场景下JWT还有效
-
将JWT存入内存数据库
将 JWT存入数据库中,Redis 内存数据库在这里是不错的选择。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 发送请求都要先从 DB 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。 -
黑名单机制
使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。缺点和第一种方案相同。 -
修改密钥(Secret)
我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大:-
如果服务是分布式的,则每次发出新的 JWT 时都必须在多台机器同步密钥。为此,你需要将密钥存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。
-
如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的
-
-
保持令牌的有效期限短并经常轮换
很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。
JWT的续签问题
-
类似于Session认证中的做法
假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。 -
每次请求都返回新JWT
这种方案的的思路很简单,但是,开销会比较大 -
JWT有效期设置到半夜
这种方案是一种折中的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。 -
用户登录返回两个JWT
第一个是 accessJWT ,它的过期时间为JWT本身的过期时间比如半个小时,另外一个是refreshJWT它的过期时间更长一点比如为 1 天。客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。
这种方案的不足是:
- 需要客户端来配合;
- 用户注销的时候需要同时保证两个 JWT 都无效;
- 重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT)。
References
-
JWT官方文档:本文开头有