使用 JJWT 库的 Java JWT 令牌教程
本文介绍如何使用 JJWT 库、密钥库、私钥/公钥来加密和解密 JWT 令牌。
引言
在本文中,我将提供一个非常简单的教程,介绍如何创建 JWT 令牌、如何加密令牌以及如何解密令牌。本文将简单有趣。虽然我不是主题专家,但我将展示以下内容
- 如何创建 Java 密钥库
- 如何从密钥库文件中提取公钥
- 如何创建 JWT 令牌
- 如何使用密钥库中的私钥加密 JWT 令牌
- 如何使用导出的公钥解密加密的 JWT 令牌
你可能会问,这样做有什么目的?我有一个个人项目,将实现为一套微服务。其中一个微服务将是一个认证服务。当请求进入我的微服务生态系统时,请求将首先得到认证。用户凭据将首先发送到认证和授权服务,该服务可以在数据库中查找用户并验证凭据,然后发送一个包含用户信息的加密 JWT 令牌。负责处理请求的服务将在请求被处理之前解密 JWT 令牌并验证用户的授权。授权检查将查看用户的角色以及用户角色是否足以处理请求。
为了做到这一切,我们需要一对密钥——RSA 私钥/公钥对。认证服务使用私钥来加密令牌。处理用户请求的服务需要解密令牌,可以使用公钥来完成。在此过程中,我们需要找到一种方法来创建未加密的 JWT 令牌。所有这些将在本文中进行解释。
创建 Java 密钥库
Java 密钥库文件是证书(公钥)和相应私钥的存储库。它们可用于 SSL 传输。您可能会注意到的一个 Java 密钥库文件的例子是,J2EE 应用程序容器(如 Jetty、Glassfish、TomCat)可以通过某些 XML 配置使用它。然后,您将为这些应用程序容器启用 HTTPS。
对于本教程,我们只需要知道如何创建一个 Java 密钥库文件,其中包含一个 RSA 证书及其对应的私钥。要创建一个新的 Java 密钥库文件,请使用以下命令
keytool -genkey -alias hanbotest -keyalg RSA -keystore hanbotest.jks -keysize 2048
上述命令的作用是创建一个包含一个 RSA 密钥的密钥库。密钥的别名是“hanbotest”。用于创建密钥的算法是 RSA。密钥的大小是 2048 位。密钥库的默认格式是 JKS。
键入命令并按 Enter 键后,它会提示您输入以下信息
- 密钥库的密码,我使用的是“123test321”。
- 再次输入相同的密码。
- 名字和姓氏,我输入的是“Han Bo”。
- 组织单位名称,我输入的是“Hanbo Enterprise”。
- 组织名称,我再次输入“Hanbo Enterprise”。
- 城市名称,我输入的是“Chicago”。
- 州或省名称,我输入的是“IL”。
- 国家的两个字符,我输入的是“US”。
- 最后一个是“[no]”,我输入“yes”并按 Enter 键。
- 最后的最后是证书和私钥的密码。我将其与密钥库的密码设置为相同。只需按 Enter 键。
- 查看目标目录,您将看到密钥库文件。
不用担心您输入的信息。这是一个模拟证书,仅用于测试。我已经将这个模拟密钥库包含在示例源代码中。一旦获得它,您就可以将其替换为您自己的密钥库文件并进行源代码的演示。
导出公钥/证书
现在您有了一个密钥库文件。下一步是将公钥导出为一个单独的实体。私钥是需要保护的,而公钥可以与其他方共享。要提取公钥,请使用以下命令
keytool -export -keystore hanbotest.jks -alias hanbotest -file hanbotest.cer
此命令的作用是指定 jks 文件、密钥的别名以及输出文件,该文件名为“hanbotest.cer”。键入命令并按 Enter 键后,它会提示输入密码。输入密码“123test321”。.cer 文件将成功创建。
现在它们可用于 JWT 令牌的加密和解密。
加密和解密 JWT 令牌
我编写了一个简单的 Java 控制台应用程序,演示了使用私钥加密 JWT 令牌。加密后它将是一个string。然后我取string,并使用公钥解密加密的令牌。
POM 文件
由于这是一个简单的 Java 控制台程序,POM 文件非常直观。最重要的是依赖项。它们在这里
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-core</artifactId>
  <version>${jackson.version}</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>${jackson.version}</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-annotations</artifactId>
  <version>${jackson.version}</version>
</dependency>
jackson 库的版本是 2.7.3。我在同一 pom 文件中的一个单独部分中定义了它,如下所示
<properties>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <jackson.version>2.7.3</jackson.version>
</properties>
关于这一点还有一件事,在 JJWT 官方库教程中,它说不需要包含任何其他依赖项。这是不正确的。我不得不包含三个 jackson jar 文件。否则应用程序将无法工作。
主程序
主程序非常简单。它是一个类,带有一个主入口和一个static辅助方法。它所做的就是创建 JWT 令牌,加密,然后解密。目的是演示 JWT 令牌的创建和保护方式。
我使用JJWT库。创建 JWT 令牌非常容易。这是代码
String compactJws = Jwts.builder()
   .setSubject("Joe")
   .setAudience("testAudienceId")
   .setExpiration(dateExp)
   .setIssuedAt(dateNow)
   .setId("testuserid")
   .signWith(SignatureAlgorithm.RS512, key)
   .compact();
   
