如何通过 Java 中的代理发送 HTTPS 请求?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1511674/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How do a send an HTTPS request through a proxy in Java?
提问by Jeff Hillman
I am trying to send a request to a server using the HttpsUrlConnection class. The server has certificate issues, so I set up a TrustManager that trusts everything, as well as a hostname verifier that is equally lenient. This manager works just fine when I make my request directly, but it doesn't seem to be used at all when I send the request through a proxy.
我正在尝试使用 HttpsUrlConnection 类向服务器发送请求。服务器有证书问题,所以我设置了一个信任一切的 TrustManager,以及一个同样宽松的主机名验证器。当我直接提出请求时,这个管理器工作得很好,但是当我通过代理发送请求时,它似乎根本没有使用。
I set my proxy settings like this:
我这样设置我的代理设置:
Properties systemProperties = System.getProperties();
systemProperties.setProperty( "http.proxyHost", "proxyserver" );
systemProperties.setProperty( "http.proxyPort", "8080" );
systemProperties.setProperty( "https.proxyHost", "proxyserver" );
systemProperties.setProperty( "https.proxyPort", "8080" );
The TrustManager for the default SSLSocketFactory is set up like this:
默认 SSLSocketFactory 的 TrustManager 设置如下:
SSLContext sslContext = SSLContext.getInstance( "SSL" );
// set up a TrustManager that trusts everything
sslContext.init( null, new TrustManager[]
{
new X509TrustManager()
{
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
public void checkClientTrusted( X509Certificate[] certs, String authType )
{
// everything is trusted
}
public void checkServerTrusted( X509Certificate[] certs, String authType )
{
// everything is trusted
}
}
}, new SecureRandom() );
// this doesn't seem to apply to connections through a proxy
HttpsURLConnection.setDefaultSSLSocketFactory( sslContext.getSocketFactory() );
// setup a hostname verifier that verifies everything
HttpsURLConnection.setDefaultHostnameVerifier( new HostnameVerifier()
{
public boolean verify( String arg0, SSLSession arg1 )
{
return true;
}
} );
If I run the following code, I end up with an SSLHandshakException ("Remote host closed connection during handshake"):
如果我运行以下代码,我最终会得到 SSLHandshakException(“握手期间远程主机关闭连接”):
URL url = new URL( "https://someurl" );
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setDoOutput( true );
connection.setRequestMethod( "POST" );
connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
connection.setRequestProperty( "Content-Length", "0" );
connection.connect();
I assume I am missing some kind of setting having to do with using a proxy when dealing with SSL. If I don't use a proxy, my checkServerTrusted method gets called; this is what I need to happen when I am going through the proxy as well.
我假设我在处理 SSL 时缺少某种与使用代理有关的设置。如果我不使用代理,我的 checkServerTrusted 方法就会被调用;这也是我在通过代理时需要发生的事情。
I don't usually deal with Java and I don't have much experience with HTTP/web stuff. I believe I have provided all the detail necessary to understand what I am trying to do. If this isn't the case, let me know.
我通常不与 Java 打交道,而且我对 HTTP/web 内容也没有太多经验。我相信我已经提供了所有必要的细节来理解我想要做什么。如果不是这种情况,请告诉我。
Update:
更新:
After reading the article suggested by ZZ Coder, I made the following changes to the connection code:
看了ZZ Coder推荐的文章,我对连接代码做了如下修改:
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setSSLSocketFactory( new SSLTunnelSocketFactory( proxyHost, proxyPort ) );
connection.setDoOutput( true );
connection.setRequestMethod( "POST" );
connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
connection.setRequestProperty( "Content-Length", "0" );
connection.connect();
The result (SSLHandshakeException) is the same. When I set the SLLSocketFactory here to the SSLTunnelSocketFactory (the class explained in the article), the stuff I did with the TrustManager and the SSLContext is overridden. Don't I still need that?
结果 (SSLHandshakeException) 是相同的。当我将此处的 SLLSocketFactory 设置为 SSLTunnelSocketFactory(文章中解释的类)时,我对 TrustManager 和 SSLContext 所做的事情将被覆盖。我还不需要那个吗?
Another Update:
另一个更新:
I modified the SSLTunnelSocketFactory class to use the SSLSocketFactory that uses my TrustManager that trusts everything. It doesn't appear that this has made any difference. This is the createSocket method of SSLTunnelSocketFactory:
我修改了 SSLTunnelSocketFactory 类以使用 SSLSocketFactory,它使用我信任一切的 TrustManager。这似乎没有任何区别。这是 SSLTunnelSocketFactory 的 createSocket 方法:
public Socket createSocket( Socket s, String host, int port, boolean autoClose )
throws IOException, UnknownHostException
{
Socket tunnel = new Socket( tunnelHost, tunnelPort );
doTunnelHandshake( tunnel, host, port );
SSLSocket result = (SSLSocket)dfactory.createSocket(
tunnel, host, port, autoClose );
result.addHandshakeCompletedListener(
new HandshakeCompletedListener()
{
public void handshakeCompleted( HandshakeCompletedEvent event )
{
System.out.println( "Handshake finished!" );
System.out.println(
"\t CipherSuite:" + event.getCipherSuite() );
System.out.println(
"\t SessionId " + event.getSession() );
System.out.println(
"\t PeerHost " + event.getSession().getPeerHost() );
}
} );
result.startHandshake();
return result;
}
When my code calls connection.connect, this method is called, and the call to doTunnelHandshake is successful. The next line of code uses my SSLSocketFactory to create an SSLSocket; the toString value of result after this call is:
当我的代码调用connection.connect时,调用了这个方法,调用doTunnelHandshake就成功了。下一行代码使用我的 SSLSocketFactory 创建一个 SSLSocket;此调用后结果的 toString 值为:
"1d49247[SSL_NULL_WITH_NULL_NULL: Socket[addr=/proxyHost,port=proxyPort,localport=24372]]".
“1d49247[SSL_NULL_WITH_NULL_NULL: Socket[addr=/proxyHost,port=proxyPort,localport=24372]]”。
This is meaningless to me, but it might be the reason things break down after this.
这对我来说毫无意义,但这可能是此后事情崩溃的原因。
When result.startHandshake() is called, the same createSocket method is called again from, according to the call stack, HttpsClient.afterConnect, with the same arguments, except Socket s is null, and when it comes around to result.startHandshake() again, the result is the same SSLHandshakeException.
当 result.startHandshake() 被调用时,根据调用堆栈 HttpsClient.afterConnect 再次调用相同的 createSocket 方法,参数相同,除了 Socket s 为 null,当它出现在 result.startHandshake()同样,结果是相同的 SSLHandshakeException。
Am I still missing an important piece to this increasingly complicated puzzle?
在这个日益复杂的谜题中,我是否仍然缺少一个重要的部分?
This is the stack trace:
这是堆栈跟踪:
javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:808) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1112) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1139) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1123) at gsauthentication.SSLTunnelSocketFactory.createSocket(SSLTunnelSocketFactory.java:106) at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:391) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:166) at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:133) at gsauthentication.GSAuthentication.main(GSAuthentication.java:52) Caused by: java.io.EOFException: SSL peer shut down incorrectly at com.sun.net.ssl.internal.ssl.InputRecord.read(InputRecord.java:333) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:789) ... 8 more
采纳答案by ZZ Coder
HTTPS proxy doesn't make sense because you can't terminate your HTTP connection at the proxy for security reasons. With your trust policy, it might work if the proxy server has a HTTPS port. Your error is caused by connecting to HTTP proxy port with HTTPS.
HTTPS 代理没有意义,因为出于安全原因,您无法在代理处终止 HTTP 连接。根据您的信任策略,如果代理服务器具有 HTTPS 端口,它可能会起作用。您的错误是由使用 HTTPS 连接到 HTTP 代理端口引起的。
You can connect through a proxy using SSL tunneling (many people call that proxy) using proxy CONNECT command. However, Java doesn't support newer version of proxy tunneling. In that case, you need to handle the tunneling yourself. You can find sample code here,
您可以使用 proxy CONNECT 命令使用 SSL 隧道(许多人称之为代理)通过代理进行连接。但是,Java 不支持更新版本的代理隧道。在这种情况下,您需要自己处理隧道。你可以在这里找到示例代码,
http://www.javaworld.com/javaworld/javatips/jw-javatip111.html
http://www.javaworld.com/javaworld/javatips/jw-javatip111.html
EDIT: If you want defeat all the security measures in JSSE, you still need your own TrustManager. Something like this,
编辑:如果你想打败 JSSE 中的所有安全措施,你仍然需要你自己的 TrustManager。像这样的东西,
public SSLTunnelSocketFactory(String proxyhost, String proxyport){
tunnelHost = proxyhost;
tunnelPort = Integer.parseInt(proxyport);
dfactory = (SSLSocketFactory)sslContext.getSocketFactory();
}
...
connection.setSSLSocketFactory( new SSLTunnelSocketFactory( proxyHost, proxyPort ) );
connection.setDefaultHostnameVerifier( new HostnameVerifier()
{
public boolean verify( String arg0, SSLSession arg1 )
{
return true;
}
} );
EDIT 2: I just tried my program I wrote a few years ago using SSLTunnelSocketFactory and it doesn't work either. Apparently, Sun introduced a new bug sometime in Java 5. See this bug report,
编辑 2:我刚刚尝试了我几年前使用 SSLTunnelSocketFactory 编写的程序,但它也不起作用。显然,Sun 在 Java 5 中的某个时候引入了一个新错误。请参阅此错误报告,
http://bugs.sun.com/view_bug.do?bug_id=6614957
http://bugs.sun.com/view_bug.do?bug_id=6614957
The good news is that the SSL tunneling bug is fixed so you can just use the default factory. I just tried with a proxy and everything works as expected. See my code,
好消息是 SSL 隧道错误已修复,因此您可以使用默认工厂。我只是尝试使用代理,一切都按预期工作。看我的代码
public class SSLContextTest {
public static void main(String[] args) {
System.setProperty("https.proxyHost", "proxy.xxx.com");
System.setProperty("https.proxyPort", "8888");
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
// set up a TrustManager that trusts everything
sslContext.init(null, new TrustManager[] { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
System.out.println("getAcceptedIssuers =============");
return null;
}
public void checkClientTrusted(X509Certificate[] certs,
String authType) {
System.out.println("checkClientTrusted =============");
}
public void checkServerTrusted(X509Certificate[] certs,
String authType) {
System.out.println("checkServerTrusted =============");
}
} }, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(
sslContext.getSocketFactory());
HttpsURLConnection
.setDefaultHostnameVerifier(new HostnameVerifier() {
public boolean verify(String arg0, SSLSession arg1) {
System.out.println("hostnameVerifier =============");
return true;
}
});
URL url = new URL("https://www.verisign.net");
URLConnection conn = url.openConnection();
BufferedReader reader =
new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
This is what I get when I run the program,
这是我运行程序时得到的
checkServerTrusted =============
hostnameVerifier =============
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
......
As you can see, both SSLContext and hostnameVerifier are getting called. HostnameVerifier is only involved when the hostname doesn't match the cert. I used "www.verisign.net" to trigger this.
如您所见, SSLContext 和 hostnameVerifier 都被调用了。HostnameVerifier 仅在主机名与证书不匹配时才涉及。我使用“www.verisign.net”来触发这个。
回答by ZZ Coder
Try the Apache Commons HttpClient library instead of trying to roll your own: http://hc.apache.org/httpclient-3.x/index.html
尝试使用 Apache Commons HttpClient 库,而不是尝试推出自己的库:http: //hc.apache.org/httpclient-3.x/index.html
From their sample code:
从他们的示例代码:
HttpClient httpclient = new HttpClient();
httpclient.getHostConfiguration().setProxy("myproxyhost", 8080);
/* Optional if authentication is required.
httpclient.getState().setProxyCredentials("my-proxy-realm", " myproxyhost",
new UsernamePasswordCredentials("my-proxy-username", "my-proxy-password"));
*/
PostMethod post = new PostMethod("https://someurl");
NameValuePair[] data = {
new NameValuePair("user", "joe"),
new NameValuePair("password", "bloggs")
};
post.setRequestBody(data);
// execute method and handle any error responses.
// ...
InputStream in = post.getResponseBodyAsStream();
// handle response.
/* Example for a GET reqeust
GetMethod httpget = new GetMethod("https://someurl");
try {
httpclient.executeMethod(httpget);
System.out.println(httpget.getStatusLine());
} finally {
httpget.releaseConnection();
}
*/