5分钟带你了解JWT
目录
JWT详解
jwt介绍:
jwt产生的原因:
JWT格式:
JWT的构成:
Header
Payload
Signature
Base64URL算法:
生成jwt信息的拼接:
token到jwt的转换:
JWT用法:
JWT存在的问题:
JWT详解
jwt介绍:
Json web token (JWT),旨在通过网络应用环境实现身份信息的安全传递,并采用JSON格式进行编码(参考RFC 7519)。作为一种经过精心设计与优化的技术方案,在现代分布式系统中发挥着关键作用。
该标准经过深入研究与完善,在实际应用中展现出极高的可靠性和灵活性。
JWT的核心功能在于实现身份认证与信息传递,并广泛应用于分布式系统中的单点登录场景。
其核心功能在于实现身份认证与信息传递。
同时支持多种加密机制以确保传输过程的安全性。
这种开放标准不仅提升了数据传输的安全性,
而且在实际应用中展现出极高的可靠性和灵活性,
特别是在多端异构环境下的统一通信需求下发挥着越来越重要的作用。
可以用一句话来简明扼要地说明。jwt 作为一种 Token ,且为无状态 Token 。服务器无需存储并发放用于验证用户身份的 Token 。它能够通过自身强大的加密解密能力实现对分布式站点上用户身份进行单一认证,并通过这种机制使得分布式系统中的身份认证更加高效和便捷。
可以用一句话来简明扼要地说明。jwt 作为一种 Token ,且为无状态 Token 。服务器无需存储并发放用于验证用户身份的 Token 。它能够通过自身强大的加密解密能力实现对分布式站点上用户身份进行单一认证,并通过这种机制使得分布式系统中的身份认证更加高效和便捷。
jwt产生的原因:
说起这个,必须从传统web服务验证客户端身份的流程说起。
第一阶段:
我们都知道HTTP协议的本质是不依赖会话的状态机制,即每当客户端发起一次请求时都需要提供当前登录者的用户名和密码信息,这种处理流程在软件初创阶段还能够应对一定的需求,但随着技术的发展这种方法逐渐被更智能的认证机制取代
第二阶段
开发团队为了使我们的应用程序能够确定是由哪个用户发起的请求而精心策划了一项技术措施 即在服务器端记录每位用户的登录数据 这些数据会在服务响应时传输到客户端设备 客户端系统需接收并存储这些数据 以便下一次请求时能够被服务器识别 这种安排从而简化了多次认证的过程 这种方法即为基于会话(session)的传统认证方案
第三阶段
伴随着互联网技术的发展推动了电子商务行业的蓬勃发展显著提升了人们的日常生活质量
JWT格式:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的构成:
正如所诉,在线测试中展示了该功能的具体实现方式
Header
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,
alg属性标识签名采用的算法,
默认是 HMAC SHA-256;
typ属性指定该令牌
的类型,
JWT 类型的标记均为JWT。
最后,
将上述JSON对象通过
Base64URL编码算法
(具体内容请参见后文)
转换为字符串形式。
Payload
iss (issuer):签发人
exp (expiration time):过期时间 //在校验jwt是否过期时就是用这个时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"useId": "1",
"phone": "xxxxxxx"
}
鉴于负载信息较为丰富,在实际应用开发过程中可能会需要定义较多的私有字段以满足特定需求,在传输效率方面考虑到了这一点
需要注意的是,在这一区域通常不推荐将敏感数据以明文形式存储。原因在于base64编码采用了对称加密机制(Symmetric Encryption),这表明这类数据应当被视为明文信息进行处理。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
为了确保数据安全起见,请先指定一个秘密密钥。该密钥必须由服务器持有,并且严格保密不得外泄。随后,请根据文档中的指定部分选择签名算法,默认情况下采用HMAC SHA256算法,并依据以下公式生成签名:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
在计算出签名之后,在头字段、负载字段和签名字段之间插入"点"(.)作为分隔符,并将这三个字段连接为一个完整的字符串序列后就可以返回给客户端
Base64URL算法:
有时前后端在交互过程中可能会将JWT编码结果放置于URL路径上(如api.example/v1/api?token=abc123)。 Base64编码中所使用的三个关键字符是'+'、'/'和'='。 但由于在URL路径中有特殊含义,在生成Base64 URL时会对这三个字符进行替换处理:'='被移除;'+'被替换成'-';'/'则被替换成 '_' 。 即使经过这样的处理后也能确保数据传输的安全性和可读性。
// 编码的时候替换
@Override
public String encode(byte[] data) {
String base64Text = TextCodec.BASE64.encode(data);
byte[] bytes = base64Text.getBytes(US_ASCII);
// base64url encoding doesn't use padding chars:
bytes = removePadding(bytes);
// replace URL-unfriendly Base64 chars to url-friendly ones:
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] == '+') {
bytes[i] = '-';
} else if (bytes[i] == '/') {
bytes[i] = '_';
}
}
return new String(bytes, US_ASCII);
}
// 解码的时候还原
@Override
public byte[] decode(String encoded) {
char[] chars = encoded.toCharArray(); //always ASCII - one char == 1 byte
// Base64 requires padding to be in place before decoding, so add it if necessary:
chars = ensurePadding(chars);
// Replace url-friendly chars back to normal Base64 chars:
for (int i = 0; i < chars.length; i++) {
if (chars[i] == '-') {
chars[i] = '+';
} else if (chars[i] == '_') {
chars[i] = '/';
}
}
String base64Text = new String(chars);
return TextCodec.BASE64.decode(base64Text);
}
生成jwt信息的拼接:
@Override
public String compact() {
// payload和claims只能二选一
if (payload == null && Collections.isEmpty(claims)) {
throw new IllegalStateException("Either 'payload' or 'claims' must be specified.");
}
if (payload != null && !Collections.isEmpty(claims)) {
throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one.");
}
// key和keyBytes只能二选一
if (key != null && keyBytes != null) {
throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either one.");
}
Header header = ensureHeader();
Key key = this.key;
if (key == null && !Objects.isEmpty(keyBytes)) {
key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
}
JwsHeader jwsHeader;
// 解析Header
if (header instanceof JwsHeader) {
jwsHeader = (JwsHeader)header;
} else {
jwsHeader = new DefaultJwsHeader(header);
}
if (key != null) {
jwsHeader.setAlgorithm(algorithm.getValue());
} else {
//no signature - plaintext JWT:
jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());
}
if (compressionCodec != null) {
jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName());
}
String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json.");
String base64UrlEncodedBody;
// 解析负载信息
if (compressionCodec != null) {
byte[] bytes;
try {
bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Unable to serialize claims object to json.");
}
base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes));
} else {
base64UrlEncodedBody = this.payload != null ?
TextCodec.BASE64URL.encode(this.payload) :
base64UrlEncode(claims, "Unable to serialize claims object to json.");
}
// 拼接Header和Payload
String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody;
// 获取Signature
if (key != null) { //jwt must be signed:
JwtSigner signer = createSigner(algorithm, key);
String base64UrlSignature = signer.sign(jwt);
jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature;
} else {
// no signature (plaintext), but must terminate w/ a period, see
// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1
jwt += JwtParser.SEPARATOR_CHAR;
}
return jwt;
}
部分不做详细的阐述, 其原理较为基础, 通过多种判断依据, 最终采用以下方式整合: 将Header字段与Payload字段进行结合, 并随后与Signature字段进行融合连接
token到jwt的转换:
在DefaultJwtParser的parse方法中可以看到具体逻辑,在DefaultJwtParser中解析过程如下:首先将token分解为三个部分分别进行验证;随后调用与JWT相关的类中的解密、解压等功能来恢复相关信息;当信息还原成功时提取失效时间等关键参数用于JWT验证;验证通过则允许用户的请求处理完成;否则会返回错误并终止处理流程。
该系统通过Claims类实现了对Payload数据的存储功能。如前所述,在实现细节中提到用户可以在Payload这里自定义属性的原因是由于Claims类实现了Map<String, Object>接口,在开发使用上带来了更加灵活的功能扩展。
JWT用法:
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
随后,在与服务器的交互过程中,客户端都将携带JWT。若将其保存于Cookie中,则可实现自动发送功能;然而这会受到跨域限制的影响而无法实现;因此通常的做法是将其放置于HTTP请求的Header Authorization字段内(Authorization: Bearer)。当涉及跨域操作时,则可考虑将JWT嵌入至POST请求的数据部分。

