使用 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日 - 初稿