当前位置 : 首页 » 文章分类 :  开发  »  HTTPS

HTTPS

HTTPS


RFC2818

https://www.ietf.org/rfc/rfc2818.txt


HTTPS/SSL/TLS

HTTPS 其实就是将 HTTP 的数据包再通过 SSL/TLS 加密后再传输。

SSL(Secure Sockets Layer) 安全套接层TLS(Transport Layer Security) 传输层安全协议 其实是一套东西。
网景公司在 1994 年提出 HTTPS 协议时,使用的是 SSL 进行加密。后来 IETF(Internet Engineering Task Force) 互联网工程任务组将 SSL 进一步标准化,于 1999 年公布第一版 TLS 协议文件 TLS 1.0. 目前最新版的 TLS 协议是 TLS 1.3, 于 2018 年公布。

HTTPS 通信过程


HTTPS通信过程

1、用户在浏览器发起 HTTPS 请求,默认使用服务端的 443 端口进行连接;HTTPS 需要使用一套 CA 数字证书,证书内会附带一个公钥 Pub,而与之对应的私钥 Private 保留在服务端不公开;服务端收到请求,返回配置好的包含公钥 Pub 的证书给客户端;
2、客户端收到证书,校验合法性,主要包括是否在有效期内、证书的域名与请求的域名是否匹配,上一级证书是否有效(递归判断,直到判断到系统内置或浏览器配置好的根证书),如果不通过,则显示 HTTPS 警告信息,如果通过则继续;
3、客户端生成一个用于对称加密的随机 Key,并用证书内的公钥 Pub 进行加密,发送给服务端;
4、服务端收到随机 Key 的密文,使用与公钥 Pub 配对的私钥 Private 进行解密,得到客户端真正想发送的随机 Key;服务端使用客户端发送过来的随机 Key 对要传输的 HTTP 数据进行对称加密,将密文返回客户端;
5、客户端使用随机 Key 对称解密密文,得到 HTTP 数据明文;
后续 HTTPS 请求使用之前交换好的随机 Key 进行对称加解密。

复述一遍简化流程,方便后续讨论
服务端有非对称加密的公钥 A1,私钥 A2;
1、客户端发起请求,服务端将公钥 A1 返回给客户端;
2、客户端随机生成一个对称加密的密钥 K,用公钥 A1 加密后发送给服务端;
3、服务端收到密文后用自己的私钥 A2 解密,得到对称密钥 K,此时完成了安全的对称密钥交换,解决了对称加密时密钥传输被人窃取的问题;
之后双方通信都使用密钥K进行对称加解密。

通过非对称加密协商一个对称加密的密钥,然后数据传输通过对称加密进行
由于非对称加解密耗时要远大于对称加解密,所以它一般用于密钥交换,双方通过公钥算法协商出一份密钥,然后通过对称加密来通信。

为什么需要CA

假如没有 CA 做公钥认证,会存在中间人攻击的情况:
非对称加密的算法都是公开的,所有人都可以自己生成一对公钥私钥。
1、当服务端向客户端返回公钥 A1 的时候,中间人将其替换成自己的公钥 B1 传送给浏览器。
2、而浏览器此时一无所知,傻乎乎地使用公钥 B1 加密了密钥 K 发送出去,又被中间人截获,中间人利用自己的私钥 B2 解密,得到密钥 K,再使用服务端的公钥 A1 加密传送给服务端,完成了通信链路,而服务端和客户端毫无感知。

出现这一问题的核心原因是客户端无法确认收到的公钥是不是真的是服务端发来的。为了解决这个问题,互联网引入了一个公信机构,这就是CA。

服务端在使用 HTTPS 前,去经过认证的 CA 机构申请颁发一份数字证书,数字证书里包含有证书持有者、证书有效期、公钥等信息,服务端将证书发送给客户端,客户端校验证书身份和要访问的网站身份确实一致后再进行后续的加密操作。

CA通过数字签名防止公钥被篡改

只有 CA 颁发数字证书还是有问题,因为数字证书中的公钥还是可能被拦截后篡改,要解决这个问题需要 CA 对公钥做数字签名。

1、CA 机构拥有自己的一对公钥和私钥
2、CA 机构在颁发证书时对证书明文信息进行哈希
3、将哈希值用私钥进行加签,得到数字签名
明文数据和数字签名组成证书,传递给客户端。

1、客户端得到证书,分解成明文部分 Text 和数字签名 Sig1
2、用 CA 机构的公钥进行解签,得到 Sig2(由于CA机构是一种公信身份,因此在系统或浏览器中会内置 CA 机构的证书和公钥信息)
3、用证书里声明的哈希算法对明文 Text 部分进行哈希得到 H
4、当自己计算得到的哈希值 H 与解签后的 Sig2 相等,表示证书可信,没有被篡改

