Appearance
JWT
概述
什么是JWT
JWT,它代表JSON Web Token,是一种用于在网络应用之间安全传递信息的开放标准,通常JWT用于身份验证和非敏感数据的传递。设计JWT的主要目标是在不需要服务器端存储用户状态的情况下,安全的传递非敏感信息给受信任的实体,注意此处强调了非敏感信息
,为什么?因为常见的JWT信息并不加密,这意味着任何人截取JWT后,都能读取其中的内容。那么JWT的意义上什么?是为了防篡改。
或许你会有疑问,既然JWT代表JSON,为什么我们在实际使用中看到JWT都呈现这样的形式呢?没错,他确实是JSON,只是之后它又使用base64编码了。
Token由三部分组成,依次为header,payload,和signature,即头部,载荷,和签名,被两个点分割。
JWT Header
JWT Header
json
{
"alg": "HS256",
"typ": "JWT"
}
这是头部部分,包含了令牌的元数据,这里包括类型为JWT、签名算法为HS256,也就是HMAC SHA256,我们还可以选择512位的加密算法,当然更安全,但也消耗更多的计算资源,也有其他的算法可以选择。HS256是目前最常见的,
JWT Payload
JWT Payload
json
{
"name": "John Doe",
"admin": true,
"sub": "1234567890",
"iat": 1698222548,
"exp": 1698222548,
"jti": "89c99f90-3aa1-4a4f-9009-b2d48b357591"
}
这是载荷部分,包含了标准声明subject,表示用户ID,还有私有声明name和admin,各自表示用户名和是否为admin的标识。
所谓标准声明就是该开放标准预先定义好的一些字段。另外还有公共声明是自定义的声明,用于在特定应用程序中共享信息,私有声明用于在同意双方之间共享信息,通常不会被JWT规范定义,而是由应用程序自行定义和使用。一般我们应用中用到最多的就是标准声明和私有声明。
JWT Signature
JWT Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
256-bit-secret
)
再看最后一部分,这是什么鬼?别惊讶,这才是正常的。最后这部分是签名,如果我们能轻易的获取到,我们不是可以伪造Token了,这个部分需要将base64加密后的header和payload使用点连接组成字符串,然后通过Header中声明的加密方式,加盐后,也就是secret,组合加密而成。
注意,secret是保存在服务器端的,JWT的签发生成也是在服务端的,secret就是用来进行JWT的签发验证,所以在任何场景都不应该泄露。一旦客户端得知这个secret,那就意味着客户端可以随意的签发JWT了。
这里有必要提一下HMAC SHA256,这是一种对称加密算法,所谓对称加密,就是用一个密钥来加密解密的算法,因此只有掌握了密钥的实体,才能来验证JWT的合法性。正因为JWT的这一特性,它被广泛用于用户的认证和授权的场合。
有对称加密,就必然有非对称的加密。程序员最熟悉加密方式,应该是SSH Key,用于登录Linux服务器。SSH Key就是非对称密钥,由公钥和私钥组成,用公钥加密的密文只能用私钥解密,用私钥加密的密文只能用公钥解密。
除了对称加密,JWT也支持用非对称加密算法签发。
与JWT紧密相关的概念
- JWS,Java Web Signature
- JWE,Java Web Encryption
这两个都是开放标准,可以把它们俩理解成JWT的实现方式。
上面我们讲的,本质上都是JWS的实现方式,它的特点是只对内容做签名,确保其不被篡改,但是内容本身并不加密。而JWE会对内容本身加密,相对更安全,当然成本也会高一些。至于选择哪一种,要根据具体的使用场景来决定。
小结
关于什么是JWT,简单小结一下,常见的JWT,或者说JWS,它的Header和Payload,或者说头部和载荷,都是不加密的,它的目的并不是为了隐藏数据,而是为了防止数据篡改,这点是通过Signature,也就是签名来实现的。
应用场景
下面谈谈JWT的应用场景
用户身份验证
第一个应用场景是用于用户的身份验证,这应该是目前最常见的应用。用户通过用户名和密码登录后,系统签发给客户端一个JWT Token,这个Token保存了一些用户的基本信息以及权限相关的信息。后续客户端的请求带上Token,服务器端就知道当前访问客户是谁,拥有哪些权限。而不需要额外查询数据库。
比如,中央认证服务器CAS返回的服务令牌ST Token,也是一个JWT Token。应用程序在得到ST Token后,向中央认证服务器验证其真伪。通常JWT Token都是通过对称加密算法签名的,持有密钥的实体才能验证其真伪,而中央认证服务器持有该密钥。
同样的,中央认证服务器也可使用非对称加密算法实现签发和验证。这样客户端也能够确认Token是否为中央认证服务器签发的。
密码重置和Email验证
第二个应用场景是密码重置和电子邮件验证,用户请求密码重置和电子邮件验证后,服务器会生成包含用户信息的JWT,并组成一个链接,发往用户的邮箱,用户点击该链接后就可以重置密码,或完成电子邮件的验证。
当然这个场景不一定非得用JWT,还有其他的一些选项,JWT最普遍的场景是用户身份验证。
与其他认证方式的对比
除了JWT,其他常见的用户认证方式,还有基于Cookie,或基于API Key的认证机制。
其中基于Cookie的认证机制,通过Cookie来传递用户标识。
而基于API Key的认证机制,将HTTP中的Body和Header,通过非对称加密算法生成签名。验证通过则继续处理,否则返回403或者类似错误。这种方式已经被绝大多数的开放API采用,包括各云服务商提供的接口,以及开放银行接口等。
JWT跟另外两种方式最大的区别就是,JWT有状态,除了是一种标识以外,本身还可以承载用户的基本信息,而其他两种只是一个标识,本身不包含额外信息。因此客户端可以直接从JWT获取用户的基本信息,而不需要额外查询数据库。
对于另外两种方式,需要在服务器端维护对应的Session信息,当用户量比较大的时候,可能占用较大的内存空间。而对于高可用的部署环境,比如有分布式环境,为了保证在各个服务器之间的Session信息时刻保持同步,需要引入其他的机制来保存这些Session,比如Redis,这就增加了系统的复杂性。API Key机制一般都用于后端系统之间的API调用,而JWT既可以用于后端系统间的调用,也能用于前后端系统间的认证。而Cookie只能用于前后端系统间的认证。JWT和基于Cookie的机制一样,会为每个登录用户维护一个Session。而API Key是针对系统的,比如某个财务管理系统需要调用人力资源系统获取员工的信息,就会给财务系统分配一个API Key,所有登录财务系统的用户共用这一个API Key,当然理论上JWT也可以用于系统的认证,也正是因为JWT本身是有状态的,因此有一个比较大的弊端,他没法随时撤销某个已经签发的Token,也没法随时修改。
JWT场景的弊端
下面三个场景下会出现问题。使用JWT做身份验证的时候务必注意。
一是退出登录,他没法做到真正的后台的退出,因为签发的Token还是在有效期内。
二是用户信息调整后不能及时的反映,比如用户的name修改了,或者用户的角色变化了,但是之前签发的JWT Token还是生效状态,里面的信息还是修改前的。
第三是发现某个Token被坏人拿到了,服务器端也没有有效的手段,立马将Token置成无效。
再论与其他认证方式的对比
JWT和Cookie都是在认证通过后自动生成的,而API Key一般是预先分配的,当然如果你想要让API Key也是自动生成,技术上也是可行的,但一般不这样做,JWT及其他两种方式都有利弊,有各自最适合的场景,每种方式都有非常广泛的应用,因此没法简单说哪种好哪种不好。而且还有些平台会结合使用JWT和Cookie,将JWT Token保存在Cookie中,简化客户端的开发,而且也可以降低因客户端存放Token不合理导致的安全风险。
快速入门
导入依赖
io.jsonwebtoken:jjwt-api:0.9.1
javax.xml.bind:jaxb-api:2.3.1
com.sun.xml.bind:jaxb-impl:4.0.4
com.sun.xml.bind:jaxb-core:4.0.4
javax.activation:activation:1.1.1
junit:junit:4.13.2
加密(Encryption)
java
private String signature = "admin";
private long time = 1000 * 60 * 60 * 24;
@Test
public void testEncryption() {
JwtBuilder jwtBuilder = Jwts.builder();
String jwtToken = jwtBuilder
// header
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
// payload
.claim("name", "John Doe")
.claim("admin", true)
// payload subject(whom the token refers to)
// 一般用于表示用户id
.subject("1234567890")
// payload iat(issued at)
.issuedAt(new Date(System.currentTimeMillis() + time))
// payload exp(expiration time)
.expiration(new Date(System.currentTimeMillis() + time))
// payload jti(JWT ID)
.id(UUID.randomUUID().toString())
// signature
.signWith(SignatureAlgorithm.HS256, signature)
// 拼接
.compact();
System.out.println(jwtToken);
}
解密(Decryption)
java
private String signature = "admin";
private long time = 1000 * 60 * 60 * 24;
@Test
public void decryption() {
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwic3ViIjoiMTIzNDU2Nzg5MCIsImlhdCI6MTY5ODIyMzcxNCwiZXhwIjoxNjk4MjIzNzE0LCJqdGkiOiI1NzI2Yjk2Mi04MGIyLTQ2MTgtYmFiMi1mZjk3MTk2MDhhMjYifQ.IL7dEAWk1zz-RB4hf6MG0opJO7o2s3sZ5IP-GtTSPKQ";
JwtParser jwtParser = Jwts.parser();
Jws<Claims> claimJws = jwtParser.setSigningKey(signature).parseClaimsJws(token);
Claims claims = claimJws.getBody();
System.out.println(claims.get("name"));
System.out.println(claims.get("admin"));
System.out.println(claims.getIssuedAt());
System.out.println(claims.getExpiration());
System.out.println(claims.getId());
}