背景知识:
- 我们都知道http是无状态的,因此我们无法通过http来标识用户,而有些信息或者资源只能给特定的用户看,例如用户的信息等等,这时候就出现了会话技术。
- 会话:一个会话包含多个http请求,它的作用主要用于这些http请求之间数据的共享。
前面两篇文章介绍了session技术和cookie技术。在上文中我也提到了Session的几个弊端,例如Session信息存储在服务器端,当信息量很大的时候会占据很多服务器端的资源从而影响其他服务的性能,其次Session的扩展性不好,因为它的信息存储在服务器上,这就导致了下次请求必须再由该服务器作出响应,降低了分布式集群的优势。本文介绍一种更好的解决方案JWT用户认证。
JWT全称是Json Web Token,它由三个部分组成:Header头部,Payload载荷,Signature签名。下面分别介绍这三个部分内容。
Header头部:它一般由两个部分组成----typ定义类型,值为JWT;alg定义JWT使用的加密算法。如下:
{
"typ": "JWT",
"alg": "HS256" // 加密算法,默认值,也可以自定义为其他算法,例如md5等
}
我们需要对其进行Base64编码,关于Base64编码和解码不同的语言API各不相同,java如下:
// 这里有一个坑:json字符串在写的时候不要留空格!
String header = "{\"typ\":\"JWT\",\"alg\":\"HS256\"}";
String s = Base64.getEncoder().encodeToString(header.getBytes("UTF-8"));
System.out.println(s); // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload载荷:它一般由一些官方提供的有具体含义的字段和自定义字段组成。如下:
{
// 以下是官方提供的一些字段,有特殊的含义但这是非必须的
"iss": "wxsatellite", // 一般可以写JWT签发者
"iat": 1652323456, // 签发的时间
"exp": 1652328456, // JWT过期时间
"aud": "三斤", // 接收方
"sub": "主题" // JWT的主题
// 以下是自定义的字段
"uid": 1
}
同上,java如下:
String payload= "{\"iss\":\"wxsatellite\",\"iat\":1652323456,\"exp\":1652323456,\"aud\":\"三斤\",\"sub\":\"主题\",\"uid\":1}";
String s1 = Base64.getEncoder().encodeToString(payload.getBytes("UTF-8"));
Signature签名:由Header部分进行Base64编码得到的字符串拼接上"."再拼接上Payload部分进行Base64编码得到的字符串,以上三部分拼接得到的字符串再经过Header头部中定义的alg加密算法得到的字符串就是签名。需要注意的是:加密的时候需要加上盐,提高安全性。具体的公式:HMACSHA256( Base64(Header) + "." + Base64(Payload) + "服务端的密码盐" )。
得到上面三部分之后,再用点进行拼接,得到的字符串就是JWT了。以下是完整的测试用例:
public class Test {
public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
// 获得header
String header = "{\"typ\":\"JWT\",\"alg\":\"HS256\"}";
String s = Base64.getEncoder().encodeToString(header.getBytes("UTF-8"));
// 获得payload
String payload= "{\"iss\":\"wxsatellite\",\"iat\":1652323456,\"exp\":1652323456,\"aud\":\"三斤\",\"sub\":\"主题\",\"uid\":1}";
String s1 = Base64.getEncoder().encodeToString(payload.getBytes("UTF-8"));
// 获得签名
MessageDigest instance = MessageDigest.getInstance("SHA-256");
String slat = "wxsatellite"; // 密码盐
String s3 = s + "." + s1 + slat;
instance.update(s3.getBytes());
String signature = Test.byteArrayToHex(instance.digest());
// 获取jwt
String jwt = s + "." + s1 + "." + singnature;
}
public static String byteArrayToHex(byte[] byteArray) {
// 首先初始化一个字符数组,用来存放每个16进制字符
char[] hexDigits = {'0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F' };
// new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方))
char[] resultCharArray =new char[byteArray.length * 2];
// 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b>>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b& 0xf];
}
// 字符数组组合成字符串返回
return new String(resultCharArray);
}
}
以上大致讲述了JWT的三个组成部分和如何使用Java生成JWT。接下来我们来说说几个注意事项:
1. Base64算法可以轻松的加解密,因此严格来说它并不是一个加密的过程,而仅仅是一个编码的过程。
2. 关于Payload这个部分,我们可以存入一些用来维护会话的信息,例如上面的例子中,存储了uid的值,其实相当于将本来存储在Session中的信息存储到了Payload中(主要用于存储一些不敏感的数据)。
3. 在生成签名的时候我们增加了密码盐,这是因为JWT字符串是暴露给用户的,并且生成的格式都是公开的,拿到这个字符串的用户可以很轻松的对头部和载荷信息进行解码,然后对信息进行篡改之后再编码返回给服务器,这就导致了安全性的问题。签名加盐的作用就是为了保证JWT的安全性和完整性,即使恶意用户修改了Header和PayLoad,并针对修改后的Header和Payload再生成签名拼接成JWT返回给服务器时,由于恶意用户不知道盐,服务器很容易可以判断出JWT是否合法。(根据用户传递的Header部分和Payload部分,加上密码盐生成Signature签名和用户传递的Signature签名进行比较,不想等则不合法。)
4. 相比于Session,JWT一个明显的优势就是将用户的登陆状态分散到了客户端,而不像Session将用户的登陆信息存储在了服务器端,因此它不会导致一个问题:当用户基数变大时会大量占用服务器端的资源。
了解了什么JWT以及它的一些注意事项,那么JWT如何在Web应用程序中使用呢,我简单画了一个草图,供大家参考:
今天的分享就结束啦,感谢阅读咯。