JWT存在的问题:
服务器无法废弃令牌
在使用过程中无法实现会话状态的持久化,在需要时无法实现会话状态的持久化这会导致无法实现会话管理功能这会导致无法实现会话管理功能这将导致系统在处理完一个会话后就不再能够处理后续相关的会话问题
此问题同样具备相应的解决办法,并可借助缓存组件来实现。具体而言,在每一次生成一个JWT token的过程中,请确保同时也会产生一个refreshToken(采用UUID方式进行编码即可),并将该token以及其失效时间一同嵌入到jwt之中。当用户退出登录状态时,则应删除系统预设缓存(如Redis)中的相应 refreshToken记录;而如果发现某个用户的 jwt 格式已发生变更,则应同步修改该用户的 Redis 存储项中的 refreshToken失效时间。每当客户端进行验证操作时,请检查其携带的 refreshToken 与 Redis 中记录的时间是否一致;如果二者存在不匹配现象,则表明该用户的 jwt 格式已被修改过,请予以拒绝处理。
用上面的解决方案也可以让服务端具备强制让用户下线的能力。具体而言,可以通过将JWT的有效时间设定得较短、同时将refreshToken的有效时间设定得较长,在客户端发起请求时,若判断JWT已过期,则从Redis中查询该用户的refreshToken是否仍在有效期内。如果查询到有效的refreshToken,则会重新生成一个JWT给客户端使用。当服务器检测到用户的登录行为出现异常或其他异常情况时,则会删除Redis中存储的当前用户信息的refreshToken条目,从而强制该用户下线。
为了考虑到信息安全的重要性,jwt的有效时间最好避免设置过长,在每个关键操作中都进行身份验证。