签名是由 CA 机构的私钥生成的,中间人篡改信息后无法拿到 CA 机构的私钥,保证了证书可信。


SSL/TLS 终结

如今约 90% 的 Web 页面都会使用安全套接字层 (SSL) 协议及其更安全的现代替代品传输层安全 (TLS) 进行加密。从安全角度而言,这是一项积极的进步,因为此举可以防止攻击者窃取或篡改 Web 浏览器和 Web 或应用服务器之间交换的数据。但是,解密所有的加密流量需要大量的计算能力,而服务器需要解密的加密页面越多,就会造成更大的负担。

SSL termination SSL 终结(或 SSL 卸载)是指解密这些加密流量的过程。与其依靠 Web 服务器来完成这项计算密集型工作,不如使用 SSL 终结来减少服务器的负载,让这一过程实现加速,并使 Web 服务器集中处理交付 Web 内容的核心工作任务。

SSL 终结是很多防火墙、网关软硬件提供的功能。


CA证书与自签证书

自签名证书是由不受信的CA机构颁发的数字证书,也就是自己签发的证书。与受信任的CA签发的传统数字证书不同,自签名证书是由一些公司或软件开发商创建、颁发和签名的。虽然自签名证书使用的是与X.509证书相同的加密密钥对架构,但是却缺少受信任第三方(如Sectigo)的验证。在颁发过程中缺乏独立验证会产生额外的风险,这就是为什么对于面向公众的网站和应用程序来说,自签名证书是不安全的。

自签名证书:
免费且不用申请,自己借助工具生成
不安全,虽然避免了明文传输数据,但是由于证书长期有效,容易被破解
不被浏览器或者一些框架信任,如果是浏览器访问自签名地址,会被标识成不被信任的站点

CA证书
ca证书需要申请或者去一些代理机构比如阿里云、腾讯云等购买
安全,正式的CA证书一般都是有有效期的,定期更新,保证了安全性
浏览器信任,各大框架也都支持


公开 HTTPS 接口

1、http://httpbin.org
curl -X POST “https://httpbin.org/post" -H “accept: application/json”

2、apifox 的 https 接口
https://echo.apifox.com/post


Java keytool 生成自签证书

jdk 中自带 keytool 工具可用来生成和管理证书

keytool -genkeypair \
-alias tomcat \
-dname "CN=Andy,OU=kfit,O=kfit,L=HaiDian,ST=BeiJing,C=CN" \
-keypass 123456 \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-storetype PKCS12 \
-keystore keystore.p12 \
-storepass 123456

参数说明:
-alias key的别名
-dname 指定证书拥有者信息(CN(Common Name名字与姓氏,OU(Organization Unit组织单位名称),O(Organization组织名称),L(Locality城市或区域名称),ST(State州或省份名称),C(Country国家名称))
-keypass 指定生成私钥的密码
-keyalg 指定密钥使用的加密算法(如 RSA)
-keysize 密钥大小
-validity 过期时间,单位天
-storetype 密钥库的类型
-keystore 指定存储密钥的密钥库的生成路径、名称
-storepass 指定访问密钥库的密码

执行后当前目录下生成 keystore.p12 文件就是证书


SpringBoot 启用自签证书 HTTPS

server:
  port: 8080
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: 123456
    key-store-type: PKCS12

server.ssl.enabled 默认值为true,也可以不配置
key-store 对应 keytool -keystore 参数的值,即证书的文件名
key-store-password 对应 keytool -storepass 参数的值
key-store-type 对应 keytool -storetype 参数的值


Feign 支持 https

Feign httpClient 对 https 接口的支持分为两种:
1、对于 https://echo.apifox.com/posthttps://httpbin.org/post 这种公共 https 接口,无需任何配置,直接就可以调用。
2、对于使用 keytool 生成证书后配置到 SpringBoot 并以 https 启动服务这种需要私有化证书的 https 接口,默认是无法调用的,报错如下:

feign.RetryableException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target executing POST https://localhost:8080/api/https
    at feign.FeignException.errorExecuting(FeignException.java:249)
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.ssl.Alert.createSSLException(Alert.java:131)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:456)
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)

对于私有化证书的 https 接口,有两种解决方法:
1、找接口提供方获取公钥文件,配置到 SpringBoot 项目中,配置 feignClient 使用指定的公钥。

