之前一直使用阿里云的免费SSL证书。由于有效期的调整,原本每年20张免费的就有点不够用了(原来一个域名一年占用1张额度,有效期调整后,一年需要占用4张额度)。所以就走上了自己制作免费SSL证书的道路

搜索资料,看起来比较多的方式是用ACME,也有很多自动化的脚本去更新证书,本着自己动手试试的原则,就多查了一些资料

用的比较多的服务商有Let's EncryptZeroSSL等,有些也只是免费几张。最终选择了Let's Encrypt(原因不必多说),另外还可以申请泛域名

https://letsencrypt.org/getting-started/上比较推荐的方式是 Certbot ACME client 。而我想用代码实现一下

ACME Client Implementations提供了很多client方式和lib方式。我选择了自己熟悉的语言,用https://github.com/shred/acme4j

ACME4J DOCS:https://shredzone.org/maven/acme4j/index.html

Maven仓库坐标

<dependency>
  <groupId>org.shredzone.acme4j</groupId>
  <artifactId>acme4j-client</artifactId>
  <version>3.2.1</version>
</dependency>

注册Let's Encrypt账号

首先,需要有一个Let's Encrypt的账号,才能获取公网可认证的证书。注册账号需要有一个根证书

openssl genrsa -traditional -out root.key 2048

至于为什么加上-traditional,可看之前的说明:macos下openssl 生成pkcs1格式rsa密钥

KeyPair adminKeyPair = KeyPairUtils.readKeyPair(FileUtil.getReader(getFilePath("root.key"), CharsetUtil.UTF_8));

Session session = new Session("acme://letsencrypt.org");

Account account = new AccountBuilder()
                        .addContact("mailto:lsw1991abc@163.com")
                        .agreeToTermsOfService()
                        .useKeyPair(adminKeyPair)
                        .create(session);
System.out.println(JSONUtil.toJsonStr(account));

这里你会得到账户地址信息,xxx 是账户ID

{"location":"https://acme-v02.api.letsencrypt.org/acme/acct/xxx"}

创建订单

Login login = new Login(URLUtil.url(accountLocation), adminKeyPair, session);
Order order = login.newOrder().domains("*.vimo.cloud").create();
System.out.println(JSONUtil.toJsonStr(order));

这里你会得到订单信息,yyy 是订单ID

{"location":"https://acme-v02.api.letsencrypt.org/acme/order/xxx/yyy"}

获取验证信息

是为了验证所有权的。获取到的验证信息,需要配置到域名解析或者用服务器文件的方式。
https://shredzone.org/maven/acme4j/usage/order.html#challenge
https://shredzone.org/maven/acme4j/challenge/index.html

我使用了DNS的方式

Login login = ...;
Order bindOrder = login.bindOrder(URLUtil.url(orderLocation));
System.out.println(JSONUtil.toJsonStr(bindOrder));
Authorization auth = bindOrder.getAuthorizations().get(0);
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.class).get();
String digest = challenge.getDigest();
System.out.println(digest);

这里会的到一个字符串,需要配置一个TXT的域名解析_acme-challenge.${domain}
参考:https://shredzone.org/maven/acme4j/challenge/dns-01.html

验证配置

配置好DNS之后,让Let's Encrypt验证配置

Login login = ...;
Order bindOrder = login.bindOrder(URLUtil.url(orderLocation));
System.out.println(JSONUtil.toJsonStr(bindOrder));
Authorization auth = bindOrder.getAuthorizations().get(0);
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.class).get();
challenge.trigger();

查询验证结果

Login login = ...;
Order bindOrder = login.bindOrder(URLUtil.url(orderLocation));
System.out.println(JSONUtil.toJsonStr(bindOrder));
Authorization auth = bindOrder.getAuthorizations().get(0);
auth.fetch();
System.out.println(auth.getStatus());
System.out.println(bindOrder.getStatus());

如果验证成功,auth的结果是VALIDorder的结果是READY。这样就可以提交域名证书信息了

注册域名信息

和账号的根证书类似,生成一个域名的根证书

openssl genrsa -traditional -out vimo.cloud.key 2048
Login login = ...;
Order bindOrder = login.bindOrder(URLUtil.url(orderLocation));
KeyPair domainKeyPair = KeyPairUtils.readKeyPair(FileUtil.getReader(getFilePath("vimo.cloud.key"), CharsetUtil.UTF_8));
bindOrder.execute(domainKeyPair, csr -> {
  csr.setCountry("CN");
  csr.setState("SD");
  csr.setLocality("QD");
  csr.setOrganization("Vimo");
  csr.setOrganizationalUnit("Vimo");
});
System.out.println(JSONUtil.toJsonStr(bindOrder));
bindOrder.fetch();
System.out.println(bindOrder.getStatus());

order的结果是VALID后,就可以下载证书信息了

下载SSL证书

Login login = ...;
Order bindOrder = login.bindOrder(URLUtil.url(orderLocation));
Writer writer = FileUtil.getWriter(getFilePath("vimo.cloud.pem"), CharsetUtil.UTF_8, true);
bindOrder.getCertificate().writeCertificate(writer);
writer.flush();

完结

到这里,就可以拿着域名的KEY和下载的PEM,去Nginx配置SSL证书了

其他说明

以上代码示例中,部分工具类依赖于Hutool


感谢大家的阅读, 如有疑问可以加我微信