
服务器 JDK 1.8u_112
Linux version 3.10.0-1062.9.1.el7.x86_64 ([email protected].org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) )
服务器有 NGINX,但是仅针对进来的请求,对 80 和 443 端口做了转发,转发到服务所在的 81 端口
问题现象:
请求客户服务器 HTTPS 接口出现 connection reset
排查第一步:
先抓包,发现客户服务器在我的服务器 Client Hello 完就 reset 了我(第一次握手
在本地 windows 环境写了个小项目,模拟一模一样的请求代码
复现问题
挨个清理 https 请求的代码,发现去掉重写 hostVerifyName 就可以
然后上网查发现是 JDK8 早期版本的一个 bug
https://bugs.openjdk.java.net/browse/JDK-8159569
按照帖子
https://javabreaks.blogspot.com/2015/12/java-ssl-handshake-with-server-name.html
重写了 SSLSocketFactory
在本地解决了这个 connection reset 的问题
接下来打包代码发到服务器
这里要说明一下,这个项目是一个迭代了六年的项目,没有上 springboot,也没有上 maven
所以每次发布都是更新的 class
更新到服务器上后 tcpdump 抓包发现
Client Hello 的 Extension 里还是没有 server_name(SNI)
这里有怀疑自己代码更新出错,没有把代码更新上去
1.下载 class 与本地进行对比,是一样的
2.将服务器上的 tomcat 和 class 拷贝下来在本地环境( linux -》 win )启动,请求客户服务器抓包是有 SNI
这里已经感到很邪门了,觉得是不是 linux 服务器对出口请求有什么过滤
写了个 springboot 小程序,里面的 https 请求代码是和公司项目里的一样的(有加上 SNI 的代码
抓包,有 SNI
写了个纯 JAVA,只有 main 函数那种
抓包有 SNI
那这就可以排除 linux 服务对出口请求的过滤
不过我也不清楚会不会有单独针对某个程序的过滤???可能性不大
截至这里我已经很绝望了,感觉像是鬼打墙。
查了 JDK 官方的记录说是 1.8u_152 解决了这个问题
先在本机验证,挂上 152 的 JDK,去掉添加 SNI 的代码,抓包有 SNI
所以连夜给服务器的项目升级了 JDK
这里根据 Catalina 里面记录可以保证 JDK 是用的 152
但是抓包,莫得!还是莫得???
下面是涉案代码
服务器上的代码
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String contentType, String outputStr) { JSONObject jsOnObject= null; StringBuffer buffer = new StringBuffer(); HttpsURLConnection httpUrlCOnn= null; OutputStream outputStream = null; InputStream inputStream = null; InputStreamReader inputStreamReader = null; BufferedReader bufferedReader = null; try { URL url = new URL(requestUrl); httpUrlCOnn= (HttpsURLConnection) url.openConnection(); if (contentType != null) { httpUrlConn.setRequestProperty("Content-Type", contentType); } TrustManager[] tm = {new TrustManager()}; SSLContext sslCOntext= SSLContext.getInstance("SSL", "SunJSSE"); sslContext.getServerSessionContext().setSessionCacheSize(1000); sslContext.init(null, tm, new SecureRandom()); // SSLSocketFactory ssf = sslContext.getSocketFactory(); httpUrlConn.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); SSLParameters sslParameters = new SSLParameters(); List sniHostNames = new ArrayList(1); sniHostNames.add(new SNIHostName(url.getHost())); sslParameters.setServerNames(sniHostNames); SSLSocketFactoryWrapper ssf = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters); httpUrlConn.setSSLSocketFactory(ssf); httpUrlConn.setDoOutput(true); httpUrlConn.setDoInput(true); httpUrlConn.setUseCaches(false); httpUrlConn.setRequestMethod(requestMethod); httpUrlConn.setConnectTimeout(20000); httpUrlConn.setReadTimeout(20000); if ("GET".equalsIgnoreCase(requestMethod)) { httpUrlConn.connect(); } if (outputStr != null) { outputStream = httpUrlConn.getOutputStream(); outputStream.write(outputStr.getBytes("UTF-8")); } if ( httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_OK || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_CREATED || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_ACCEPTED) { inputStream = httpUrlConn.getInputStream(); } else { inputStream = httpUrlConn.getErrorStream(); } inputStreamReader = new InputStreamReader(inputStream, "UTF-8"); bufferedReader = new BufferedReader(inputStreamReader); String str = null; while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } try { jsOnObject= JSONObject.fromObject(buffer.toString()); } catch (Exception e1) { try { jsOnObject= JSONObject.fromObject("{id:\"" + buffer.toString() + "\"}"); } catch (Exception e2) { log.error("请求异常:" + requestUrl, e2); return null; } } } catch (Exception e) { log.error("请求异常:" + requestUrl, e); if (e.getMessage().contains("401 for URL")) { return JSONObject.fromObject("{id:\"401\"}"); } return null; } finally { closeConnection( httpUrlConn, outputStream, inputStream, inputStreamReader, bufferedReader); } return jsonObject; } 可以看出除了 TrustManager 和 SSLSocketFactoryWrapper 其他的都是 JDK 自带的类
TrustManager
public class TrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkerverTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return null; } } SSLSocketFactoryWrapper
public class SSLSocketFactoryWrapper extends SSLSocketFactory { private final SSLSocketFactory wrappedFactory; private final SSLParameters sslParameters; public SSLSocketFactoryWrapper(SSLSocketFactory factory, SSLParameters sslParameters) { this.wrappedFactory = factory; this.sslParameters = sslParameters; } @Override public Socket createSocket(String host, int port) throws IOException, UnknownHostException { SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port); setParameters(socket); return socket; } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port, localHost, localPort); setParameters(socket); return socket; } @Override public Socket createSocket(InetAddress host, int port) throws IOException { SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port); setParameters(socket); return socket; } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(address, port, localAddress, localPort); setParameters(socket); return socket; } @Override public Socket createSocket() throws IOException { SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(); setParameters(socket); return socket; } @Override public String[] getDefaultCipherSuites() { return wrappedFactory.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return wrappedFactory.getSupportedCipherSuites(); } @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(s, host, port, autoClose); setParameters(socket); return socket; } private void setParameters(SSLSocket socket) { socket.setSSLParameters(sslParameters); } } 我照抄的呀!我照着大佬抄的!!!
下面是 Springboot 和那个单类测试项目,都放到公司服务器上测试了,有 SNI
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); tryConnect(); } public static void tryConnect() { String requestUrl = "https://xhoa.xinhuamed.com.cn:443/seeyon/rest/token/wechat"; String cOntentType= null; String requestMethod = "GET"; String outputStr = null; StringBuffer buffer = new StringBuffer(); HttpsURLConnection httpUrlCOnn= null; OutputStream outputStream = null; InputStream inputStream = null; InputStreamReader inputStreamReader = null; BufferedReader bufferedReader = null; try { URL url = new URL(requestUrl); httpUrlCOnn= (HttpsURLConnection) url.openConnection(); if (contentType != null) { httpUrlConn.setRequestProperty("Content-Type", contentType); } TrustManager[] tm = {new TrustManager()}; SSLContext sslCOntext= SSLContext.getInstance("SSL", "SunJSSE"); sslContext.getServerSessionContext().setSessionCacheSize(1000); sslContext.init(null, tm, new SecureRandom()); // SSLSocketFactory ssf = sslContext.getSocketFactory(); SSLParameters sslParameters = new SSLParameters(); List sniHostNames = new ArrayList(1); sniHostNames.add(new SNIHostName(url.getHost())); sslParameters.setServerNames(sniHostNames); SSLSocketFactoryWrapper ssf = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters); httpUrlConn.setSSLSocketFactory(ssf); httpUrlConn.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); httpUrlConn.setDoOutput(true); httpUrlConn.setDoInput(true); httpUrlConn.setUseCaches(false); httpUrlConn.setRequestMethod(requestMethod); httpUrlConn.setConnectTimeout(20000); httpUrlConn.setReadTimeout(20000); if ("GET".equalsIgnoreCase(requestMethod)) { httpUrlConn.connect(); } if (outputStr != null) { outputStream = httpUrlConn.getOutputStream(); outputStream.write(outputStr.getBytes("UTF-8")); } if ( httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_OK || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_CREATED || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_ACCEPTED) { inputStream = httpUrlConn.getInputStream(); } else { inputStream = httpUrlConn.getErrorStream(); } inputStreamReader = new InputStreamReader(inputStream, "UTF-8"); bufferedReader = new BufferedReader(inputStreamReader); String str = null; while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } try { System.out.println(buffer.toString()); } catch (Exception e1) { System.out.println(e1); } } catch (Exception e) { System.out.println(e); } finally { closeConnection( httpUrlConn, outputStream, inputStream, inputStreamReader, bufferedReader); } } private static void closeConnection(HttpURLConnection httpUrlConn, OutputStream outputStream, InputStream inputStream, InputStreamReader inputStreamReader, BufferedReader bufferedReader) { if (outputStream != null) { try { outputStream.close(); outputStream = null; } catch (IOException e2) { } } if (bufferedReader != null) { try { bufferedReader.close(); bufferedReader = null; } catch (IOException e1) { } } if (inputStreamReader != null) { try { inputStreamReader.close(); inputStreamReader = null; } catch (IOException e1) { } } if (inputStream != null) { try { inputStream.close(); inputStream = null; } catch (IOException e) { } } if ( httpUrlConn != null) { httpUrlConn.disconnect(); httpUrlCOnn= null; } } } 
拜托大家发散一下思路,想想还有什么方式能测试一下,以及还有哪里有可能限制
1 swiftg 2021-04-10 21:58:47 +08:00 域名被墙成关键词了 |
2 dorothyREN 2021-04-10 22:16:13 +08:00 如果过墙的话 connection reset 一般都是被墙阻断了。 |
3 xarthur 2021-04-10 22:20:30 +08:00 via iPhone 你是不是过墙了? SNI 已经被全部阻断了。 |
4 redford42 OP @swiftg @dorothyREN @xarthur 我服务器出口的墙吗? 截图的这个包都是在公司服务器上 tcpdump 的 这时候抓包已经过了公司服务器的防火墙了吗?不太明白 如果有墙阻断的话 springboot 这个应该也会没有 sni |
5 redford42 OP @swiftg @dorothyREN @xarthur 客户服务器那边是有防火墙的,有个云盾 相当于一个云盾是一个服务器转发外来请求,然后会对应多个虚拟机,所以 https 请求必须带上这个 sni 才能定位转发到哪一台 |
6 mytsing520 PRO 限制了开启 SNI 的,如果客户端发过来没有携带 SNI 字段,定会返回 reset,这和墙不墙的没关系。 如果你的业务前面有 WAF 或 nginx,那么需要在 WAF 或 nginx 上开启 SNI 才会携带信息。 |
7 redford42 OP @mytsing520 我是负责发请求的那个,我不明白我代码里加了 sni 为什么抓包发现发往客户的请求没有带上 |
8 mytsing520 PRO @redford42 我在 6 楼回复中已经说明了。 你在 5 楼回复中描述说有个云盾,可以和云盾核实一下有没有打开 SNI 识别。 |
9 redford42 OP |
10 no1xsyzy 2021-04-12 09:48:56 +08:00 你这 “客户” “服务器” 混乱得一批 这么说:自己控制的服务器上的一个 HTTPS Client ( Java 写的)没能发出 SNI |
11 redford42 OP @no1xsyzy 嗯,是这个情况 打跟踪包把 httpsUrlConnection 里的 SSLSocketFactory 打印出来,ssl 对象的 sslparameters 是有 sni 的 |
12 redford42 OP 破案了 启动的 java options 里面有个 enable_sni=false 浑身轻松 |