目的
如果现在当前系统需要开放一个接口 /say-hello
给第三方应用调用。对于认证可以有两种方式:
- 提供username/password认证后返回token,当访问接口时携带token访问
- 接口是通过客户端证书来分配权限的,把客户端的cert导入到服务端,客户端通过配置认证才能访问
本文章将讨论「如何使用客户端证书」访问接口。
概念
自签名证书
自签名证书是由不受信的CA机构颁发的数字证书,也就是自己签发的证书。与受信任的CA签发的传统数字证书不同,自签名证书是由一些公司或软件开发商创建、颁发和签名的。虽然自签名证书使用的是与X.509证书相同的加密密钥对架构,但是却缺少受信任第三方(如Sectigo)的验证。
创建自签名SSL证书
1、生成私钥
要创建SSL证书,需要私钥和证书签名请求(CSR)。您可以使用一些生成工具或向CA申请生成私钥,私钥是使用RSA和ECC等算法生成的加密密钥。生成RSA私钥的代码示例:openssl genrsa -aes256 -out servername.pass.key 4096,随后该命令会提示您输入密码。
2、生成CSR
私钥生成后,您的私钥文件现在将作为 servername.key 保存在您的当前目录中,并将用于生成CSR。自签名证书的CSR的代码示例:openssl req -nodes -new -key servername.key -out servername.csr。然后需要输入几条信息,包括组织、组织单位、国家、地区、城市和通用名称。通用名称即域名或IP地址。
输入此信息后,servername.csr 文件将位于当前目录中,其中包含 servername.key 私钥文件。
3、颁发证书
最后,使用server.key(私钥文件)和server.csr 文件生成新证书(.crt)。以下是生成新证书的命令示例:openssl x509 -req -sha256 -days 365 -in servername.csr -signkey servername.key -out servername.crt。最后,在您的当前目录中找到servername.crt文件即可。
参考链接:https://segmentfault.com/a/1190000040834174
SSL证书格式
- .DER .CER,文件是二进制格式,只保存证书,不保存私钥。
- .PEM,一般是文本格式,可保存证书,可保存私钥。
- .CRT,可以是二进制格式,可以是文本格式,与 .DER 格式相同,不保存私钥。
- .PFX .P12,二进制格式,同时包含证书和私钥,一般有密码保护。
- .JKS,二进制格式,同时包含证书和私钥,一般有密码保护。
参考链接:
https://blog.csdn.net/farrellcn/article/details/119779348
对称加密与非对称加密
- 对称加密(Symmetric Cryptography)
对称加密是最快速、最简单的一种加密方式,加密(encryption)与解密(decryption)用的是同样的密钥(secret key)。 - 非对称加密(Asymmetric Cryptography)
非对称加密为数据的加密与解密提供了一个非常安全的方法,它使用了一对密钥,公钥(public key)和私钥(private key)。私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。
常用的命令
- OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。
- keytool 是个密钥和证书管理工具。它使用户能够管理自己的公钥/私钥对及相关证书,用于(通过数字签名)自我认证(用户向别的用户/服务认证自己)或数据完整性以及认证服务。它还允许用户储存他们的通信对等者的公钥(以证书形式)。
- JKS和PKCS#12:都是比较常用的两种密钥库格式/标准。对于前者,搞Java开发,尤其是接触过HTTPS平台的朋友,并不陌生。JKS文件(通常为.jks或.keystore,扩展名无关)可以通过Java原生工具——KeyTool生成;而后者PKCS#12文件(通常为.p12或.pfx,意味个人信息交换文件),则是通过更为常用的OpenSSL工具产生。
当然,这两者之间是可以通过导入/导出的方式进行转换的!当然,这种转换需要通过KeyTool工具进行!
生成私钥private.key
openssl genrsa -out private.key 2048
生成用于申请请求的证书文件csr,一般会将该文件发送给CA机构进行认证,本例使用自签名证书request.csr
openssl req -new -key private.key -out request.csr
自签名证书root.crt
openssl x509 -req -days 365 -in request.csr -signkey private.key -out root.crt
查看证书信息
openssl x509 -noout -text -in root.crt
openssl生成p12文件cert.p12
openssl pkcs12 -export -out cert.p12 -inkey private.key -in root.crt
p12文件转换为keystore(jks)文件my.keystore
keytool -importkeystore -srckeystore cert.p12 -destkeystore my.keystore -deststoretype pkcs12
keytool生成p12文件ssl-key.p12
keytool -genkeypair -alias serverssl -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ssl-key.p12 -validity 3650
生成keystore(jks)文件
keytool -genkeypair -dname "cn=clientAuth_vmi_th, ou=IS, o=SGM, c=CN" -alias serverssl -keypass Pass1234 -keystore serverssl.jks -validity 3600 -keyalg RSA -keysize 2048 -sigalg SHA256WithRSA
查看密钥库
keytool -list -rfc --keystore clientAuth_vmi_th.jks -storepass Pass1234
密钥库导出别名clientAuthCert证书clientAuthCert.cer
keytool -export -file clientAuthCert.cer -keystore clientAuthCert.jks -storepass Pass1234 -alias clientAuthCert
keytool -export -file clientAuthCert.cer -keystore clientAuthCert.p12 -storepass Pass1234 -alias clientAuthCert
导入证书到密钥库
keytool -import -alias localhost -file localhost.cer -keystore client.jks
参考链接:
https://www.cnblogs.com/qq931399960/p/11889349.html
http://t.zoukankan.com/zwh0910-p-15214672.html
https://www.cnblogs.com/kabi/p/6232966.html
https://www.jianshu.com/p/e83150254ef8
https://zhuanlan.zhihu.com/p/406815419
示例
后端代码SpringBoot
application.yml
server:
# p12密钥匙库:keytool -genkeypair -alias serverssl -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ssl-key.p12 -validity 3650
# ssl:
# key-store: classpath:ssl-key.p12
# key-store-password: Pass1234
# keyStoreType: PKCS12
# keyAlias: serverssl
# client-auth: need
# JKS密钥匙库:keytool -genkeypair -dname "cn=clientAuth_vmi_th, ou=IS, o=SGM, c=CN" -alias clientAuthCert -keypass Pass1234 -keystore clientAuthCert.jks -storepass Pass1234 -validity 3600 -keyalg RSA -keysize 2048 -sigalg SHA256WithRSA
ssl:
key-store: classpath:clientAuth_vmi_th.jks
key-store-password: Pass1234
keyStoreType: JKS
keyAlias: clientAuthCert
key-password: Pass1234
client-auth: need # 需要客户端认证
port: 8443
SslApplication.java
@SpringBootApplication
@RestController
public class SslApplication {
public static void main(String[] args) {
SpringApplication.run(SslApplication.class, args);
}
@GetMapping("say-hello")
public String sayHello() {
return "hello";
}
}
ServerConfig.java
http跳转https
@Configuration
public class ServerConfig {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(getHttpConnector());
return tomcat;
}
private Connector getHttpConnector() {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}
客户端请求
Postman请求
- 生成公钥
public.key
keytool -list -rfc --keystore clientAuth_vmi_th.jks | openssl x509 -inform pem –pubkey
显示的内容拷贝到文件 public.key
- 生成私钥
private.key
keytool -v -importkeystore -srckeystore clientAuth_vmi_th.jks -srcstoretype jks -srcstorepass Pass1234 -destkeystore clientAuth_vmi_th.pfx -deststoretype pkcs12 -deststorepass Pass1234 -destkeypass Pass1234
openssl pkcs12 -in clientAuth_vmi_th.pfx -nocerts -nodes -out private.key
- 添加证书
Java 代码OkHttp请求
SSLTest.java
class SSLTest {
private static final String KEYSTOREPASS = "Pass1234";
private static final String KEYPASS = "Pass1234";
static KeyStore readStore() throws Exception {
try (InputStream keyStoreStream = new FileInputStream("/Users/rick/jkxyx205/ca/clientAuth_vmi_th.jks")) {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(keyStoreStream, KEYSTOREPASS.toCharArray());
return keyStore;
}
}
static SSLSocketFactory getSSLSocketFactory() {
SSLSocketFactory factory = null;
try {
//初始化SSLContext
SSLContext sslContext = SSLContexts.custom()
.loadKeyMaterial(readStore(), KEYPASS.toCharArray())
.build();
factory = sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return factory;
}
public static void main(String[] args) throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(SSLHelper2.getSSLSocketFactory())
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectTimeout(6, TimeUnit.SECONDS)
.hostnameVerifier(new NoopHostnameVerifier())
.build();
Request request = new Request.Builder()
.url("https://test.com:8443/say-hello")
.get()
.build();
Response response = client.newCall(request).execute();
System.out.println(response.body().string()); // hello
}
}
参考链接:
https://www.cnblogs.com/youngdeng/p/12868717.html
认证工具类
CertificateCoder.java
public class CertificateCoder {
/**
* Java密钥库(Java Key Store,JKS)KEY_STORE
*/
public static final String KEY_STORE = "JKS";
public static final String X509 = "X.509";
/**
* 由 KeyStore获得私钥
*
* @param keyStorePath
* @param keyStorePassword
* @param alias
* @param aliasPassword
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(String keyStorePath,
String keyStorePassword, String alias, String aliasPassword)
throws Exception {
KeyStore ks = getKeyStore(keyStorePath, keyStorePassword);
PrivateKey key = (PrivateKey) ks.getKey(alias,
aliasPassword.toCharArray());
return key;
}
/**
* 由 Certificate获得公钥
*
* @param certificatePath
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(String certificatePath)
throws Exception {
Certificate certificate = getCertificate(certificatePath);
PublicKey key = certificate.getPublicKey();
return key;
}
/**
* 获得Certificate
*
* @param certificatePath
* @return
* @throws Exception
*/
private static Certificate getCertificate(String certificatePath)
throws Exception {
CertificateFactory certificateFactory = CertificateFactory
.getInstance(X509);
FileInputStream in = new FileInputStream(certificatePath);
Certificate certificate = certificateFactory.generateCertificate(in);
in.close();
return certificate;
}
/**
* 获得Certificate
*
* @param keyStorePath
* @param keyStorePassword
* @param alias
* @return
* @throws Exception
*/
private static Certificate getCertificate(String keyStorePath,
String keyStorePassword, String alias) throws Exception {
KeyStore ks = getKeyStore(keyStorePath, keyStorePassword);
Certificate certificate = ks.getCertificate(alias);
return certificate;
}
/**
* 获得KeyStore
*
* @param keyStorePath
* @param password
* @return
* @throws Exception
*/
private static KeyStore getKeyStore(String keyStorePath, String password)
throws Exception {
FileInputStream is = new FileInputStream(keyStorePath);
KeyStore ks = KeyStore.getInstance(KEY_STORE);
ks.load(is, password.toCharArray());
is.close();
return ks;
}
/**
* 私钥加密
*
* @param data
* @param keyStorePath
* @param keyStorePassword
* @param alias
* @param aliasPassword
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String keyStorePath,
String keyStorePassword, String alias, String aliasPassword)
throws Exception {
// 取得私钥
PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
alias, aliasPassword);
// 对数据加密
Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
/**
* 私钥解密
*
* @param data
* @param keyStorePath
* @param alias
* @param keyStorePassword
* @param aliasPassword
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] data, String keyStorePath,
String alias, String keyStorePassword, String aliasPassword)
throws Exception {
// 取得私钥
PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
alias, aliasPassword);
// 对数据加密
Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
/**
* 公钥加密
*
* @param data
* @param keyStorePath
* @param alias
* @param keyStorePassword
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String keyStorePath,
String alias, String keyStorePassword)
throws Exception {
// 取得公钥
PublicKey publicKey = getCertificate(keyStorePath, keyStorePassword, alias).getPublicKey();
// 对数据加密
Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* 公钥加密
*
* @param data
* @param certificatePath
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String certificatePath)
throws Exception {
// 取得公钥
PublicKey publicKey = getPublicKey(certificatePath);
// 对数据加密
Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* 公钥解密
*
* @param data
* @param certificatePath
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] data, String certificatePath)
throws Exception {
// 取得公钥
PublicKey publicKey = getPublicKey(certificatePath);
// 对数据加密
Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* 验证Certificate
*
* @param certificatePath
* @return
*/
public static boolean verifyCertificate(String certificatePath) {
return verifyCertificate(new Date(), certificatePath);
}
/**
* 验证Certificate是否过期或无效
*
* @param date
* @param certificatePath
* @return
*/
public static boolean verifyCertificate(Date date, String certificatePath) {
boolean status = true;
try {
// 取得证书
Certificate certificate = getCertificate(certificatePath);
// 验证证书是否过期或无效
status = verifyCertificate(date, certificate);
} catch (Exception e) {
status = false;
}
return status;
}
/**
* 验证证书是否过期或无效
*
* @param date
* @param certificate
* @return
*/
private static boolean verifyCertificate(Date date, Certificate certificate) {
boolean status = true;
try {
X509Certificate x509Certificate = (X509Certificate) certificate;
x509Certificate.checkValidity(date);
} catch (Exception e) {
status = false;
}
return status;
}
/**
* 签名
*
* @param keyStorePath
* @param alias
* @param keyStorePassword
* @param aliasPassword
* @return
* @throws Exception
*/
public static byte[] sign(byte[] sign, String keyStorePath, String alias,
String keyStorePassword, String aliasPassword) throws Exception {
// 获得证书
X509Certificate x509Certificate = (X509Certificate) getCertificate(
keyStorePath, keyStorePassword, alias);
// 取得私钥
PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
alias, aliasPassword);
// 构建签名
Signature signature = Signature.getInstance(x509Certificate
.getSigAlgName());
signature.initSign(privateKey);
signature.update(sign);
return signature.sign();
}
/**
* 验证签名
*
* @param data
* @param sign
* @param certificatePath
* @return
* @throws Exception
*/
public static boolean verify(byte[] data, byte[] sign,
String certificatePath) throws Exception {
// 获得证书
X509Certificate x509Certificate = (X509Certificate) getCertificate(certificatePath);
// 获得公钥
PublicKey publicKey = x509Certificate.getPublicKey();
// 构建签名
Signature signature = Signature.getInstance(x509Certificate
.getSigAlgName());
signature.initVerify(publicKey);
signature.update(data);
return signature.verify(sign);
}
/**
* 验证Certificate
*
* @param keyStorePath
* @param keyStorePassword
* @param alias
* @return
*/
public static boolean verifyCertificate(Date date, String keyStorePath,
String keyStorePassword, String alias) {
boolean status = true;
try {
Certificate certificate = getCertificate(keyStorePath,
keyStorePassword, alias);
status = verifyCertificate(date, certificate);
} catch (Exception e) {
status = false;
}
return status;
}
/**
* 验证Certificate
*
* @param keyStorePath
* @param keyStorePassword
* @param alias
* @return
*/
public static boolean verifyCertificate(String keyStorePath,
String keyStorePassword, String alias) {
return verifyCertificate(new Date(), keyStorePath, keyStorePassword,
alias);
}
}
参考链接:
https://xhope.top/?p=1336
理解
- 一般情况,公钥加密,私钥解密。证书中包含公钥发送给客户端用于加密。
- 密钥库中既有公钥又有私钥
- 证书有很多格式,服务器不一样需要的格式也不一样。大体分为文本和二进制格式,是否保存私钥,是否需要密码。
其他参考链接
https://www.thomasvitale.com/https-spring-boot-ssl-certificate/
https://www.cnblogs.com/xa-xiaochen/p/15671213.html