在物联网IOT平台设备接入到云端设备安全和数据安全置关重要,目前比较大型的IOT云平台,比如亚马逊,微软,阿里,腾讯都有自己的设备接入方案,虽然流程不尽相同,但都是使用以下技术来保证设备接入和数据传输的安全性。
- 使用证书来进行设备和云端的双向认证
- 使用HTTPS来进行数据加密传输
这些都离不开证书相关的技术,我最近也在做一个有关这方面的项目,因此把自己实现和学习到的一些东西记录下来,希望能加深自己的理解。
什么是CA
CA证书就是电子商务认证授权机构,也称为电子商务认证中心,是负责发放和管理数字证书的权威机构,并作为电子商务交易中受信任的第三方,承担公钥体系中公钥的合法性检验的责任。
证书的内容包括:电子签证机关的信息、公钥用户信息、公钥、权威机构的签字和有效期等等。目前,证书的格式和验证方法普遍遵循X.509 国际标准。证书能用来认证发送者的身份,证书也能保证信息不会被篡改。
CA证书的签发和认证流程
证书的生成和验证过程简单的概括就是用CA的私钥给用户的证书签名,然后在证书的签名字段中填充这个签名值,浏览器在验证这个证书的时候就是使用CA的公钥进行验签。完整的流程如下:
第一步,服务方S向第三方机构CA提交公钥、组织信息、个人信息(域名)等信息并申请认证;注意不含私钥,这个文件是CSR(Certificate Servcie Requst)。私钥保留在服务器端。
第二步,CA通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等;
第三步,如信息审核通过,CA会向申请者签发认证文件-证书,这个证书可以保留在服务器端或者客户端,可以公开给任何客户端使用。证书包含以下信息:
- 申请者公钥、申请者的组织信息和个人信息、签发机构CA的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;
- 签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用CA的私钥对信息摘要进行加密,密文即签名;信息摘要可以防止信息被篡改。CA的私钥加密能验证证书的合法性和颁发机构的身份。
第三步,客户端 C 向服务器 S 发出请求时,S 返回证书文件;如果不是浏览器的客户端,比如JAVA客户端,也可以把证书放在客户端。
第四步,客户端 C读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应CA的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法;
第五步,客户端然后验证证书相关的域名信息、有效时间等信息;
第六步,客户端会内置信任CA的证书信息(包含公钥),如果CA不被信任,则找不到对应 CA的证书,证书也会被判定非法。
在这个过程注意几点:
a.申请证书不需要提供私钥,确保私钥永远只能服务器掌握;
b.证书的合法性仍然依赖于非对称加密算法,证书主要是增加了服务器信息以及签名;
c.内置 CA 对应的证书称为根证书,颁发者和使用者相同,自己为自己签名,即自签名证书,自签名证书可能会存在中间人攻击。
d.证书=公钥(服务方生成密码对中的公钥)+申请者与颁发者信息+签名(用CA机构生成的密码对的私钥进行签名);
即便有人截取服务器A证书,再发给客户端,想冒充服务器A,也无法实现。因为证书和url的域名是绑定的。
什么是单向认证
大多数情况下认证可能都是单向的,只需要客户端确认服务端是可靠的,而服务端不管客户端是否可靠。平常我们访问一个https网页时,这就是一个典型的单向认证场景,浏览器会验证web服务器的证书,如果不是一个有效的CA证书则无法访问,如果是一个不可信任的CA证书,浏览器也会给出提示,但是服务器不会验证每个浏览器.
什么是双向认证
双向认证相对于单向认证,即客户端需要确认服务端是否可信,服务端也需要确认客户端是否可信。双方都要验证对方的证书。在我之前描述的IOT设备接入到云平台,从安全考虑,肯定需要双向认证。即云端要验证IOT设备的证书,IOT设备也要验证云端证书。详细的双向认证,后面会根据实例详细讲解。
证书生成方法
证书生成工具一般有两个OpenSSL和KeyTool,这两个工具的最大区别是keytool没办法签发证书,而openssl能够进行签发和证书链的管理。因此,keytool 签发的所谓证书只是一种 自签名证书,它是JDK提供给我们弄些 JDK 能认识的证书的。在Java编程中,可以使用BouncyCastle来生成证书。下面会有详细演示。
双向认证实例(自签证书)
单向认证是客户端根据ca根证书验证服务端提供的服务端证书和私钥;双向认证还要服务端根据ca根证书验证客户端证书和私钥,因此双向认证需要生成两套证书。下面以一个实例来演示自签证书的生成和验证过程,使用openssl工具和BouncyCastle,Nginx来演示双向认证流程。
openssl来生成证书
第一步,产生根证书
- 生成根证书私钥
openssl genrsa -out ca.key 4096
- 生成根证书
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
填写相应的信息,注意common name,可以取一个名字,比如ca
第二步,生成服务端证书
- 生成服务端私钥
openssl genrsa -out server.key 4096
- 生成服务端证书请求CSR
openssl req -new -key server.key -out server.csr
此处同样注意Common Name,如果是域名绑定,请填写域名,如果是IP填写服务器IP
- 根据根证书私钥和服务端证书请求生成服务端证书
openssl x509 -req -sha256 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out server.crt
第三步,生成客户端证书
客户端证书生成步骤和服务端基本一样,需要注意的就是在生成签发请求的时候填写的信息中,comm name可以是客户端Ip,其他信息最好是和服务端的不一样。
第四步,导出客户端证书到PKCS12
客户端证书比服务端稍微多一步的就是,需要对客户端证书和私钥进行打包处理,这里方便安装以后后续访问时候携带,一般都是使用pkcs12进行打包,并在浏览器里导入。
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
证书文件格式大概有以下几种:
- .DER .CER,文件是二进制格式,只保存证书,不保存私钥。
- .PEM,一般是文本格式,可保存证书,可保存私钥。
- .CRT,可以是二进制格式,可以是文本格式,与 .DER 格式相同,不保存私钥。
- .PFX .P12,二进制格式,同时包含证书和私钥,一般有密码保护。
- .JKS,二进制格式,同时包含证书和私钥,一般有密码保护。
在Java程序中,也有两个用于保存证书的两个文件trustStore和keyStore,这两个文件也有不同的作用,都有密码保护。如果要保存秘钥和证书,应该使用KeyStore,并且该文件要保持私密不外泄,不要传播该文件;如果要保存你信任的来自他人的公钥和证书,应该使用TrustStore,而不是KeyStore;关于它们的作用,也很好理解,当客户端向服务器端发送一个https请求,客户端需要验证服务器,因此服务器需要向客户端提供认证以便客户端确认这个服务器是否可信。服务器向客户端提供的认证信息就是自身的证书和公钥,而这些信息,包括对应的私钥,服务器就是通过KeyStore来保存的。当服务器提供的证书和公钥到了客户端,客户端就要生成一个TrustStore文件保存这些来自服务器证书和公钥。
而在浏览器中,这些信息就是通过在浏览器中导入证书来实现对服务器的自签证书的验证,就是类似于TrustStore。导入pk12文件来实现客户端向服务器发送证书这一过程,类似于服务器的KeyStore
第五步,在Nginx里配置双向认证
为了演示证书的验证过程,可以在web服务上配置证书,每种web server有不同的配置方式,以Nginx为例,需要做如下配置:
- 配置服务端认证
server {
server_name www.example.com;
ssl on;
ssl_certificate /usr/local/nginx/ssl/server.crt;
ssl_certificate_key /usr/local/nginx/ssl/server.key;
...
}
默认的 listen 80为 listen 443 ssl;
server_name指向之前生成服务端证书时指向的域名www.example.com;
使用 ssl on开启ssl安全认证功能;
ssl_certificate指定服务端证书的地址,如/usr/local/nginx/ssl/server.crt;
ssl_certificate_key指定服务端私钥地址,如/usr/local/nginx/ssl/server.key;
-配置客户端认证
server {
ssl_client_certificate /usr/local/nginx/ssl/ca.crt;
ssl_verify_client on;
}
浏览器请求时会带上客户端的证书,服务器会使用ca.crt证书来验证客户端的身份,作用类似于浏览器中安装了根证书,来验证服务器证书。同样服务器中配置了根证书来验证浏览器。这一过程总结下来如下:
发送方配置公钥和私钥并发送证书给接收方。接收方配置根证书来验证发送方的证书有效性。当发送方是服务器时,比如上例中的Nginx,因此配置了服务器证书和私钥,而且配置了验证客户端的ca证书。当发送方时浏览器时,需要导入PK12文件,里面包括了私钥和证书,请求时带上客户端证书,同时需要配置验证服务器的根证书,如果该根证书不是被CA机构颁发,就必须通过导入根证书的方式导入到浏览器。当发送方时java客户端时,需要配置keystore,里面包括了证书和私钥,请求时会带上证书,同时配置了truststore来验证服务器的证书的有效性。这就是双向认证。
Java中证书的生成
在Java中可以使用BouncyCastle来实现这套证书的颁发流程。测试的库是 BouncyCastle 1.64。
引入库文件
org.bouncycastle
bcprov-jdk15on
1.64
证书工具类
工具类里提供了以下功能:
- 如何使用BouncyCastle生成X509证书
- 如何生成公钥和私钥
- 如果从PEM文件中读取私钥
- 如果从证书文件中获取证书信息
- 如何生成CSR
- 如何从DER文件中读取私钥
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v1CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import javax.xml.bind.DatatypeConverter;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Slf4j
public class SecurityUtil {
private static SecureRandom random = new SecureRandom();
public SecurityUtil(){
}
public static X509Certificate getX509Certificate(File certificatePem) throws IOException, CertificateException {
FileInputStream fin = new FileInputStream(certificatePem);
CertificateFactory f = CertificateFactory.getInstance("X.509");
X509Certificate certificate = (X509Certificate)f.generateCertificate(fin);
fin.close();
return certificate;
}
/**
* Create a X509 V1 {@link Certificate}
*
* @param pair {@link KeyPair}
* @param numberOfDays Number of days the certificate will be valid
* @param DN The DN of the subject
* @return
* @throws CertificateException
*/
public Certificate createX509V1Certificate(KeyPair pair, int numberOfDays, String DN) throws CertificateException {
try {
AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA");
AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
AsymmetricKeyParameter privateKeyAsymKeyParam = PrivateKeyFactory.createKey(pair.getPrivate().getEncoded());
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(pair.getPublic().getEncoded());
ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(privateKeyAsymKeyParam);
Date startDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
Date endDate = new Date(System.currentTimeMillis() + numberOfDays * 24 * 60 * 60 * 1000);
X500Name name = new X500Name(DN);
BigInteger serialNum = createSerialNumber();
X509v1CertificateBuilder v1CertGen = new X509v1CertificateBuilder(name, serialNum, startDate, endDate, name,
subPubKeyInfo);
X509CertificateHolder certificateHolder = v1CertGen.build(sigGen);
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certificateHolder);
} catch (CertificateException e1) {
throw e1;
} catch (Exception e) {
throw new CertificateException(e);
}
}
/**
* Create a certificate signing request
*
* @throws CertificateException
*/
public byte[] createCSR(String dn, KeyPair keyPair) throws CertificateException {
X500Name name = new X500Name(dn);
try {
AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA512WITHRSA");
AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
AsymmetricKeyParameter privateKeyAsymKeyParam = PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded());
ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(privateKeyAsymKeyParam);
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
PKCS10CertificationRequestBuilder builder = new PKCS10CertificationRequestBuilder(name, subPubKeyInfo);
PKCS10CertificationRequest csr = builder.build(sigGen);
return csr.getEncoded();
} catch (Exception e) {
throw new CertificateException(e);
}
}
/**
* Get the CSR as a PEM formatted String
*
* @param csrEncoded
* @return
* @throws IOException
*/
public String getPEM(byte[] csrEncoded) throws IOException {
String type = "CERTIFICATE REQUEST";
PemObject pemObject = new PemObject(type, csrEncoded);
StringWriter str = new StringWriter();
PEMWriter pemWriter = new PEMWriter(str);
pemWriter.writeObject(pemObject);
pemWriter.close();
str.close();
return str.toString();
}
/**
* Generate a Key Pair
*
* @param algo (RSA, DSA etc)
* @return
* @throws GeneralSecurityException
*/
public KeyPair generateKeyPair(String algo) throws GeneralSecurityException {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(algo);
return kpg.genKeyPair();
}
/**
* Create a random serial number
*
* @return
* @throws GeneralSecurityException
*/
public BigInteger createSerialNumber() throws GeneralSecurityException {
BigInteger bi = new BigInteger(4, random);
return bi;
}
private static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes)
throws InvalidKeySpecException, NoSuchAlgorithmException {
final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
final KeyFactory factory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) factory.generatePrivate(spec);
}
private static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException {
final CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
}
public static KeyPair getKeyFromClassPath(String filename) throws CertificateException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InputStream stream = loader.getResourceAsStream("cert/" + filename);
if (stream == null) {
throw new CertificateException("Could not read private key from classpath:" + "certificates/" + filename);
}
BufferedReader br = new BufferedReader(new InputStreamReader(stream));
try {
PEMParser pp = new PEMParser(br);
PEMKeyPair pemKeyPair = (PEMKeyPair) pp.readObject();
KeyPair kp = new JcaPEMKeyConverter().getKeyPair(pemKeyPair);
pp.close();
return kp;
} catch (IOException ex) {
throw new CertificateException("Could not read private key from classpath", ex);
}
}
/**
* Create a KeyStore from standard PEM files
*
* @param privateKeyPem the private key PEM file
* @param certificatePem the certificate(s) PEM file
* @param the password to set to protect the private key
*/
public static KeyStore createKeyStore(File privateKeyPem, File certificatePem, final String password)
throws Exception, KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
final X509Certificate[] cert = createCertificates(certificatePem);
final KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null);
// Import private key
final PrivateKey key = createPrivateKey(privateKeyPem);
keystore.setKeyEntry(privateKeyPem.getName(), key, password.toCharArray(), cert);
return keystore;
}
private static X509Certificate[] createCertificates(File certificatePem) throws Exception {
final List result = new ArrayList();
final BufferedReader r = new BufferedReader(new FileReader(certificatePem));
String s = r.readLine();
if (s == null || !s.contains("BEGIN CERTIFICATE")) {
r.close();
throw new IllegalArgumentException("No CERTIFICATE found");
}
StringBuilder b = new StringBuilder();
while (s != null) {
if (s.contains("END CERTIFICATE")) {
String hexString = b.toString();
final byte[] bytes = DatatypeConverter.parseBase64Binary(hexString);
X509Certificate cert = generateCertificateFromDER(bytes);
result.add(cert);
b = new StringBuilder();
} else {
if (!s.startsWith("----")) {
b.append(s);
}
}
s = r.readLine();
}
r.close();
return result.toArray(new X509Certificate[result.size()]);
}
public static PrivateKey createPrivateKey(File privateKeyPemFile) throws Exception {
String privateKeyPem = initPemFile(privateKeyPemFile);
privateKeyPem = privateKeyPem.replace("-----BEGIN RSA PRIVATE KEY-----\n", "");
privateKeyPem = privateKeyPem.replace("-----END RSA PRIVATE KEY-----\n", "");
privateKeyPem = privateKeyPem.replace(" ", "").replace("\n", "");
byte[] encoded = Base64.decodeBase64(privateKeyPem.getBytes(StandardCharsets.UTF_8));
KeyFactory kf = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return (RSAPrivateKey) kf.generatePrivate(keySpec);
}
private static String initPemFile(File privateKeyPem) {
StringBuilder pemKey = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(privateKeyPem))) {
String tempStr;
while ((tempStr = br.readLine()) != null) {
pemKey.append(tempStr).append("\n");
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return pemKey.toString();
}
}
创建证书请求
有了上面的工具类,生成CSR就比较简单,代码如下:
SecurityUtil util = new SecurityUtil();
KeyPair pair = util.generateKeyPair("RSA");
String DN = "CN=helloworld";
byte[] csr = util.createCSR(DN, pair);
String pem = util.getPEM(csr);
生成X509证书
使用上面的工具类,也可以很容易的根据CA证书的密钥来生成X509证书,下面演示密钥文件从文件中读取。
PemReader reader = new PemReader(new StringReader(csrPEM));
PemObject pemObj = reader.readPemObject();
log.info("Parsed PEM type {}", pemObj.getType());
PKCS10CertificationRequest inputCSR = new
PKCS10CertificationRequest(pemObj.getContent());
AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA512WITHRSA");
AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
PrivateKey caPrivate = SecurityUtil.getKeyFromClassPath("cert/your_ca.key.pem").getPrivate();
X509Certificate x509Certificate = SecurityUtil.getX509Certificate(getFileFromResource("cert/your_ca.cert.pem"));
AsymmetricKeyParameter keyParameter = PrivateKeyFactory.createKey(caPrivate.getEncoded());
SubjectPublicKeyInfo keyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
String name = x509Certificate.getSubjectDN().getName();
X509v3CertificateBuilder myCertificateGenerator = new X509v3CertificateBuilder(
new X500Name(name), BigInteger.valueOf(666666666L), new Date(
System.currentTimeMillis()), new Date(
System.currentTimeMillis() + 30 * 365 * 24 * 60 * 60
* 1000), inputCSR.getSubject(), inputCSR.getSubjectPublicKeyInfo());
ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
.build(keyParameter);
X509CertificateHolder holder = myCertificateGenerator.build(sigGen);
Certificate eeX509CertificateStructure = holder.toASN1Structure();
CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");
InputStream is = new ByteArrayInputStream(eeX509CertificateStructure.getEncoded());
X509Certificate theCert = (X509Certificate) cf.generateCertificate(is);
is.close();
Java中双向认证的实现
在Nginx中配置了服务器端证书和客户端证书认证后,在Java中使用RestTemplate中可以验证服务器证书和发送客户端证书。会用到上面介绍的TrustStore和KeyStore两个文件
生成trust store文件
keytool -keystore truststore.jks -keypass 131112 -storepass 131112 -alias DemoCA -import -trustcacerts -file ca.cer
生成key store文件
我们可以使用以下命令把pkcs12文件转为keystore文件,pkcs里面包括了证书和私钥。
keytool -importkeystore -deststorepass 123456 -destkeypass 123456 -destkeystore keystore.jks -srckeystore sh.pk12 -srcstoretype PKCS12 -srcstorepass 123456 -alias shkey
使用RestTemplate发送请求
private RestTemplate getRestTemplateClientAuthentication()
throws IOException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
final String allPassword = "123456";
SSLContext sslContext = SSLContextBuilder
.create()
.loadKeyMaterial(ResourceUtils.getFile("classpath:keystore.jks"),
allPassword.toCharArray(), allPassword.toCharArray())
.loadTrustMaterial(ResourceUtils.getFile("classpath:truststore.jks"), allPassword.toCharArray())
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
HttpClient client = HttpClients.custom()
.setSSLContext(sslContext)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(client);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
写在最后
文章内容有点多,但涵盖了关于证书认证的方方面面,包括证书生成流程,通过命令和代码来创建证书,证书格式转换,如何配置双向认证,以及任何使用Java代码来实现这些功能。
参考文章
1.https://www.cnblogs.com/xdyixia/p/11610102.html
2.https://blog.csdn.net/tuzongxun/article/details/88647172