acme4j是自动证书管理环境(ACME)协议的java客户端,可以自动执行验证和证书的颁发,该客户端是通过连接到ACME服务器,并执行所必要的步骤来管理证书。
1.引入依赖
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-client</artifactId>
<version>3.4.0</version>
</dependency>
2.自动申请证书
import club.symx.api.common.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.KeyPairUtils;
import java.io.*;
import java.security.KeyPair;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Duration;
/**
* @Description
* @Author srx
* @date 2024/10/14 11:07
*/
@Slf4j
public class SslUtil {
/**
* acme账号的key,由该key确认身份,请勿遗失或泄露该key
*/
private static final String ACCOUNT_KEY_PATH = "/home/frp/frp_client/ssl/account.key";
/**
* 证书
*/
private static final String CERT_KEY_PATH = "/home/frp/frp_client/ssl/cert.key";
/**
* 证书
*/
private static final String FULLCHAIN_CER_PATH = "/home/frp/frp_client/ssl/fullchain.cer";
/**
* http验证证书目录
*/
private static final String SSL_VALIDATION_PATH = "/home/domain/.well-known/acme-challenge/";
/**
* 新增ssl证书
* @param domain 域名
* @throws Exception
*/
public static void addSsl(String domain) throws Exception {
// 获取账号秘钥
KeyPair keyPair = getAccountKeyPair();
// 创建会话并连接到 ACME 服务器
Session session = new Session("https://acme-v02.api.letsencrypt.org/directory");
// 登录
Login login = new AccountBuilder()
.addContact("mailto:your email")
.agreeToTermsOfService()
.useKeyPair(keyPair)
.createLogin(session);
Account account = login.getAccount();
// 加载域名秘钥
KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);
KeyPairUtils.writeKeyPair(domainKeyPair, new FileWriter(CERT_KEY_PATH));
// 创建订单
Order order = account.newOrder().domain(domain).create();
log.info("订单创建完成");
// 轮询挑战
for (Authorization auth : order.getAuthorizations()) {
// 授权域
authorize(auth);
}
// 等待订单变为 READY
log.info("等待订单状态改变");
order.waitUntilReady(Duration.ofSeconds(15));
// 订购证书
log.info("开始订购证书");
order.execute(domainKeyPair, csr -> {
csr.setCountry("CN");
csr.setState("Guangxi");
csr.setLocality("Nanning");
csr.setOrganization("山有木兮");
csr.setCommonName(domain);
});
// 等待订单完成
log.info("等待订单完成");
Status status = order.waitForCompletion(Duration.ofSeconds(15));
if (status != Status.VALID) {
log.error("订单失败,原因: {}", order.getError().map(Problem::toString).orElse("unknown"));
throw new BusinessException("申请SSL证书失败");
}
// 获取证书
log.info("开始下载证书");
Certificate certificate = order.getCertificate();
log.info("成功!域 {} 的证书已生成!", domain);
log.info("证书 URL: {}", certificate.getLocation());
// 编写包含证书和链的组合文件。
try (FileWriter fw = new FileWriter(FULLCHAIN_CER_PATH)) {
certificate.writeCertificate(fw);
log.info("证书已成功写入{}", FULLCHAIN_CER_PATH);
}
}
/**
* 获取账号key
* @return
* @throws IOException
*/
private static KeyPair getAccountKeyPair() throws IOException {
log.info("开始读取账号key");
File file = new File(ACCOUNT_KEY_PATH);
if (file.exists()) {
return KeyPairUtils.readKeyPair(new FileReader(file));
}
KeyPair keyPair = KeyPairUtils.createKeyPair(2048);
KeyPairUtils.writeKeyPair(keyPair, new FileWriter(ACCOUNT_KEY_PATH));
return keyPair;
}
/**
* 授权域
* @param auth
* @throws AcmeException
* @throws InterruptedException
*/
private static void authorize(Authorization auth) throws AcmeException, InterruptedException, IOException {
log.info("授权状态:{}", auth.getStatus());
// 授权已经有效, 无需处理质询
if (auth.getStatus() == Status.VALID) {
log.info("授权已经有效, 无需处理质询");
return;
}
// 找到所需的挑战并做好准备
Http01Challenge challenge = auth.findChallenge(Http01Challenge.class).orElseThrow(() -> new BusinessException("域名SSL证书申请失败"));
// 如果质询已经过验证,则无需再次执行
if (challenge.getStatus() == Status.VALID) {
log.info("质询已经过验证,无需再次执行");
return;
}
String token = challenge.getToken();
String authorization = challenge.getAuthorization();
// 创建验证文件
log.info("创建验证文件");
createVerificationFile(token, authorization);
// 停止一秒钟后再提醒acme服务验证
Thread.sleep(1000);
challenge.trigger();
// 轮询要完成的挑战
Status status = challenge.waitForCompletion(Duration.ofSeconds(15));
if (status != Status.VALID) {
log.error("质询失败,原因: {}", challenge.getError().map(Problem::toString).orElse("unknown"));
throw new BusinessException("质询失败...放弃。");
}
log.info("质询已完成!");
}
/**
* 创建证书验证文件
* @param token
* @param authorization
*/
private static void createVerificationFile(String token, String authorization) throws IOException{
FileWriter fileWriter = new FileWriter(SSL_VALIDATION_PATH + token);
fileWriter.write(authorization);
fileWriter.close();
}
}
3.吊销证书
public static boolean revoke() {
try {
// 读取证书文件
InputStream inStream = new FileInputStream(FULLCHAIN_CER_PATH);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
inStream.close();
// 连接acme服务
Session session = new Session("https://acme-v02.api.letsencrypt.org/directory");
KeyPair keyPair = getAccountKeyPair();
Login login = new AccountBuilder()
.addContact("mailto:your email")
.agreeToTermsOfService()
.useKeyPair(keyPair)
.createLogin(session);
// 吊销证书,原因为null
Certificate.revoke(login, cert, null);
} catch (Exception e) {
log.error("吊销证书出错:{}", e.getMessage());
}
return false;
}
4.查询证书过期时间
public static boolean checkExpire() {
try {
// 读取证书文件
InputStream inStream = new FileInputStream(FULLCHAIN_CER_PATH);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
inStream.close();
// 获取证书过期时间
long expire = cert.getNotAfter().getTime();
System.out.println(expire);
} catch (Exception e) {
log.error("查询ssl过期时间出错:{}", e.getMessage());
}
return false;
}