Acme4J教程
Acme4j是一个Java工具包,使我们能够自动创建免费的TLS / SSL证书。 Acme4J通过与Let's Encrypt证书颁发机构(CA)进行通信来获取TLS / SSL证书。与手动更新证书相比,自动生成证书是一个很大的优势。自动化后,我们可以节省时间,并且可以更频繁地更改证书,从而降低了证书被泄露的风险。
我们可以通过Acme4J GitHub存储库找到Acme4J。
Acme4J Maven依赖关系
本教程中的Java示例是使用Java 8,Acme4J Client v.2.1和Acme4J Utils v.0.3创建的。以下是示例中使用的工具箱版本的Maven依赖关系:
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-client</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-utils</artifactId>
<version>0.3</version>
</dependency>
Acme4J阶段
使用Acme4J从Let's Encrypt获取证书需要以下阶段。
- 为"加密"帐户创建一个私钥。
- 使用上一阶段中生成的私钥创建一个"让我们加密"帐户。
- 创建一个证书订单并将其发送到Acme4J,以获得证书。
这些阶段中的每个阶段都可以在时间上彼此分离地执行。实际上,阶段1和2通常只执行一次。对每个所需的新证书重复阶段3.
阶段1:为我们加密帐户创建私钥
在Acme4J中使用"让我们加密"的第一步是为"让我们加密"帐户创建私钥。这是在我们实际创建帐户之前完成的。首先,创建私钥,然后使用该私钥创建"加密"帐户。
这是一个Java类,能够创建与Acme4J一起使用的私钥:
import org.shredzone.acme4j.util.KeyPairUtils;
import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyPair;
public class AccountKeyPairCreation {
private String certificateFilePath = null;
public AccountKeyPairCreation(String certificateFilePath) {
this.certificateFilePath = certificateFilePath;
}
public void execute() throws IOException {
// Create a private / public key pair to attach to your CA account.
String keyPairFile = "data/jenkov-com-ca-account-key-pair.pem";
KeyPair accountKeyPair = createAndSaveKeyPair(keyPairFile);
}
private KeyPair createAndSaveKeyPair(String keyPairFile) throws IOException {
KeyPair accountKeyPair = KeyPairUtils.createKeyPair(2048);
// Write key to disk, so it can be reused another time.
try (FileWriter fw = new FileWriter(keyPairFile)) {
KeyPairUtils.writeKeyPair(accountKeyPair, fw);
}
return accountKeyPair;
}
}
创建私钥的是execute()方法。私钥存储在文件中,以备后用。
我们应该保留此私钥。每当我们与Let's Encrypt帐户进行互动时,都将需要它,因此请不要丢失它。另外,请勿让任何人访问它。有权访问我们私钥的任何人都可以代表我们与"让我们加密"互动!
阶段2:创建一个"让我们加密"帐户
为Let's Encrypt帐户创建私钥后,需要创建一个Let's Encrypt帐户。
帐户创建过程的结果是,我们从"加密"中获取了一个帐户URL。我们需要存储此URL供以后使用。将来在订购证书时使用帐户URL。
这是一个可以创建"让我们加密"帐户的类:
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AccountBuilder;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.KeyPairUtils;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.security.KeyPair;
public class AccountCreation {
private String accountKeyPairFilePath = null;
private String accountUrlFilePath = null;
private String letsEncryptUrl = null;
public AccountCreation accountKeyPairFilePath(String filePath){
this.accountKeyPairFilePath = filePath;
return this;
}
public AccountCreation accountUrlFilePath(String filePath){
this.accountUrlFilePath = filePath;
return this;
}
public AccountCreation letsEncryptUrl(String url){
this.letsEncryptUrl = url;
return this;
}
public void execute() throws IOException, AcmeException {
KeyPair keyPair = KeyPairUtils.readKeyPair(new FileReader(this.accountKeyPairFilePath));
Session session = new Session(this.letsEncryptUrl);;
Account account = new AccountBuilder()
//.onlyExisting()
.useKeyPair(keyPair)
.agreeToTermsOfService()
.create(session)
;
URL accountLocationUrl = account.getLocation();
try(FileWriter fileWriter = new FileWriter(this.accountUrlFilePath)){
fileWriter.write(accountLocationUrl.toString());
}
}
}
execute()方法启动了帐户创建过程。该班级需要3项投入才能完成工作:
- 包含"让我们加密"私钥的文件的路径。
- 用于存储返回的帐户URL的路径。
- "让我们加密" API端点的URL。
这是使用上述类的示例:
import com.jenkov.acme4j.commands.AccountCreation;
import org.shredzone.acme4j.exception.AcmeException;
import java.io.IOException;
public class AcmePhase2 {
/*
ACME phase 2 is the creation of an account with the CA.
*/
public static void main(String[] args) throws IOException, AcmeException {
AccountCreation accountCreation = new AccountCreation()
.accountKeyPairFilePath ("le-account/account-private-key.pem")
.accountUrlFilePath ("le-account/account-url.txt")
.letsEncryptUrl ("acme://letsencrypt.org/staging")
;
accountCreation.execute();
}
}
阶段3:创建证书
阶段3是Acme4J阶段中最复杂的阶段。第三阶段本身包含多个较小的步骤。步骤在这里列出:
- 创建一个Order对象并对其进行配置。
- 处理返回的授权。
- 下载证书
以下各节将更详细地说明这些步骤。
创建订单对象
阶段3由代码中的以下步骤组成:
Order order = account.newOrder()
.domains(domains)
.create();
for (Authorization auth : order.getAuthorizations()) {
if (auth.getStatus() != Status.VALID) {
processAuth(auth);
}
}
createCertificateSigningRequest(order, domains);
downloadCertificate(order);
阶段3的第一部分包括创建一个代表证书订单的"订单"对象。 Order类的完全限定名称为org.shredzone.acme4j.Order。
调用create()导致将请求发送到Let's Encrypt。响应是一组授权,我们必须对其进行处理才能获得颁发的证书。
流程授权
一旦调用了order.create()方法,请求就会发送到Let's Encrypt。让我们的Encrypt响应我们需要处理的一组授权,以获取请求的证书。授权会验证"加密",即我们实际上拥有我们要为其申请证书的域。
授权有两种主要类型:
- HTTP挑战
- DNS挑战
HTTP挑战包括"让我们加密"(Let's Encrypt),它为我们提供了一些需要上传到托管给定域的Web服务器的数据。然后,让我们加密将下载该数据,如果成功,则将其视为我们拥有(或者至少管理)给定域的确认。
DNS挑战的工作原理与此类似,但需要对DNS进行一些配置。然后,让我们加密将检查此配置,并将其用作我们拥有域的验证。在本教程中,我将不讨论DNS挑战选项,而仅讨论HTTP挑战选项。
证书顺序中每个域将有一个授权。我们可以按以下方式获取和处理授权:
for (Authorization auth : order.getAuthorizations()) {
if (auth.getStatus() != Status.VALID) {
processAuth(auth);
}
}
处理授权是在processAuth(auth)方法调用中完成的。该方法的外观如下:
private void processAuth(Authorization auth) throws AcmeException, InterruptedException {
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
String fileName = challenge.getToken());
String fileContent = challenge.getAuthorization();
String domain = challenge.getDomain();
challenge.trigger();
while (auth.getStatus() != Status.VALID) {
Thread.sleep(3000L);
auth.update();
}
}
在" Authorization"对象(" auth"参数)内,存在一个或者多个挑战,其中一个必须满足。
上面的示例专门寻找一个HTTP授权对象,然后通过调用challenge.trigger()来触发该对象。在触发质询之前,我们必须从challenge.getAuthorization()中读取值,并将其以文件形式上传到Web服务器。该值必须可用的URL是:
http://${domain}/.well-known/acme-challenge/${token}
...其中$$ domain是我们要为其请求域名的域名(由challenge.getDomain()返回),而$ {token}是由challenge.getToken返回的值( )`。
在上面的示例中," $ {domain}"的值存储在变量" domain"中," $ {token}"的值存储在" fileName"变量中,并且在该URL上找到的文件内容应是fileContent变量的内容。
如果代码在调用challenge.trigger()之前无法访问Web服务器上传挑战文件,则可以在调试器中运行代码,在调用challenge.trigger()之前设置一个断点,然后在该处暂停代码断点,从质询对象中读取令牌(文件名)和授权(文件内容),将其上载到Web服务器,然后在调试器中继续执行代码。
触发挑战后,我们需要等待,直到让我们加密验证了挑战。那就是在processAuth()方法的最后一部分中发生的事情。更具体地说,正是此块等待验证被验证为止:
while (auth.getStatus() != Status.VALID) {
Thread.sleep(3000L);
auth.update();
}
创建证书签名请求
第三阶段的第二部分是创建实际的证书签名请求。看起来是这样的:
private void createCertificateSigningRequest(Order order, String ... domains) throws Exception {
KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);
CSRBuilder csrb = new CSRBuilder();
for(String domain : domains){
csrb.addDomain(domain);
}
csrb.setOrganization(this.organization);
csrb.sign(domainKeyPair);
byte[] csr = csrb.getEncoded();
csrb.write(new FileWriter(this.certificateSigningRequestFilePath));
order.execute(csr);
}
首先,生成一个域公钥/私钥对。该密钥对与我们的"加密"帐户使用的密钥对不同。
其次,添加我们想要证书的所有域。
第三,我们将组织(组织)设置为证书签名请求。
第四,我们使用域密钥对对证书签名请求进行签名。
第五,将证书签名请求写到磁盘上只是为了供将来参考。
最后,我们执行订单。这会将证书签名请求发送到"让我们加密"。
下载证书
阶段3的第3部分是在执行证书签名命令之后下载证书。这是代码中的样子:
private void downloadCertificate(Order order) throws InterruptedException, AcmeException, IOException {
while (order.getStatus() != Status.VALID) {
Thread.sleep(3000L);
order.update();
}
Certificate cert = order.getCertificate();
try (FileWriter fw = new FileWriter(this.certificateFilePath)) {
cert.writeCertificate(fw);
}
}
首先,上面的代码一直等到" Order"对象的状态为" Status.VALID"。
其次,代码从"订单"对象获取新发行的证书。
第三,将证书写入文件。
完整代码示例
这是有关如何实现第3阶段的完整Java代码示例:
import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.KeyPairUtils;
import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
public class OrderCertificate {
public static void orderCertificate(Account account, Instant validUntil,
String ... domains)
throws AcmeException, InterruptedException, IOException {
Order order = account.newOrder()
.domains(domains)
.notAfter(validUntil)
.create();
for (Authorization auth : order.getAuthorizations()) {
if (auth.getStatus() != Status.VALID) {
processAuth(auth);
}
}
createCertificateSigningRequest(order, domains);
downloadCertificate(order);
}
private static void downloadCertificate(Order order)
throws InterruptedException, AcmeException, IOException {
while (order.getStatus() != Status.VALID) {
Thread.sleep(3000L);
order.update();
}
Certificate cert = order.getCertificate();
try (FileWriter fw = new FileWriter("jenkov-com-cert-chain.crt")) {
cert.writeCertificate(fw);
}
}
private static void createCertificateSigningRequest(Order order, String ... domains)
throws IOException, AcmeException {
KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);
CSRBuilder csrb = new CSRBuilder();
for(String domain : domains){
csrb.addDomain(domain);
}
csrb.setOrganization("Jenkov Aps");
csrb.sign(domainKeyPair);
byte[] csr = csrb.getEncoded();
csrb.write(new FileWriter("example.csr"));
order.execute(csr);
}
private static void processAuth(Authorization auth) throws AcmeException,
InterruptedException {
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
challenge.trigger();
while (auth.getStatus() != Status.VALID) {
Thread.sleep(3000L);
auth.update();
}
}
}
使用签发的证书
一旦下载了新发行的SSL / TLS证书,就需要使用它。确切的使用方式取决于我们使用的Web服务器。

