简介
- acme4j官方文档,通过acme4j客户端与Let’s Encrypt集成,申请免费证书。
- 当前域名解析在阿里云DNS,所以示例代码中添加DNS解析时,使用了阿里云相关SDK。如果域名解析在其他云服务,则自行修改添加DNS的相关代码或手动添加DNS解析。
开发环境
- 开发工具:IntelliJ IDEA
- JDK:OpenJDK 17.0.7
- Maven:3.8.6
代码
Maven依赖
<!-- acme4j客户端 -->
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-client</artifactId>
<version>3.3.1</version>
</dependency>
<!-- 阿里云dns sdk -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alidns20150109</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-console</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-util</artifactId>
<version>0.2.21</version>
</dependency>
Java代码
-
修改
addDomainRecord
方法中的accessKeyId
和accessKeyId
,确保这个账号有DNS解析的相关权限。 -
方法
addDomainRecord
可以不写,如果不写此方法则需要手动添加DNS解析。可以在challenge.trigger();
处打断点,手动添加DNS解析后再继续执行代码。 -
isTest
为true
代表测试环境,不限制证书频率,但是证书不被浏览器信任,仅用于测试。测试通过后记得改为false
再申请正式环境的证书。
package com.hyx.ssl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import com.aliyun.alidns20150109.Client;
import com.aliyun.alidns20150109.models.*;
import com.aliyun.teautil.models.RuntimeOptions;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.util.KeyPairUtils;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.security.KeyPair;
import java.util.Optional;
public class Ssl {
public void applySsl(Boolean isTest, String domains) throws Exception {
System.out.println("==========申请证书开始==========");
//创建ACME服务器session
//同一个域名频繁申请会被限制,使用测试环境acme://letsencrypt.org/staging则不受数量限制,但是测试环境签发的证书不被浏览器信任
Session session = null;
//使用测试环境
if (isTest != null && isTest) {
//测试环境
session = new Session("acme://letsencrypt.org/staging");
} else {
//正式环境
session = new Session("acme://letsencrypt.org");
}
System.out.println("==========创建账户密钥==========");
//读取密钥,此密钥对用于创建账户,只需要创建一次,后续复用
String keyPath = "key.pem";
File keyFile = new File(keyPath);
//文件不存在则创建密钥对
if (!keyFile.exists()) {
KeyPair keyPair = KeyPairUtils.createKeyPair(2048);
KeyPairUtils.writeKeyPair(keyPair, new FileWriter(keyPath));
}
FileReader fileReader = new FileReader(keyFile);
KeyPair keyPair = KeyPairUtils.readKeyPair(fileReader);
System.out.println("==========创建账户==========");
//创建账户
Account account = new AccountBuilder()
.addContact("mailto:example@qq.com")
.agreeToTermsOfService()
.useKeyPair(keyPair)
.create(session);
System.out.println("==========创建订单==========");
//创建订单
Order order = account.newOrder()
.domains(domains)
.create();
System.out.println("==========域名验证==========");
for (Authorization auth : order.getAuthorizations()) {
Optional<Dns01Challenge> challengeOpt = auth.findChallenge(Dns01Challenge.TYPE);
Dns01Challenge challenge = challengeOpt.get();
String domain = auth.getIdentifier().getDomain();
String resourceName = Dns01Challenge.toRRName(auth.getIdentifier());
String digest = challenge.getDigest();
//设置DNS
String[] split = domain.split("\\.");
//获取域名
String domainRoot = ArrayUtil.get(split, split.length - 2) + "." + ArrayUtil.get(split, split.length - 1);
//获取域名前缀
String rrKeyWord = resourceName.replace("." + domainRoot + ".", "");
//给阿里云的域名添加DNS解析记录,如果不用addDomainRecord方法,可以在此打断点,然后手动添加记录后再放行
System.out.println("==========添加DNS解析==========");
System.out.println("域名:" + domainRoot + ",前缀:" + rrKeyWord);
Boolean addDomainRecord = this.addDomainRecord(domainRoot, rrKeyWord, "TXT", digest);
if (!addDomainRecord) {
System.out.println("添加DNS解析异常");
return;
}
//触发DNS验证,确保在此之前成功添加DNS解析记录
System.out.println("==========触发DNS验证==========");
challenge.trigger();
//轮询获取验证状态
System.out.println("==========获取验证状态==========");
int i = 0;
while (true) {
Thread.sleep(3000L);
System.out.println("DNS状态:" + auth.getStatus());
if (Status.VALID == auth.getStatus()) {
//验证成功,跳出循环
break;
}
auth.fetch();
if (i >= 99) {
System.out.println("DNS验证超时");
return;
}
i++;
}
}
//生成秘钥,此秘钥用于生成域名证书
KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);
//执行完成订单
order.execute(domainKeyPair);
//判断证书状态
System.out.println("==========判断证书状态==========");
int i = 0;
while (true) {
Thread.sleep(3000L);
System.out.println("证书状态:" + order.getStatus());
if (Status.VALID == order.getStatus()) {
//验证成功,跳出循环
break;
}
order.fetch();
if (i >= 99) {
System.out.println("==========证书状态验证超时==========");
return;
}
i++;
}
//获取证书
Certificate cert = order.getCertificate();
//保存证书公钥
try (FileWriter fileWriter = new FileWriter("cert.crt")) {
cert.writeCertificate(fileWriter);
}
//保存证书私钥
try (JcaPEMWriter pr = new JcaPEMWriter(new FileWriter("private-key.pem"))) {
pr.writeObject(domainKeyPair.getPrivate());
}
System.out.println("==========申请证书结束==========");
}
/**
* 阿里云的域名添加解析记录
*
* @param domain 域名
* @param rrKeyWord 记录域名前缀
* @param typeKeyWord 记录类型
* @param value 记录值
*/
public Boolean addDomainRecord(String domain, String rrKeyWord,
String typeKeyWord, String value) throws Exception {
String accessKeyId = "你的阿里云accessKeyId";
String accessKeySecret = "你的阿里云accessKeySecret";
//初始化配置
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
config.endpoint = "alidns.cn-beijing.aliyuncs.com";
Client client = new Client(config);
//构建查询条件
DescribeDomainRecordsRequest request = new com.aliyun.alidns20150109.models.DescribeDomainRecordsRequest()
.setPageNumber(1L)
.setPageSize(10L)
.setDomainName(domain)
//精确查询
.setSearchMode("EXACT")
.setKeyWord(rrKeyWord);
DescribeDomainRecordsResponse describeDomainRecordsResponse = client.describeDomainRecords(request);
DescribeDomainRecordsResponseBody body = describeDomainRecordsResponse.getBody();
//先查询解析是否已存在
DescribeDomainRecordsResponseBody.DescribeDomainRecordsResponseBodyDomainRecords domainRecords = body.getDomainRecords();
if (CollUtil.isEmpty(domainRecords.getRecord())) {
//新增解析
AddDomainRecordRequest addDomainRecordRequest = new AddDomainRecordRequest()
.setDomainName(domain)
.setRR(rrKeyWord)
.setType(typeKeyWord)
.setValue(value);
RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
client.addDomainRecordWithOptions(addDomainRecordRequest, runtime);
} else {
String recordId = domainRecords.getRecord().get(0).getRecordId();
if (!typeKeyWord.equals(domainRecords.getRecord().get(0).getType()) ||
!value.equals(domainRecords.getRecord().get(0).getValue())) {
//修改解析
UpdateDomainRecordRequest updateDomainRecordRequest = new UpdateDomainRecordRequest()
.setRecordId(recordId)
.setRR(rrKeyWord)
.setType(typeKeyWord)
.setValue(value);
RuntimeOptions runtime = new RuntimeOptions();
client.updateDomainRecordWithOptions(updateDomainRecordRequest, runtime);
}
}
return true;
}
public static void main(String[] args) throws Exception {
//申请测试证书
new Ssl().applySsl(true, "*.xinpapa.com");
}
}
- 代码执行后会生成三个文件
key.pem
:生成订单的秘钥,可复用cert.crt
:证书的公钥文件private-key.pem
:证书的私钥文件