问题描述
- 如果服务端只服务单个域名,则可能无视Host的值,返回正确的页面。
- 如果服务端服务多个域名,则通常会返回404或403错误。
另外,如果通过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; } });
- 证书HOST校验问题
- 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
方法的定义,可参考AFNetworking
中AFSecurityPolicy
模块的代码,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的目的。
- 证书HOST校验问题
游戏盾解决方案
- 将被访问网站的DNS域名解析到127.0.0.1。
说明 更改DNS域名解析需要确认该域名没有其它线上业务。
- HTTP、HTTPS请求时,使用
域名:代理端口
的形式替换原先的127.0.0.1: 代理端口
接入方式来访问Web服务器。其中的代理端口即getProxyTcpByDomain
接口返回的端口。
同时,与传统解决方案相比,游戏盾解决方案在安全性方面也更完善。由于传统解决方案在代码中暴露域名信息,如果域名配置了源站服务器IP,攻击者很容易找到源站服务器直接进行攻击;而采用游戏盾解决方案,即使攻击者发现源站域名,也无法获取源站服务器IP。
因此,无论是从兼容性、简单性、还是安全性角度,推荐您使用游戏盾解决方案解决HOST不匹配问题。