文章目录
- 什么是JSON Web Token
- 何时使用JSON Web Token
- JSON Web Token的结构是什么
- 头部(Header)
- 负载(Payload)
- 签名(Signature)
- 拼接起来
- 如何使用JSON Web Token
- 工具库
- 依赖
- 流程
- 对称签名
- 非对称签名
- 总结
JWT的基础介绍可以参见这个地址:https://jwt.io/introduction,下面都是从上面摘录的。
什么是JSON Web Token
JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于安全地在各方之间传输信息,其格式为JSON对象。这些信息可以被验证和信任,因为它们是数字签名的。
JWT可以使用对称算法(使用HMAC算法)或非对称算法(使用RSA或ECDSA的公钥/私钥)对进行签名。
尽管JWT可以加密以实现机密性(JWK),但我们更多使用的是签名令牌(JWS)。
- 签名令牌可以验证声明的完整性,而加密令牌则隐藏这些声明,不让其他方看到。
- 使用公钥/私钥对进行签名时,签名还证明只有持有私钥的一方才是签名者。
何时使用JSON Web Token
以下是一些适用于JSON Web Tokens的常见场景:
-
授权:这是使用JWT的最常见场景。一旦用户登录,每个后续请求都会包含JWT,允许用户访问受该令牌允许的路由、服务和资源。单点登录是一种广泛使用JWT的功能,因为它的开销很小,并且可以轻松地在不同域之间使用。
-
信息交换:JSON Web Tokens是安全地在各方之间传输信息的良好方式。因为JWT可以进行签名,例如使用公钥/私钥对,所以您可以确信发送方就是它们所声称的那个。此外,由于签名是使用头部和负载计算的,您还可以验证内容是否被篡改。
JSON Web Token的结构是什么
在其紧凑形式中,JSON Web Token由三个由点(.)分隔的部分组成,它们是:
- 头部(Header)
- 负载(Payload)
- 签名(Signature)
因此,一个JWT通常为:xxxxx.yyyyy.zzzzz
让我们逐个解释不同的部分。
头部(Header)
头部通常由两部分组成:令牌类型(JWT)和所使用的签名算法,例如HMAC SHA256
或RSA
。
{
"alg": "HS256",
"typ": "JWT"
}
然后,对该 JSON 进行Base64Url编码以形成 JWT 的第一部分。
负载(Payload)
令牌的第二部分是有效载荷,其中包含声明(claims)。声明是关于实体(通常是用户)和附加数据的陈述。有三种类型的声明:注册声明、公共声明和私有声明。
- 注册声明:这是一组预定义的声明,它们不是强制性的,但建议使用,以提供一组有用的、可互操作的声明。其中一些包括:iss(发行人)、exp(过期时间)、sub(主题)、aud(受众)等。
请注意,声明名称只有三个字符长,因为JWT旨在紧凑。
-
公共声明:这些声明可以由使用JWT的人自由定义。但为了避免冲突,它们应在IANA JSON Web Token注册表中定义,或者定义为包含冲突安全命名空间的URI。
-
私有声明:这些是自定义的声明,用于在同意使用它们的各方之间共享信息,既不是注册声明也不是公共声明。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后通过Base64Url编码,形成JSON Web Token的第二部分。
请注意,对于已签名的令牌,尽管受到篡改的保护,但任何人都可以读取此信息。除非进行加密,否则不要将机密信息放在JWT的有效载荷或头部元素中。
签名(Signature)
要创建签名部分,您需要获取编码的头部、编码的有效载荷、一个密钥、头部中指定的算法,并对其进行签名。
例如,如果您想使用HMAC SHA256算法,签名将按以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在传递过程中是否被更改,并且对于使用私钥签名的令牌,还可以验证JWT的发送者身份。
拼接起来
输出是由点分隔的三个Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,与基于XML的标准(如SAML)相比更紧凑。
以下是一个具有先前编码的头部和有效载荷,并使用密钥签名的JWT的示例。
如何使用JSON Web Token
在身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web 令牌。需要注意的是,不应将JWT保存的时间超过必要的时间,因为它们是需要保护的凭据。此外,由于安全性的缺乏,应避免将敏感会话数据存储在浏览器存储中。
令牌传输:当用户想要访问受保护的路由或资源时,通常将JWT包含在请求中,放置在授权头部中,使用Bearer方案。头部的内容应如下所示:
Authorization: Bearer <token>
服务器的受保护路由会检查授权头部中是否存在有效的JWT,如果存在,则允许用户访问受保护的资源。如果JWT包含必要的数据,则可以减少某些操作对数据库的查询需求,尽管这并非总是如此。
请注意,如果通过HTTP头部发送JWT令牌,应尽量防止其过大。某些服务器不接受超过8 KB的头部。
工具库
可以在这个网址查找比较权威好用的工具库。
- maven: com.auth0 / java-jwt / 3.3.0
- maven: io.jsonwebtoken / jjwt-root / 0.11.1
- maven: com.nimbusds / nimbus-jose-jwt / 5.7
大部分都是使用上面3个中的某一个,我们这里使用nimbus-jose-jwt。
文档:https://connect2id.com/products/nimbus-jose-jwt
依赖
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.31</version>
</dependency>
<!-- 如果您正在使用以下情况,请取消注释下面的依赖项:
- JDK 10或更早版本,并且您想要使用RSASSA-PSS(PS256、PS384、PS512)签名算法。
- JDK 10或更早版本,并且您想要使用EdECDH(X25519或X448)椭圆曲线迪菲-赫尔曼密钥交换加密。
- JDK 14或更早版本,并且您想要使用EdDSA(Ed25519或Ed448)椭圆曲线签名算法。
在JDK 15或更高版本上,这些算法是不必要的。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
<scope>runtime</scope>
</dependency>
-->
最新版本
流程
对称签名
加签示例:
// 生成对称加密密钥
byte[] sharedKey = "YourSharedKey-122345678sahkjhjkasdfasdf".getBytes();
// 创建一个JWT对象
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().subject("user123")
// 设置过期时间为当前时间后的一分钟
.expirationTime(new Date(System.currentTimeMillis() + 60 * 1000)).build();
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
// 创建HMAC签名器
JWSSigner signer = new MACSigner(sharedKey);
// 对JWT进行签名
signedJWT.sign(signer);
// 将JWT序列化为字符串
String jwtString = signedJWT.serialize();
System.out.println("JWT Token: " + jwtString);
验签示例
// 生成对称加密密钥
byte[] sharedKey = "YourSharedKey-122345678sahkjhjkasdfasdf".getBytes();
// 解析JWT字符串
String jwtString = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODg1NDY2MDAsInN1YiI6InVzZXIxMjMifQ.3mGtNjwt6Z50DhEeBv2zo9qi8aHGh9Mu2RWLVeH0FE8";
SignedJWT signedJWT = SignedJWT.parse(jwtString);
// 创建HMAC验证器
JWSVerifier verifier = new MACVerifier(sharedKey);
// 验证JWT签名
boolean isValid = signedJWT.verify(verifier);
if (isValid) {
System.out.println("JWT signature is valid.");
// 获取JWT的声明
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
System.out.println("Subject: " + claimsSet.getSubject());
System.out.println("Expiration Time: " + claimsSet.getExpirationTime());
if (!signedJWT.getJWTClaimsSet().getExpirationTime().after(new Date())) {
System.out.println("JWT signature is expired.");
}
} else {
System.out.println("JWT signature is not valid.");
}
非对称签名
RSA算法和ECDSA (Elliptic Curve Digital Signature Algorithm)算法是常用的非对称加密算法,用于生成和验证数字签名。
RSA算法是基于大素数分解的数论问题。它使用一对公钥和私钥来进行加密和解密操作,同时也可以用于生成和验证数字签名。RSA算法在安全性和广泛应用上都有很好的表现,但由于其计算复杂性,对于大数据量的加密和解密操作可能会比较耗时。
ECDSA算法基于椭圆曲线离散对数问题。相比于RSA算法,ECDSA算法使用更短的密钥长度,提供相同的安全性水平。这使得ECDSA算法在资源受限的环境中更具优势,如移动设备和物联网设备。ECDSA算法还具有更快的加密和解密速度。
如何生成EC384公私钥
方式一
// Generate an EC key pair
ECKey ecJWK = new ECKeyGenerator(Curve.P_384)
.keyID("123")
.generate();
ECPublicKey ecPublicKey = ecJWK.toECPublicKey();
ECPrivateKey ecPrivateKey = ecJWK.toECPrivateKey();
// 将公钥编码为Base64字符串
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
// 将私钥编码为Base64字符串
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKey.getEncoded())
// 或者
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
// 生成ECDSA密钥对
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(384); // 使用EC384曲线
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 获取私钥和公钥
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
// 打印私钥和公钥
System.out.println("Private Key: " + privateKey);
System.out.println("Public Key: " + publicKey);
// 将公钥编码为Base64字符串
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
// 将私钥编码为Base64字符串
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKey.getEncoded())
publicKeyBase64: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVT+YNmKBnvXtS11FvcKe7tBHi3aAbvk87+tBGFadfHM/zy1+Q4EjlXjbLhhl1LNPup5BHhQBG+jKRP0/Rvoy0LiNmDdX9MqC0xvTtefFKBL4CsM0vlViObOUNxumzxMH
privateKeyBase64: ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDDAaCeLDnCRmkmZ8vs7nlnApCxBIL2RyizpY4jh1VE5Svr4d92AwjZyrt5Szl8AvPE=
方式二
openssl ecparam -list_curves
# 生成私钥
openssl ecparam -genkey -name secp384r1 -noout -out ec384-private.pem
# 根据私钥生成公钥
openssl ec -in ec384-private.pem -pubout -out ec384-public.pem
# 把私钥转换为PKCS8格式
openssl pkcs8 -topk8 -nocrypt -in ec384-private.pem -out ec384-private.pem_pkcs8.pem
# 注意
publicKeyBase64 = ec384-public.pem中的字符串
privateKeyBase64 = ec384-private.pem_pkcs8.pem中的字符串
序列化,反序列化和传输公私钥
注意,私钥一定是颁发者自己好好保存,公钥的话无所谓,公钥本来就是要公开的。可以通过微信邮件等传输。
ECDSA公钥可以以多种格式进行存储和传输。以下是使用Base64编码的示例:
// 假设已经有Base64编码的公钥和私钥字符串
String publicKeyBase64 = "YourBase64EncodedPublicKey";
String privateKeyBase64 = "YourBase64EncodedPrivateKey";
// 将Base64字符串解码为字节数组
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);
// 创建公钥的KeySpec对象
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
// 创建私钥的KeySpec对象
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
// 使用KeyFactory生成公钥和私钥对象
KeyFactory keyFactory = KeyFactory.getInstance("EC");
ECPublicKey publicKey = (ECPublicKey) keyFactory.generatePublic(publicKeySpec);
ECPrivateKey privateKey = (ECPrivateKey) keyFactory.generatePrivate(privateKeySpec);
System.out.println("Public Key: " + publicKey);
System.out.println("Private Key: " + privateKey);
加签示例:
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
// 创建一个JWT对象
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("user123")
.expirationTime(new Date(new Date().getTime() + 60 * 1000)) // 设置过期时间为当前时间后的一分钟
.build();
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES384)
.build();
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
// 创建ECDSA私钥签名器
JWSSigner signer = new ECDSASigner(privateKey);
// 对JWT进行签名
signedJWT.sign(signer);
// 将JWT序列化为字符串
String jwtString = signedJWT.serialize();
System.out.println("JWT Token: " + jwtString);
验签示例
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
// 解析JWT字符串
SignedJWT signedJWT = SignedJWT.parse(jwtString);
// 创建ECDSA公钥验证器
JWSVerifier verifier = new ECDSAVerifier(publicKey);
// 验证JWT签名
boolean isValid;
try {
isValid = signedJWT.verify(verifier);
} catch (JOSEException e) {
isValid = false;
}
if (isValid) {
System.out.println("JWT signature is valid.");
// 获取JWT的声明
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
System.out.println("Subject: " + claimsSet.getSubject());
System.out.println("Expiration Time: " + claimsSet.getExpirationTime());
} else {
System.out.println("JWT signature is not valid.");
}
总结
对称签名适用于以下情况:
- 快速性能要求:对称签名算法通常比非对称签名算法更快,因为它们使用相同的密钥进行签名和验证。
- 内部通信:当签名用于内部通信,不需要在不同的实体之间共享密钥时,对称签名是一种简便的选择。
- 密钥管理:对称签名只需要管理一个密钥,而非对称签名需要管理公钥和私钥对。
非对称签名适用于以下情况:
- 安全性要求:非对称签名提供更高的安全性,因为它使用不同的密钥进行签名和验证,私钥保持私密,公钥可公开共享。
- 跨网络通信:当签名用于跨网络通信,需要在不同的实体之间共享公钥时,非对称签名是更安全的选择。
- 数字证书:非对称签名用于生成和验证数字证书,以确保通信的身份验证和数据的完整性。