https总结及在java中的应用

  • 概念

    http协议传输的数据都是未加密的,也就是明文的,因此使用http协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了ssl(Secure Sockets Layer)协议用于对http协议传输的数据进行加密,从而就诞生了https。简单来说,https协议是由http协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。详解:https://www.cnblogs.com/wqhwe/p/5407468.html

    https是采用非对称加密算法+对称加密算法来保证数据的安全(c 客户端 | s 服务端)。首先由c发起https请求s,s收到请求后返回证书(公钥),然后c收到公钥后随机生成一个称加密的密钥,并使用s返回的公钥加密生成的密钥,发送至s,s使用自己的私钥解密得到c的密钥,后续都是用c生成的密钥进行通信

    • 非对称加密两种用法:

      • 第一种用法:公钥加密,私钥解密。用于加解密(https中c向s发送随即密钥时使用此方式)
      • 第二种用法:私钥签名,公钥验签。用于签名
        详解:https://www.kuacg.com/22672.html
  • HTTPS中间人攻击

    https的加密传输弥补了http明文传输的安全隐患,保证了在传输过程中信息的安全,但是https本身依旧存在一个隐患,即中间人攻击。过程大致是,当我们向https服务器发送请求时,请求被劫持(DNS劫持,或者信任不安全证书),然后伪装成https服务器,接收c的请求,并返回c公钥,然后建立与c端的请求,得到c的所有请求信息,并构造一个c1伪装为c与真实的s建立通信,得到所有的返回信息,这个过程信息就会被泄露或篡改。因为c无法知道自己访问到的是真正的s,还是伪装成的s,所以https中间人攻击依然使得https存在一定的安全风险。而解决这一风险的方式就是SSL证书+CA机构

  • CA证书与keytool自制证书

    CA证书为CA机构颁发的SSL证书,keytool自制证书为使用java证书管理工具生成的证书。浏览器接受服务返回的证书后查找操作系统中已内置的受信任的证书发布机构CA,与服务器的证书中的CA匹配,查看是否为合法机构颁发,如果使用keytool自制证书则有可能被拦截,如果在keytool中将机构写成受信任的机构,浏览器从操作系统中获取CA机构的公钥,解密证书中的签名,同时使用相同的hash算法计算出服务器的证书的hash值与签名做对比,对比结果即代表证书是否受信任

  • HostnameVerifier

    在使用httpclient进行https请求时,为防止https中间人攻击http默认会使用HostnameVerifer对请求证书中的主机名与配置的主机名或请求的主机名进行匹配检验以判断证书是否为伪装

  • SSL、TLS、SSH协议

    SSH 应用层的通信加密协议,往往用于远程登录的会话;TLS(传输层安全协议)时SSL(安全套接层)标准化的结果

  • 配置

    • Spring Boot 配置
      首先生成证书:

      keytool -genkeypair -alias eairlv -keyalg RSA -keystore C:\eairlv.key
      

      然后配置:

      server:
        port: 8083
        ssl:
          key-store: eairlv.key
          key-store-type: JKS
          key-alias: eairlv
          key-store-password: eairlv.com
      http:
        port: 8084
      
    • 注入Bean:

      /**
       * server.port作为https端口
       * 如果不配置http端口的bean则默认只开启https服务
       */
      @Value("${server.port}")
      private Integer httpsPort;
      
      @Value("${http.port}")
      private Integer httpPort;
      
      @Bean
      public UndertowEmbeddedServletContainerFactory embeddedServletContainerFactory() {
          UndertowEmbeddedServletContainerFactory undertow = new UndertowEmbeddedServletContainerFactory();
          undertow.addBuilderCustomizers((Undertow.Builder builder) ->
              builder.addHttpListener(httpPort, "0.0.0.0")
          );
          log.info("undertow http port: {}, https port: {} ", httpPort, httpsPort);
          return undertow;
      }
      
      //spring boot 2.0 UndertowEmbeddedServletContainerFactory 被剔除
      //spring boot 2.0 正确配置:
      
      /**
       * server.port作为https端口
       */
      @Value("${server.port}")
      private Integer httpsPort;
      
      @Value("${http.port}")
      private Integer httpPort;
      
      @Bean
      public UndertowServletWebServerFactory embeddedServletContainerFactory() {
          UndertowServletWebServerFactory undertow = new UndertowServletWebServerFactory();
          undertow.addBuilderCustomizers((UndertowBuilderCustomizer) builder ->
              builder.addHttpListener(httpPort, "0.0.0.0")
          );
          return undertow;
      }
      
      //增强版:
      @Bean
      public UndertowServletWebServerFactory embeddedServletContainerFactory() {
          UndertowServletWebServerFactory undertow = new UndertowServletWebServerFactory();
          undertow.addBuilderCustomizers((UndertowBuilderCustomizer) builder -> {
                          // IP配置'0.0.0.0'任意方式访问
                  builder.addHttpListener(httpPort, "0.0.0.0");
                  // 开启HTTP2
                  builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
          });
          undertow.addDeploymentInfoCustomizers(deploymentInfo -> {
              // 开启HTTP自动跳转至HTTPS
              deploymentInfo.addSecurityConstraint(new SecurityConstraint()
                      .addWebResourceCollection(new WebResourceCollection().addUrlPattern("/*"))
                      .setTransportGuaranteeType(TransportGuaranteeType.CONFIDENTIAL)
                      .setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.PERMIT))
                      .setConfidentialPortManager(exchange -> httpsPort);
          });
          return undertow;
      }
      
      //undertow配置http跳转https后post接口400。
      //https://stackoverflow.com/questions/37464220/springboot-undertow-redirect-post-from-http-to-https
      
    • RestTemplate配置

      @Bean
      public RestTemplate restTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
      TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
      
      SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
              .loadTrustMaterial(null, acceptingTrustStrategy)
              .build();
      
      SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
      
      CloseableHttpClient httpClient = HttpClients.custom()
              .setSSLContext(sslContext)
              .setSSLSocketFactory(csf)
              .build();
      
      HttpComponentsClientHttpRequestFactory requestFactory =
              new HttpComponentsClientHttpRequestFactory();
      
      requestFactory.setHttpClient(httpClient);
      return new RestTemplate(requestFactory);
      }
              
      //不可行方案:
      SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
      CloseableHttpClient httpClient = HttpClients.custom()
              .setSSLSocketFactory(csf)
              .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
              .build();
      
      //可行方案1:
      SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
      CloseableHttpClient httpClient = HttpClients.custom()
              .setSSLSocketFactory(csf)
              .build();
      //可行方案2:
      CloseableHttpClient httpClient = HttpClients.custom()
              .setSSLContext(sslContext)
              .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
              .build();
      
      
      //  HttpClientBuilder 
      //  933 public CloseableHttpClient build() {...} 
      //  914 if (sslSocketFactoryCopy == null) {...} 
      //  如果设置了SSLConnectionSocketFactory,CloseableHttpClient的HostnameVerifier就会使用SSLConnectionSocketFactory中的SSLHostnameVerifier,即出现不可行情况
      
    • 血案

      • httpclient访问https血案:Certificate for doesn't match any of the subject alternative names: []
      • httpclient默认会通过HostnameVerifier验证访问主机,而我们想要访问https服务就需要对HostnameVerifier配置
      • HostnameVerifier 拥有一个DefaultHostnameVerifier,它对访问的主机名与证书中的主机名匹配校验,如:https://api.eairlv.com:8443/v2/api可访问(IE直接访问证书合法),而访问https://115.28.100.23:8443/v2/api则访问不通过(IE直接访问证书不合法)
    • 信任证书

      • 信任服务器证书,如果不配置证书信任,则httpclient默认会对证书进行校验,keytool自制的证书则无法通过校验

      • https://api.eairlv.com:8443/v2/api,IE信任而httpclient默认不信任,猜测为证书类型为DV,即信任等级较低。DV < OV < EV。DV证书仅对传输信息进行加密,并不能真正证明网站的真实身份,只是提高了伪造身份的复杂度,依然存在https中间人攻击的可能性(中间人拥有可信任机构颁发的证书,且域名一致)

        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
        SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
              .loadTrustMaterial(null, acceptingTrustStrategy)
              .build();
        或:
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, new TrustManager[] { new SkipX509TrustManager() },
              new SecureRandom());
        private static class SkipX509TrustManager implements X509TrustManager {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) {
            }
        
            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) {
            }
        
        }
        
    • 信任主机名(配置的主机名或请求的主机名与证书中主机名的匹配)

      重写HostnameVerifier类中的verify方法,可自行配置主机名的匹配规则,也可使用DefaultHostnameVerifier匹配请求的主机名与证书中的主机名,如果信任所有主机则只需使verify方法始终返回true

      SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
      // 据说这种方式在google play上会报不合法,需要简单的在verify中添加逻辑
      SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, (s, sslSession) -> true);
      
  • 总结

    要想防止中间人攻击证书的信任与主机名的信任需要同时开启,我之前一直也觉得只需要信任证书就行了,为什么还需要信任主机名?而如果中间人伪装的服务器也拥有CA信任证书,主机名则能够帮助客户端区分证书是否为真正受信任的CA证书。同一个域名可能对应多个可信任证书,即依然存在安全隐患,而如果希望安全性更高的话,则可采取客户端预埋证书的方式锁死证书,当客户端证书与服务端证书完全一致才能进行通信,对于证书过期问题则需要要求用户在官网(软件更新本身也会有可能被中间调包)下载证书来解决。在一般的Android程序中,连接https请求时为保证安全性可自定义规则,也可使用默认(默认更严格,自定义规则用于子域名的情况,CA通配符证书较贵),首先检验证书是否被信任与主机名是否被信任,而在浏览器中则使用默认的匹配规则以达到防止https中间人的攻击

你可能感兴趣的:(https总结及在java中的应用)