你的浏览器不支持canvas

做你害怕做的事情,然后你会发现,不过如此。

Java代码申请域名SSL免费证书

时间: 作者: 黄运鑫

本文章属原创文章,未经作者许可,禁止转载,复制,下载,以及用作商业用途。原作者保留所有解释权。


简介

  • 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方法中的accessKeyIdaccessKeyId,确保这个账号有DNS解析的相关权限。

  • 方法addDomainRecord可以不写,如果不写此方法则需要手动添加DNS解析。可以在challenge.trigger();处打断点,手动添加DNS解析后再继续执行代码。

  • isTesttrue代表测试环境,不限制证书频率,但是证书不被浏览器信任,仅用于测试。测试通过后记得改为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:证书的私钥文件

对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。