从零开发短视频电商 使用nimbus-jose-jwt进行对称签名和非对称签名的JWT实现

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 SHA256RSA

{
  "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的示例。

Encoded JWT

如何使用JSON Web Token

在身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web 令牌。需要注意的是,不应将JWT保存的时间超过必要的时间,因为它们是需要保护的凭据。此外,由于安全性的缺乏,应避免将敏感会话数据存储在浏览器存储中。

令牌传输:当用户想要访问受保护的路由或资源时,通常将JWT包含在请求中,放置在授权头部中,使用Bearer方案。头部的内容应如下所示:

Authorization: Bearer <token>

服务器的受保护路由会检查授权头部中是否存在有效的JWT,如果存在,则允许用户访问受保护的资源。如果JWT包含必要的数据,则可以减少某些操作对数据库的查询需求,尽管这并非总是如此。

请注意,如果通过HTTP头部发送JWT令牌,应尽量防止其过大。某些服务器不接受超过8 KB的头部。

工具库

可以在这个网址查找比较权威好用的工具库。

大部分都是使用上面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.");
}

总结

对称签名适用于以下情况:

  1. 快速性能要求:对称签名算法通常比非对称签名算法更快,因为它们使用相同的密钥进行签名和验证。
  2. 内部通信:当签名用于内部通信,不需要在不同的实体之间共享密钥时,对称签名是一种简便的选择。
  3. 密钥管理:对称签名只需要管理一个密钥,而非对称签名需要管理公钥和私钥对。

非对称签名适用于以下情况:

  1. 安全性要求:非对称签名提供更高的安全性,因为它使用不同的密钥进行签名和验证,私钥保持私密,公钥可公开共享。
  2. 跨网络通信:当签名用于跨网络通信,需要在不同的实体之间共享公钥时,非对称签名是更安全的选择。
  3. 数字证书:非对称签名用于生成和验证数字证书,以确保通信的身份验证和数据的完整性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lakernote

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值