Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(Single Sign On,SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
其他三个概念:
JWE(RFC 7516):JSON Web Encryption,表示基于JSON数据结构的加密内容,加密机制对任意八位字节序列进行加密、提供完整性保护和提高破解难度。
JWS(RFC 7515):JSON Web Signature,表示使用JSON数据结构和BASE64URL编码表示经过数字签名或消息认证码(MAC)认证的内容,数字签名或者MAC能够提供完整性保护。
JWA(RFC 7518):JSON Web Algorithm,JSON Web算法,数字签名或者MAC算法
JWT的实现有两种:基于JWT、基于JWS。基于JWE的实现依赖于加解密算法、BASE64URL编码和身份认证等手段提高传输的Claims的被破解难度,而基于JWS的实现使用了BASE64URL编码和数字签名的方式对传输的Claims提供了完整性保护,也就是仅仅保证传输的Claims内容不被篡改,但是会暴露明文。即区别主要在于数据是否被加密。
目前主流的JWT框架中大部分都没有实现JWE,所以下文的讨论是指JWS。
(注:如下所涉及的base64指base64 URL算法,其与普通的base64算法有区别:Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法)
由句号分隔的三段base64 URL串b1.b2.b3,如:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
header:头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等,为json格式。用base64 URL算法转成一个串b1。示例:
{ "typ": "JWT", "alg": "HS256" }
在JWT规范中称这些Header为JOSE Header(Javascript Object Signature Encryption,Javascript对象签名和加密框架),常用的是上面两个。当然还要其他header,如:
paload:放入一些自定义信息,为json格式,这些字段称作"Claim"。用base64转成一个串b2。jwt预放入了五个字段:
iss: 该JWT的签发者
sub: 该JWT所面向的用户
aud: 接收该JWT的一方
exp(expires): 什么时候过期,这里是一个Unix时间戳
iat(issued at): 在什么时候签发的
除了上述字段,还有:nbf(Not Before,早于该定义的时间的JWT不能被接受处理)、jti(JWT ID)等。
这些预定义的Claim并不要求强制使用,何时选用何种Claim完全由使用者决定,而为了使JWT更加紧凑,这些Claim都使用了简短的命名方式去定义。在不和内建的Claim冲突的前提下,使用者可以自定义新的Claim。
signature:用header中所声明的签名算法(需要为之提供一个key),根据 base64( header).base64(paload) 算得签名值:第三个base64串b3。
注:
三部分都是明文的,可以通过base64解码看出原始内容,故jwt payload等部分中一定不要放入敏感数据如密码等内容。
注意签名和加密的区别:前者指根据内容产生一段摘要信息,摘要信息长度通常固定且比原始值少很多且不可逆,签名也可以理解为摘要、指纹、哈希等,具体算法有MD5、HS256等;后者则指用某种算法将原始内容转换成不可读的内容,只有知道解密方法才能根据被加密的内容获知原始内容,具体算法有RSA等。
校验原理
由于用base64,所以可以直接逆转码提取header、payload信息。服务端收到token后会根据header声明的加密算法再计算下signature,若与token中的signature不同则当成未授权的token。
基于JWT实现认证
1、客户端不需要持有密钥,由服务端通过密钥生成Token。
2、客户端登录时通过账号和密码到服务端进行认证,认证通过后,服务端通过持有的密钥生成Token,Token中一般包含失效时长和用户唯一标识,如用户ID,服务端返回Token给客户端。
3、客户端保存服务端返回的Token。
4、客户端进行业务请求时在Head的Authorization字段里面放置Token,如:Authorization: Bearer Token
5、服务端对请求的Token进行校验,并通过Redis查找Token是否存在,主要是为了解决用户注销,但Token还在时效内的问题,如果Token在Redis中存在,则说明用户已注销;如果Token不存在,则校验通过。
6、服务端可以通过从Token取得的用户唯一标识进行相关权限的校验,并把此用户标识赋予到请求参数中,业务可通过此用户标识进行业务处理。
7、用户注销时,服务端需要把还在时效内的Token保存到Redis中,并设置正确的失效时长。
时序图如下:
在上述过程中,登录时服务端需要查询数据库以确定用户名、密码是否正确,在登录成功之后的其他请求中则可以直接从token中提取需要的信息而不需要查询数据库。
基于JWT实现认证的方式下,服务端颁发JWT后不需要保存,客户端可以通过它与服务端交互,由于JWT泄露后哟偶安全风险,故实践时应注意:
JWT需要设置有效期,也就是exp这个Claim必须启用和校验;
JWT需要建立黑名单,一般使用jti这个Claim即可,技术上可以使用布隆过滤器加数据库的组合(数量少的情况下简单操作甚至可以借助Redis实现);
JWS的签名算法尽可能使用安全性高的算法,如RSXXX;
Claims不要写入敏感信息;
高风险场景如支付操作等不能仅仅依赖JWT认证,需要进行短信、指纹等二次认证;
与基于Cookie/Session的会话技术的对比
可参阅一片通俗易懂的文章:https://mp.weixin.qq.com/s/_ViJ-xtj_sRtHppled6AQg
利弊
(与传统session或token的区别):
- 适合用于向Web应用传递一些非敏感信息如userId、isAdmin等,不能包含密码等敏感信息;
- 本身具备失效判断机制:根据串本身就能知道该token是否失效,而不用自己出来了;
- 服务端不需要存储token,而是分散给各个客户端存储,session机制则要。有利就有弊,jwt增加了计算开销如加解密,但总的利大于弊。
- 服务端能识别被篡改的token,所以只要token校验通过,就可以把里面封装的信息当成可信的。
- 由于jwt是分发到客户端存储的而服务端不需要存储,故很容易借之实现单点登录(假设需单点登录的各域名有共同顶级域名):只需要将含有JWT的Cookie的domain设置为顶级域名即可,各子域名站点就能够获得该JWT从而实现共享。
有利就有弊:
- 单纯地实验jwt存在问题:一个浏览器内(即一个session)可以多账号同时登录、一个账号可以在多个浏览器上同时登录,需要借助session等加以解决。
- jwt的一个很重要的特点或优点是使得服务端完全无状态(stateless),服务端无须存储认证相关信息了。但这也成为其缺点:用户注销时token不会立马失效,只能等签发有效期到期。可以通过Redis等为用户登出时的token维护一个黑名单来解决,但这就使得有状态了。
相比较于session/cookie, token能提供更加重要的好处:
1. CORS。
2. 不需要CSRF的保护。
3. 更好的和移动端进行集成。
4. 减少了授权服务器的负载。
5. 不再需要分布式会话的存储。
有一些交互操作会用这种方式需要权衡的地方:
1. 更容易受到XSS攻击
2. 访问令牌可以包含过时的授权声明(e。g当一些用户权限撤销)
3. 在claims 的数在曾长的时候,Access token 也能在一定程度上增长。
4. 文件下载API难以实现的。
5. 无状态和撤销是互斥的。
更好的方案-结合token和kookie:生成token后发给客户端并设置客户端cookie,之后请求优先检验请求头是否有token,没有的话从cookie取。这样对于浏览器端前端无需写带token的逻辑(通过cookie来让浏览器自动带上)、移动端则通过设置请求头token实现认证;而后端则可以兼容这两种场景。
使用示例
依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
代码:
1 import java.security.Key; 2 3 import io.jsonwebtoken.Claims; 4 import io.jsonwebtoken.ExpiredJwtException; 5 import io.jsonwebtoken.Jws; 6 import io.jsonwebtoken.Jwts; 7 import io.jsonwebtoken.MalformedJwtException; 8 import io.jsonwebtoken.SignatureAlgorithm; 9 import io.jsonwebtoken.SignatureException; 10 import io.jsonwebtoken.impl.crypto.MacProvider; 11 12 public class JWTtest { 13 14 public static void main(String[] args) { 15 // 生成jwt 16 Key key = MacProvider.generateKey();// 这里是加密解密的key。 17 String compactJws = Jwts.builder()// 返回的字符串便是我们的jwt串了 18 .setSubject("Joe")// 设置主题 19 .claim("studentId", 2)// 添加自定义数据 20 .signWith(SignatureAlgorithm.HS512, key)// 设置算法(必须) 21 .compact();// 这个是全部设置完成后拼成jwt串的方法 22 System.out.println("the generated token is: " + compactJws); 23 24 // 解析jwt 25 try { 26 27 Jws<Claims> parseClaimsJws = Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);// compactJws为jwt字符串 28 Claims body = parseClaimsJws.getBody();// 得到body后我们可以从body中获取我们需要的信息 29 // 比如 获取主题,当然,这是我们在生成jwt字符串的时候就已经存进来的 30 String subject = body.getSubject(); 31 System.out.println("the subject is: " + subject); 32 System.out.println("the studentId is: " + body.get("studentId")); 33 34 // OK, we can trust this JWT 35 36 } catch (SignatureException | MalformedJwtException e) { 37 // TODO: handle exception 38 // don't trust the JWT! 39 // jwt 解析错误 40 } catch (ExpiredJwtException e) { 41 // TODO: handle exception 42 // jwt 已经过期,在设置jwt的时候如果设置了过期时间,这里会自动判断jwt是否已经过期,如果过期则会抛出这个异常,我们可以抓住这个异常并作相关处理。 43 } 44 } 45 }