原理
https 的原理是通过在传输层(tcp)和应用层(http)之间添加一层协议来达到加密的目的。
一个https请求分为两个阶段:
-
第一阶段: 在客户端生成key,该key用来数据传输的对称加密使用。这时候服务端是没有这个key的,所以客户端需要通过非对称加密的方式将key发送给服务端。
-
第二阶段: 在服务端收到key之后,服务端和客户端就都有key了,这时候服务端和客户端就可以进行加密通信了。
证书就是非对称加密的公钥
非对称加密: 发送者和接收者使用两个不同的密钥。非对称加密,也称为公钥加密 ,使用公钥加私钥:用公钥加密的数据只能用私钥解密。
而证书就是公钥,整个加密流程如下:
通过上面的流程服务端和客户端都拥有了key,这时候就可以通过key加密传输的数据。
对称加密传输数据
为什么不直接使用非对称加密传输数据,这样少了一步同步key的操作?因为非对称加密是单向加密传输,接收端必须持有私钥。当然可以把客户端的公钥发送给服务端,然后服务端使用客户端的公钥进行解密内容。但是还有一个考量因素是非对称加密的效率比较低下,不适合频繁使用的场景。
生成方式
生成方式主要有两种,分别是openssl和jdk的keytool,下面是分别使用两种方式生成域名water.com的例子。
1 使用openssl
1.1 生成私钥
首先需要生成私钥。
openssl genpkey -algorithm RSA -out water
- -algorithm:生成密钥库的算法,这里使用RSA。
- -out:指定密钥库的输出路径。
1.2 生成证书签名请求
在生成证书之前,需要生成证书签名请求,该请求在生成证书的时候需要使用。
openssl req -new -key water -out cert.csr
- -new:创建一个新的证书签名请求。
- -key water:使用的私钥。
- -out:输出请求的路径。
命令执行的时候需在Common Name这个选项填写域名water.com
1.3 生成证书
openssl x509 -req -days 365 -in cert.csr -signkey water -out water.pub
- x509:使用证书处理命令。
- -req:表示输入的是证书签名请求 (CSR)。
- -days 365:指定生成的证书的有效期,这里设置为365天。
- -in cert.csr:指定输入的证书签名请求文件路径和文件名,这里是cert.csr。
- -signkey water:指定用于对证书签名的私钥文件路径和文件名,这里是 water。通过该私钥,OpenSSL将对CSR进行签名,生成一个数字证书。
- -out water.pub:指定生成的数字证书文件的输出路径和文件名,这里是 water.pub。
1.4 查看证书
openssl x509 -noout -text -in water.pub
- x509: 使用证书处理命令。
- -noout: 不打印证书的文本表示(不显示证书的原始编码)。
- -text: 显示证书的文本表示(显示证书的人类可读的详细信息)。
- -in water.pub: 指定输入的数字证书文件路径和文件名,这里是water.pub。
2 使用keytool
keytool是jdk自带的一个密钥工具。
2.1 生成密钥库
keytool -genkey -alias water -keyalg RSA -keystore keystore
- -genkey: 生成密钥对
- -alias water: 指定生成的密钥对的别名。在后续的操作中,可以通过别名来引用该密钥对。
- -keyalg RSA: 指定生成密钥对所使用的算法,这里是RSA算法。
- -keystore keystore: 指定存储密钥对的密钥库文件的路径和文件名,这里是keystore。
在交互中What is your first and last name? 中需要填写域名water.com。
2.2 从密钥库中导出证书
keytool -export -trustcacerts -alias water -file water.cer -keystore keystore
- -export: 导出证书的命令。
- -trustcacerts: 表示将证书链中的所有证书都导出,包括根证书和中间证书。如果用于配置信任链,通常需要加上这个选项。
- -alias water: 指定要导出的证书的别名,这里是water。
- -file water.cer: 指定导出的证书文件的输出路径和文件名,这里是water.cer。这个文件将会包含导出的证书信息。
- -keystore keystore: 指定包含要导出证书的密钥库文件的路径和文件名,keystore。
生成方式的区别
keytool是先生成密钥库(包含公私钥),然后再从密钥库导出证书(公钥),文件内容是JKS格式。
JKS文件是一种经过加密的安全文件,以二进制的Java密钥库(KeyStore)格式存储的一组密钥和证书,需要密码才能打开
openssl先生成私钥,然后生成证书(公钥),文件内容是pem格式。
PEM(Privacy Enhanced Mail)**一般为文本格式,以“-----BEGIN ***-----”开头,以“-----END *-----结尾”,中间的内容是Base64编码。
应用
为water.com配置https
配置域名到host文件
vim /etc/hosts
127.0.0.1 water.com
1 tomcat配置https
因为tomcat使用的格式是jks,所以使用jdk生成的keystore比较方便。
1.1 生成keystore密钥对
keytool -genkey -alias water -keyalg RSA -keystore keystore
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CNZEFbAA-1691475072357)(./certificate.assets/image-20230803074113890.png)]
密码设置123456。
1.2 配置tomcat
修改tomcat的配置文件$TOMCAT_HOME/conf/server.xml,增加如下配置,只需要用到密钥库即可,并不需要手动导出证书,证书由tomcat根据密钥库生成并发送到浏览器。
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
SSLEnabled="true" maxThreads="150" scheme="https"
secure="true" clientAuth="false" sslProtocol="TLS"
keystoreFile="/home/yqz/tmp/keystore" keystorePass="123456"
/>
secure="true"
: 表示这个连接是安全的,基于HTTPS协议。clientAuth="false"
: 是否需要客户端进行身份验证。这里设置为false,表示不需要客户端提供证书进行身份验证。sslProtocol="TLS"
: 指定使用的SSL/TLS协议版本,这里是TLS,表示使用TLS协议进行加密通信。keystoreFile="/home/yqz/tmp/keystore"
: 指定密钥库文件的路径和文件名,这里是keystore
,用于存储服务器的密钥对和证书。keystorePass="123456"
: 指定密钥库的密码,这里是123456
,密钥库密码用于访问密钥库中的密钥对。
1.3 验证效果
启动tomcat,访问https://water.com:8443
成功访问,https划线是因为自签名证书对浏览器来说是不安全的。
2 nginx配置https
nginx需要使用pem格式的证书,所以使用openssl来进行生成更加方便。
2.1生成私钥
openssl genpkey -algorithm RSA -out water
2.2 生成证书签名请求
openssl req -new -key water -out cert.csr
在交互中,Common Name选项需要设置域名water.com
2.3 生成证书
openssl x509 -req -days 365 -in cert.csr -signkey water -out water.pub
至此我们得到了三个文件
water为私钥,water.pub为公钥
2.4 配置nginx
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
#监听443端口
listen 443;
#你的域名
server_name water.com;
ssl on;
#ssl证书的pem文件路径
ssl_certificate /home/yqz/tmp/openssl/water.pub;
#ssl证书的key文件路径
ssl_certificate_key /home/yqz/tmp/openssl/water;
location / {
proxy_pass http://公网地址:项目端口号;
}
}
}
2.5 验证
3 springboot配置https
3.1 生成keystore密钥对
keytool -genkey -alias water -keyalg RSA -keystore keystore
密码设置123456。
3.2 Gateway Maven依赖
<!-- 服务网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
3.3 application.yml
复制生成的密钥库到项目resource目录下
server:
port: 8083
ssl:
enabled: true
key-store: classpath: keystore
key-store-type: JKS
key-store-password: 123456
3.4 测试Controller
package com.yqz.testgateway;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
return "hello";
}
}
3.5 验证
4 jdk信任证书
在chrome浏览器中,对于安全的证书能够手动设置信任该证书,但是在程序中如何调用不安全的https接口呢?这时候就需要在jdk手动导入第三方证书,使得jdk信任该证书。
4.1 测试程序中调用https接口
引入openfeign
<!--feign组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
定义feign接口
package com.yqz.testgateway;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "test", url = "https://water.com")
public interface TestFeign {
@GetMapping("/test")
String test();
}
测试
package com.yqz.testgateway;
import jakarta.annotation.Resource;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Resource
private TestFeign testFeign;
@GetMapping("/test")
public String test() {
return "hello";
}
@GetMapping("/test2")
public String test2() {
// 调用https://water.com:8083/test
return testFeign.test();
}
}
没有导入证书的时候调用不安全的https:water.com:8083/test2接口会抛出SunCertPathBuilderException异常。
feign.RetryableException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target executing GET https://water.com:8083/test
4.2 从keystore中导出证书
keystore是一个密钥库,包含了公钥和私钥,现在要导出公钥作为证书。
keytool -export -trustcacerts -alias water -file water.cer -keystore keystore
通过该命令得到water.cer文件,该文件就是公钥。
4.3 导入证书
将water.cer公钥导入到jdk中
keytool -import -trustcacerts -alias water -file water.cer -keystore "/home/yqz/Applications/jdk-17.0.8/lib/security/cacerts"
密码是cacerts的默认密码"changeit“,不是生成密钥库的密码123456
4.4 验证是否存在证书
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
public class Test {
public static void main(String[] args) throws Exception {
// 信任的证书文件路径
String certificateFilePath = "/home/yqz/tmp/water.cer";
// 加载信任的证书
Certificate certificate;
try (FileInputStream is = new FileInputStream(certificateFilePath)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
certificate = cf.generateCertificate(is);
}
// 加载 cacerts 文件
String cacertsPath = System.getProperty("java.home") + "/lib/security/cacerts";
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
char[] password = "changeit".toCharArray(); // 默认的 cacerts 密码为 "changeit"
try (FileInputStream is = new FileInputStream(cacertsPath)) {
keyStore.load(is, password);
}
// 检查是否信任证书
String alias = null;
Enumeration<String> aliasEnum = keyStore.aliases();
while (aliasEnum.hasMoreElements()) {
String currentAlias = aliasEnum.nextElement();
Certificate currentCertificate = keyStore.getCertificate(currentAlias);
if (currentCertificate instanceof X509Certificate) {
if (currentCertificate.equals(certificate)) {
alias = currentAlias;
break;
}
}
}
// 输出结果
if (alias != null) {
System.out.println("The certificate is trusted. Alias: " + alias);
} else {
System.out.println("The certificate is NOT trusted.");
}
}
}