问题描述

终端通过HTTP或HTTPS协议接入的情况下,如果将URL中的HOST配置直接从域名替换为服务器IP后,由于不通过域名而是通过IP直接进行请求。这种情况下,服务端获取到的域名信息为服务器IP,因此请求报文内容中的Host信息为IP,而对于这种请求,服务端的响应结果将取决于服务端的具体配置:
  • 如果服务端只服务单个域名,则可能无视Host的值,返回正确的页面。
  • 如果服务端服务多个域名,则通常会返回404403错误。

另外,如果通过HTTPS协议接入,服务端可能无法找到匹配的证书,只能返回默认证书或者不返回。而客户端在进行证书校验时,也会因为域名不匹配的问题(证书是域名,而校验的是服务器IP),导致SSL证书校验失败 。

传统解决方案

HTTP协议接入

针对HTTP协议接入情况的解决方案相对简单。一般来说,第三方库都提供相应接口支持修改HTTP请求Header的HOST信息,只需要开发人员将HTTP Header的HOST改为对应的域名即可。

HTTPS协议接入

  • Android系统
    • 证书HOST校验问题

      终端在SSL握手过程中会校验当前请求URL的HOST是否在服务端证书的可选域名列表中。例如,假设原本需要请求的URL为https://www.aliyundoc.com,使用服务器IP直连后实际请求的URL变成https://192.0.XX.XX

      由于请求的HOST被替换成服务器IP,底层在进行证书的HOST校验时失败,最终导致请求失败。

      一般来说,系统都提供相应接口,允许终端设置证书HOST校验实现。因此,利用该接口,将底层默认实现中取终端传入URL的HOST信息(即服务器IP)替换回对应的域名即可解决证书HOST校验问题。

      Java代码示例
      HostnameVerifier hnv = new HostnameVerifier() {
      @Override
      public boolean verify(String hostname, SSLSession session) {
      //示例
      if("yourhostname".equals(hostname)){ 
      return true; 
      } else { 
      HostnameVerifier hv =
      HttpsURLConnection.getDefaultHostnameVerifier();
      return hv.verify(hostname, session);
      }
      }
      };
      
      HttpsURLConnection.setDefaultHostnameVerifier(hnv);
    • SNI(Server Name Indicator)问题

      由于不通过域名而是通过IP直接进行请求。这种情况下,服务端获取到的域名信息为服务器IP,因此请求报文内容中的Host信息为IP,而服务端配置了多个域名,导致无法正确选择域名。

      一般来说,系统都提供相应接口,允许终端传入自定义SSLSocketFactory,SSLSocketFactory是用来创建SSLSocket的工厂,SSLSocket是Socket协议的拓展,具有SSL握手功能,且系统提供解决SNI问题的实现类SSLCertificateSocketFactory。因此,利用该方法解决SNI问题。

      Java代码示例

      conn.setSSLSocketFactory(new SSLSocketFactory(){
      @Override
      public Socket createSocket(Socket s, String host, int port,boolean autoClose) throws IOException{
      SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory)SSLCertificateSocketFactory.getDefault(0);
      SSLSocket sslSocket = (SSLSocket)sslSocketFactory.createSocket(s, realHost,port,autoClose);
      sslSocket.setEnableProtocols(sslSocket.getSupportedProtocols());
      sslSocketFactory.setHostname(sslSocket, realHost);
      return sslSocket;
      }
      });
  • iOS系统
    • 证书HOST校验问题

      NSURLSession的证书校验代理方法URLSession:didReceiveChallenge:completionHandler中增加前置处理,将待验证的 domain由原本的服务器IP转换为其对应的域名,然后再进行后续处理。

      Objective-C代码示例

      - (void)URLSession:(NSURLSession *)session
      didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
      completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
      {
      NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
      NSURLCredential *credential = nil;
      
      // 证书验证前置处理。
      NSString *domain = challenge.protectionSpace.host; // 获取当前请求的 host(域名或者 IP),假设此时为:192.0.XX.XX。
      NSString *testHostIP = self.tempDNS[self.testHost];
      // 此时服务端返回的证书里的CN字段(即证书颁发的域名)与上述host可能不一致,
      // 因为上述host在发请求前已经被替换为IP,所以校验证书时会发现域名不一致而无法通过,导致请求被取消。
      // 所以,需要在校验证书前进行替换处理。
      if ([domain isEqualToString:testHostIP]) {
      domain = self.testHost; // 替换为对应域名:www.aliyundoc.com。
      }
      
      // 以下逻辑与AFNetworking -> AFURLSessionManager.m里的代码一致。
      if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
      if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:domain]) {
      // 上述evaluateServerTrust:forDomain方法用于验证SSL握手过程中服务端返回的证书是否可信任,
      // 以及请求的URL中的域名与证书里声明的CN字段是否一致。
      credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
      if (credential) {
      disposition = NSURLSessionAuthChallengeUseCredential;
      } else {
      disposition = NSURLSessionAuthChallengePerformDefaultHandling;
      }
      } else {
      disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
      }
      } else {
      disposition = NSURLSessionAuthChallengePerformDefaultHandling;
      }
      
      if (completionHandler) {
      completionHandler(disposition, credential);
      }
      }
      其中,关于evaluateServerTrust:forDomain方法的定义,可参考 AFNetworkingAFSecurityPolicy模块的代码,Objective-C代码示例如下所示。
      - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
      // 创建证书校验策略。
      NSMutableArray *policies = [NSMutableArray array];
      if (domain) {
      // 需要验证请求的域名与证书中声明的CN字段是否一致。
      [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
      } else {
      [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
      }
      
      // 绑定校验策略到服务端返回的证书(serverTrust)。
      SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
      
      // 评估当前serverTrust是否可信任,
      // 根据苹果官方文档https://developer.apple.com/library/ios/technotes/tn2232/_index.html。
      // 当result为kSecTrustResultUnspecified或kSecTrustResultProceed的情况下,serverTrust可以被验证通过。
      SecTrustResultType result;
      SecTrustEvaluate(serverTrust, &result);
      return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
      }
                                      
    • SNI问题

      通过使用基于原生支持设置SNI字段的更底层的库(libcurl),解决SNI问题。

      Objective-C代码示例

      //{HTTPS域名}:443:{IP地址}
      NSString *curlHost = ...;
      _hosts_list = curl_slist_append(_hosts_list, curlHost.UTF8String);
      curl_easy_setopt(_curl, CURLOPT_RESOLVE, _hosts_list);
      其中,curlHost(如{HTTPS域名}:443:{IP地址},_hosts_list)是结构体类型hosts_list,可设置多个IP与Host域名间的映射关系。通过在curl_easy_setopt方法中传入CURLOPT_RESOLVE,将该映射设置到HTTPS请求中,即可达到设置SNI的目的。

游戏盾解决方案

利用游戏盾自身机制,阿里云提供一种更好的解决方案:
  1. 将被访问网站的DNS域名解析到127.0.0.1。
    说明 更改DNS域名解析需要确认该域名没有其它线上业务。
  2. HTTP、HTTPS请求时,使用域名:代理端口的形式替换原先的127.0.0.1: 代理端口接入方式来访问Web服务器。其中的代理端口即getProxyTcpByDomain接口返回的端口。
通过上述操作,就可以完美解决SSL认证、单IP多HTTPS服务器等问题。使用这种方式,无需改动代码,且能更好地保护源站服务器。

同时,与传统解决方案相比,游戏盾解决方案在安全性方面也更完善。由于传统解决方案在代码中暴露域名信息,如果域名配置了源站服务器IP,攻击者很容易找到源站服务器直接进行攻击;而采用游戏盾解决方案,即使攻击者发现源站域名,也无法获取源站服务器IP。

因此,无论是从兼容性、简单性、还是安全性角度,推荐您使用游戏盾解决方案解决HOST不匹配问题。