System.out.println(compactJws);
有两件事您必须注意
- setExpiration()应该设置一个比- setIssuedAt()的- Date对象更晚的- Date对象。
- signWith()使用私钥对 JWT 令牌进行签名。这就是我们应该使用私钥的地方。
问题是,我们如何加载私钥。结果发现,这并不难
String jksPassword = "123test321";
         
KeyStore ks  = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(new FileInputStream("C:\\DevJunk\\jwt\\keystore\\hanbotest.jks"), jksPassword.toCharArray());
Key key = ks.getKey("hanbotest", jksPassword.toCharArray());
第一行定义了代表密码的string。第二行获取一个KeyStore实例,并指定使用的类型是默认类型。JKS 有许多不同的类型,您可以使用。除了 Java 开箱即用的默认类型外,还有 PKCS12 格式以及许多其他格式。在这种情况下,我只是使用默认类型。第三行从 JKS 文件加载KeyStore。要加载,必须提供与 JKS 文件关联的密码。最后一行从KeyStore对象中提取私钥。名为“key”的对象是在前面的代码示例中signWith()使用的私钥。
如果只运行带有这两段代码的程序,您将看到类似以下的输出
eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJKb2UiLCJhdWQiOiJ0ZXN0QXVkaWVuY2VJZCIsImV4cCI6MTUzMjM5OTI4NCwiaWF0I
joxNTMyMTQwMDg0LCJqdGkiOiJ0ZXN0dXNlcmlkIn0.L9w54tGTWGa720T1TqCf7T5c-JnT0RI1kyK1vUmuuEfcX8wmwWnvZk2
Ybm4eEyXZHP5tBLWHtCVdZOPqLwdjDzZpr10Q5AmjasTE9FJqnzQVLvd5cJOKFK9VAXAc_hA4ZxP6mTM802vssNGfuVseqFg1a
r08uD-jbRRnN8DqegzEV9z0yJC-TNuUUkQBo5Rj2GKVKWyTi5MkPPL2aPsY7JExDj7QraYvaKl57XBwT2XRGefOaA0mXuCxcJj
xZv638LupQl9c1HcS7qTOSlV7V-h-QLqanDXZkKt-xhwR1hilEPCYjVyGPSDMWVYlLtxSNWXvFji1TI2baFD7i9fpOQ
string包含三个加密部分。它们由两个“.”分隔。这些部分是令牌头、正文和签名。最后一部分可用于验证 JWT 令牌是由合法的私钥生成的。
程序的下一部分是将string解密回 JWT 令牌对象。这是使用公钥完成的。为了做到这一点,我们首先需要加载公钥。这是本教程中唯一困难的部分。我写了一个非常简单的辅助方法
public static PublicKey loadPublicKey(String filename)
   throws Exception
{
   CertificateFactory cf = CertificateFactory.getInstance("X.509");
   Certificate cert = cf.generateCertificate(new FileInputStream(filename));
   PublicKey retVal = cert.getPublicKey();
   return retVal;
}
此辅助方法接受.cer 文件(我刚刚创建的)的完整文件路径,并返回一个PublicKey对象。该方法有 4 行
- 首先,创建一个CertificateFactory对象。证书的类型由string指定。这可能是所有中最难的。类型实际上是“X.509”。
- 下一行获取实际.cer 文件的FileInputStream,并使用CertificateFile对象创建Certificate对象。
- 第三行从Certificate对象中提取PublicKey对象。
- 最后一行将PublicKey对象返回给调用者。
一旦有了PublicKey对象,我们就可以获取加密的string并使用 JJWT 库尝试对其进行解码。以下是操作方法
PublicKey publicKey = loadPublicKey("C:\\DevJunk\\jwt\\keystore\\hanbotest.cer");
Jws<Claims> x = Jwts.parser()
   .setSigningKey(publicKey)
   .parseClaimsJws(compactJws);
这里只有两行。第一行使用辅助方法从.cer 文件获取公钥。第二行使用JJWT库方法解密 JWT 令牌。返回的对象是Jws<Claims>类型。compactJws是保存加密 JWT 令牌的string。
令牌被解密后,将有三个不同的部分。
- JWT 令牌的头
- JWT 令牌的正文——用户凭据存储在这里
- JWT 令牌签名
如果令牌可以正确解密,那么我们关心的是令牌的正文部分。以下是我输出并检查其是否正确解密的方法
String id = x.getBody().getId();
System.out.println("id: " + id);
System.out.println("audience: " + x.getBody().getAudience());
System.out.println("audience: " + x.getBody().getSubject());
id、受众和主题的输出与创建 JWT 令牌时使用的输入值相同。
关注点
正如我所承诺的,我保持了这篇文章的简短易懂、简单有趣。我在本文中所做的所有事情都是
- 创建一个仅包含一个 RSA 密钥的 JKS 密钥库。
- 将 JKS 密钥库文件中的公钥证书导出为.cer 文件。
- 一个示例程序,该程序创建 JWT 令牌,使用私钥对其进行加密,并使用公钥(导出的证书)解密令牌。
这并不算多,但它可以扩展并集成到实际的 Web 应用程序中,其中将有一个服务专门用于身份验证和创建 JWT 令牌,而其他服务可以消费和验证 JWT 令牌。它只是让 Web 应用程序的开发变得更加有趣。
历史
- 2018年7月21日 - 初稿