2、配置 feignClient 跳过 https 的 ssl 证书验证,就类似 curl 命令增加 -k, --insecure 选项一样,可直接跳过。
Feign 跳过 https 验证又分为两种:
(1)使用 @FeignClient 注解自动生成客户端时,会使用 FeignAutoConfiguration 自动配置类,默认使用的是 apache 的 HttpCLient

public class FeignAutoConfiguration {
      protected static class HttpClientFeignConfiguration {
        @Bean
        @ConditionalOnMissingBean(Client.class)
        public Client feignClient(HttpClient httpClient) {
          return new ApacheHttpClient(httpClient);
        }
    }
}

SpringBoot 中增加下面配置项即可:

feign:
   httpclient:
      disableSslValidation: true

(2)如果是自己通过 Feign.builder 构造 FeignClient,可以如下构造 Client.Default() 客户端,即 HttpURLConnection 实现

public class MyFeignDefaultClient {
    public MyFeignClient client(@Autowired Encoder encoder, @Autowired Decoder decoder) {
        return Feign.builder()
                .logLevel(Logger.Level.FULL)
                .logger(new Slf4jLogger())
                .retryer(new Retryer.Default(100, SECONDS.toMillis(1), 3))
                .encoder(encoder)
                .decoder(decoder)
                .client(feignDefaultClient())
                .contract(new Contract.Default())
                .target(MyFeignClient.class, "https://localhost:8080/api/https");
    }

    private Client feignDefaultClient() {
        try {
            SSLContext ctx = SSLContext.getInstance("SSL");
            X509TrustManager tm = new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            };
            ctx.init(null, new TrustManager[]{tm}, null);
            return new Client.Default(ctx.getSocketFactory(), (hostname, session) -> true);
        } catch (Exception e) {
            return null;
        }
    }
}

或者像下面这样构造 HttpClient 客户端

public class MyFeignApacheHttpClient {
    public MyFeignClient client(@Autowired Encoder encoder, @Autowired Decoder decoder) {
        return Feign.builder()
                .logLevel(Logger.Level.FULL)
                .logger(new Slf4jLogger())
                .retryer(new Retryer.Default(100, SECONDS.toMillis(1), 3))
                .encoder(encoder)
                .decoder(decoder)
                .client(apacheFeignClient())
                .contract(new Contract.Default())
                .target(MyFeignClient.class, "https://localhost:8080/api/https");
    }

    private Client apacheFeignClient() {
        return new ApacheHttpClient(apacheHttpClient());
    }

    private CloseableHttpClient apacheHttpClient() {
        int timeout = 10000;
        try {
            SSLContext sslContext = SSLContextBuilder.create()
                    .loadTrustMaterial(new TrustSelfSignedStrategy()).build();
            RequestConfig config = RequestConfig.custom()
                    .setConnectTimeout(timeout)
                    .setConnectionRequestTimeout(timeout)
                    .setSocketTimeout(timeout)
                    .build();
            return HttpClientBuilder
                    .create()
                    .useSystemProperties()
                    .setDefaultRequestConfig(config)
                    .setSSLContext(sslContext)
                    .setSSLHostnameVerifier(new NoopHostnameVerifier())
                    .build();
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
}

Feign忽略https接口调用SSL证书验证
https://blog.csdn.net/qq_36599564/article/details/116176888

Spring-cloud-starter-openfeign: SSL handshake exception with feign-httpclient
https://stackoverflow.com/questions/53645425/spring-cloud-starter-openfeign-ssl-handshake-exception-with-feign-httpclient


WebClient 支持 https

不需要证书的 https 可直接调用

@Test
public void testHttpsDirectly() {
    Object resp = WebClient.create()
            .post()
            .uri("https://httpbin.org/post")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(ImmutableMap.of("name", "xiaoming"))
            .retrieve()
            .bodyToMono(Object.class)
            .block();
    System.out.println(resp);
}

需要证书的 https 接口可配置 all trust 跳过 SSL 证书认证

@Test
public void testHttpsInsecureTrust() throws SSLException {
    SslContext context = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
    HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(context));
    WebClient webClient = WebClient.builder()
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
    Object resp = webClient
            .post()
            .uri("https://localhost:8080/need/certification")
            .bodyValue(ImmutableMap.of("name", "xiaoming"))
            .retrieve()
            .bodyToMono(Object.class)
            .block();
    System.out.println(resp);
}

Java调用Http/Https接口(7)–WebClient调用Http/Https接口
https://www.cnblogs.com/wuyongyin/p/11959240.html


上一篇 Mockito

下一篇 MyBatisPlus

阅读
评论
3.2k
阅读预计13分钟
创建日期 2022-12-07
修改日期 2023-03-06
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论