本文将介绍一下JWT,希望看完对你有所帮助
JWT全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;
官网:https://jwt.io
JWT包含三部分数据:
Header:头部,通常头部有两部分信息:
我们会对头部进行base64编码,得到第一部分数据 base64编码和解码的
Payload:载荷,就是有效数据,一般包含下面信息:
这部分也会采用base64编码,得到第二部分数据
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法(不可逆的)生成一个签名。用于验证整个数据完整和可靠性。
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
服务器不需要记录客户端的状态信息,即:
带来的好处是什么呢?
无状态登录的流程:
流程图:
客户端请求登录,登录之后颁发凭证
整个登录过程中,最关键的点是什么?
token的安全性
token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。
采用何种方式加密才是安全可靠的呢?
我们将采用:JWT + RSA非对称加密
流程图:
步骤翻译:
因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。
加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
RSA算法历史:
1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA
目前流行的还有oauth2
nimbus-jose-jwt、jose4j、java-jwt 和 jjwt 是几个 Java 中常见的操作 JWT 的库。就使用细节而言,nimbus-jos-jwt(和jose4j)要好于 java-jwt 和 jjwt 。
nimbus-jose-jwt 官网:https://connect2id.com/products/nimbus-jose-jwt
所需坐标
<dependency>
<groupId>com.nimbusdsgroupId>
<artifactId>nimbus-jose-jwtartifactId>
<version>9.11.1version>
dependency>
这里我们需要了解下 JWT、JWS、JWE 三者之间的关系:
简单来说,JWT 和 JWS、JWE 类似于接口与实现类。由于,我们使用的是 JWS ,所以,后续内容中,就直接列举 JWS 相关类,不再细分 JWS 和 JWE 了,numbus-jose-jwt 中的 JWE 相关类和接口我们也不会使用到
nimbus-jose-jwt 支持的算法都在它的 JWSAlgorithm 和 JWEAlgorithm 类中有定义。
例如:JWSAlgorithm algorithm = JWSAlgorithm.HS256
在 nimbus-jose-jwt 中,使用 Header 类代表 JWT 的头部,不过,Header 类是一个抽象类,我们使用的是它的子类 JWSHeader 。
创建头部对象:
@Test
public void createToken(){
//创建头部对象
JWSHeader jwsHeader =
new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法
.type(JOSEObjectType.JWT) // 静态常量
.build();
System.out.println(jwsHeader);
}
你可以通过 .toBase64URL()
方法求得头部信息的 Base64 形式(这也是 JWT 中的实际头部信息):
使用 Payload 类的代表 JWT 的荷载部分
创建荷载部对象:
@Test
public void createToken(){
//创建头部对象
JWSHeader jwsHeader =
new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法
.type(JOSEObjectType.JWT) // 静态常量
.build();
System.out.println(jwsHeader);
//创建载荷
Payload payload = new Payload("hello world");
System.out.println(payload);
}
你可以通过 .toBase64URL()
方法求得荷载部信息的 Base64 形式(这也是 JWT 中的实际荷载部信息):
签名部分
签名部分没有专门的类表示,只有通用类 Base64URL ,而且签名部分并非你自己创建出来的,而是靠 头部 + 荷载部 + 加密算法
算出来的。在 nimbus-jose-jwt 中,签名算法由 JWSAlgorithm 表示。
注意:在创建 JWSHeader 对象时就需要指定签名算法,因为在标准中,头部需要保存签名算法名字。
用头部和荷载部分,再加上指定的签名算法和密钥来生成签名部分的过程,在 nimbus-jose-jwt 中被称为『签名(sign)』。nimbus-jose-jwt 专门提供了一个签名器 JWSSigner ,用来参与到签名过程中。密钥就是在创建签名器的时候指定的:
JWSSigner jwsSigner = new MACSigner("密钥"); //MACSigner()中要指定一个密钥
最终,整个 JWT 由一个 JWSObject 对象表示:
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 进行签名(根据前两部分生成第三部分)
jwsObject.sign(jwsSigner);
在 nimbus-jose-jwt 中 JWSObject 是有状态的:未签名、已签名和签名中。很显然,在执行完 .sign()
方法之后,JWSObject 对象就变成了已签名状态。
当然,我们最终『要』的是 JWT 字符串,而不是对象,这里接着对代表 JWT 的 JWSObject 对象调用 .serialize()
方法即可:
String token = jwsObject.serialize();
完整示例:
@Test
public void createToken() throws JOSEException {
//创建头部对象
JWSHeader jwsHeader =
new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法
.type(JOSEObjectType.JWT) // 静态常量
.build();
//创建载荷
Payload payload = new Payload("hello world");
//创建签名器
JWSSigner jwsSigner = new MACSigner("woniu");//woniu为密钥
//创建签名
JWSObject jwsObject = new JWSObject(jwsHeader, payload);// 头部+载荷
jwsObject.sign(jwsSigner);//再+签名部分
//生成token字符串
String token = jwsObject.serialize();
System.out.println(token);
}
如果出现:com.nimbusds.jose.KeyLengthException: The secret length must be at least 256 bits异常,是因为密钥的长度不够增加密钥长度即可
//创建签名器
JWSSigner jwsSigner = new MACSigner("woniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniu");
生成的token如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aGVsbG8gd29ybGQ.p1E7jMNXs4zzCHDDbFzQXOko6s9NtT7Sqt15-T-7KVY
/**
* 取出token中的内容
*/
@Test
public void getToken() throws IOException {
//获得token头部内容
String jwsHeader="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
//反编码
BASE64Decoder decoder=new BASE64Decoder();
byte[] txtByte= jwsHeader.getBytes(StandardCharsets.UTF_8);
System.out.println(new String(decoder.decodeBuffer(jwsHeader),"UTF-8"));
//获得token载荷中的内容
String payload="aGVsbG8gd29ybGQ";
System.out.println(new String(decoder.decodeBuffer(payload),"UTF-8"));
}
反向的解密和验证过程核心 API 就 2 个:JWSObject 的静态方法 parse 方法和验证其 JWSVerifier 对象。
JWSObject.parse()
方法是上面的 serialize 方法的反向操作,它可以通过一个 JWT 串生成 JWSObject 。有了 JWObject 之后,你就可以获得 header 和 payload 部分了。
如果你想直接验证 JWSObject 对象的合法性,你需要创建一个 JWSVerifier 对象。
//创建验证器
JWSVerifier jwsVerifier = new MACVerifier("密钥");//密钥要和加密时的相同
然后直接调用 jwsObject 对象的 verify 方法:
if (!jwsObject.verify(jwsVerifier)) {
throw new RuntimeException("token 签名不合法!");
}
案例二:验证token是否合法
/**
* 验证token
*/
@Test
public void validationToken() throws Exception {
//获得token
String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aGVsbG8gd29ybGQ.p1E7jMNXs4zzCHDDbFzQXOko6s9NtT7Sqt15-T-7KVYA";
//创建验证器
JWSVerifier jwsVerifier = new MACVerifier("woniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniu");
//获得签名对象
JWSObject jwsObject=JWSObject.parse(token);
//使用jws较验器,验证用户传递token是否合法
if (!jwsObject.verify(jwsVerifier)) {
throw new RuntimeException("token 签名不合法!");
}
}
/**
* jwt工具类
*/
@Component
public class JwtUtils {
//使用uuid生成密钥
private final String secret= UUID.randomUUID().toString();
//用户数据的key
private final String usernameKey="usernameKey";
/**
* 生成token
* @param username 用户名
* @return
*/
public String createJwtToken(String username) throws Exception {
//创建头部对象
JWSHeader jwsHeader =
new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法
.type(JOSEObjectType.JWT) // 静态常量
.build();
//创建载荷
Map<String,Object> map=new HashMap<String,Object>();
map.put(usernameKey, username);
Payload payload= new Payload(map);
//创建签名器
JWSSigner jwsSigner = new MACSigner(secret);//密钥
//创建签名
JWSObject jwsObject = new JWSObject(jwsHeader, payload);// 头部+载荷
jwsObject.sign(jwsSigner);//再+签名部分
//生成token字符串
return jwsObject.serialize();
}
/**
* 验证jwt token是否合法
* @param jwtStr
* @return
*/
@SneakyThrows
public boolean verify(String jwtStr) {
JWSObject jwsObject=JWSObject.parse(jwtStr);
JWSVerifier jwsVerifier=new MACVerifier(secret);
return jwsObject.verify(jwsVerifier);
}
/**
* 从token中解析出用户名
* @param jwtStr
* @return
*/
@SneakyThrows
public String getUserNameFormJwt(String jwtStr){
JWSObject jwsObject=JWSObject.parse(jwtStr);
Map<String,Object> map=jwsObject.getPayload().toJSONObject();
return (String) map.get(usernameKey);
}
}