如何在Java中通过代理发送HTTPS请求?

31
我将尝试使用HttpsUrlConnection类向服务器发送请求。由于服务器存在证书问题,因此我设置了一个信任所有内容的TrustManager,以及一个同样宽松的主机名验证器。当我直接发出请求时,这个管理器运行得很好,但是当我通过代理发送请求时,它似乎完全没有被使用。
我像这样设置了代理设置:
Properties systemProperties = System.getProperties();
systemProperties.setProperty( "http.proxyHost", "proxyserver" );
systemProperties.setProperty( "http.proxyPort", "8080" );
systemProperties.setProperty( "https.proxyHost", "proxyserver" );
systemProperties.setProperty( "https.proxyPort", "8080" );

默认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;
    }
} );

如果我运行以下代码,最终会得到一个SSLHandshakeException(“握手期间远程主机关闭了连接”):
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();

我猜测我缺少一些与使用代理处理SSL有关的设置。如果我不使用代理,我的checkServerTrusted方法会被调用;当我通过代理时,这也是我需要发生的事情。
我通常不处理Java,也没有太多HTTP/网络方面的经验。我相信我已经提供了所有必要的细节,以便理解我试图做什么。如果不是这种情况,请让我知道。
更新:
在阅读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();

结果(SSLHandshakeException)是相同的。当我在这里将SLLSocketFactory设置为SSLTunnelSocketFactory(文章中解释的类),我对TrustManager和SSLContext所做的操作被覆盖了。我还需要那个吗?
另一个更新:
我修改了SSLTunnelSocketFactory类,使用了使用信任所有内容的TrustManager的SSLSocketFactory。看起来这没有任何区别。这是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;
}

当我的代码调用connection.connect时,会调用此方法,并且调用doTunnelHandshake成功。代码的下一行使用我的SSLSocketFactory创建一个SSLSocket;在此调用后,result的toString值为:“1d49247[SSL_NULL_WITH_NULL_NULL: Socket[addr=/proxyHost,port=proxyPort,localport=24372]]”。这对我来说毫无意义,但这可能是事情在此之后出现问题的原因。
调用result.startHandshake()时,从同一createSocket方法中再次调用,根据调用堆栈,是HttpsClient.afterConnect,参数相同,除了Socket s为空,当再次到达result.startHandshake()时,结果仍然是相同的SSLHandshakeException。
我是否仍然缺少这个日益复杂的难题的重要部分?
这是堆栈跟踪:
javax.net.ssl.SSLHandshakeException:握手期间远程主机关闭连接 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) 原因是:java.io.EOFException:SSL对等方关闭连接不正确 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

你是否曾经检查过代理服务器是否关闭了连接,即代理服务器是否具有内置的证书验证功能? - sfussenegger
错误信息显示“握手期间远程主机关闭了连接”。这是代理还是我正在尝试发送请求的服务器?我对证书验证一无所知。 - Jeff Hillman
从客户端的角度来看,我认为代理也是远程的。因此,我会确保(至少)将代理排除在故障点之外。我有一些信任存储和SSL有时会很痛苦的经验。但我从未见过这样的异常情况。我从未使用过带有SSL的代理,但是首先我会确保代理不会对您的连接造成任何伤害。 - sfussenegger
1
你有没有尝试使用浏览器连接?如果在那里也不起作用,那么一定是代理在愚弄你。 - sfussenegger
通过浏览器连接非常好用(除了我必须解除的证书错误),使用.NET编程连接也可以。目标是使用一个叫做The Grinder的框架进行自动化负载测试。The Grinder使用Jython进行测试脚本,但它的HTTPRequest类似乎相当有限,因此我一直在尝试先用纯Java解决这个问题。 - Jeff Hillman
如果我不通过代理,就会得到UnknownHostException;代理是必要的,而且不是问题,因为通过浏览器或使用C#登录很容易。 - Jeff Hillman
2个回答

34
HTTPS代理没有意义,因为出于安全原因,您无法在代理上终止HTTP连接。根据您的信任策略,如果代理服务器有一个HTTPS端口,它可能会起作用。您的错误是由于使用HTTPS连接到HTTP代理端口引起的。
您可以通过SSL隧道连接到代理(许多人称之为代理),使用代理CONNECT命令。然而,Java不支持较新版本的代理隧道。在这种情况下,您需要自己处理隧道。您可以在这里找到示例代码。

http://www.javaworld.com/javaworld/javatips/jw-javatip111.html

编辑:如果你想要打败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;
    }
 }  );

编辑2:我刚刚尝试了我几年前写的程序,使用SSLTunnelSocketFactory,但它也不起作用。显然,Sun在Java 5中引入了一个新的错误。请参阅这个错误报告。

https://bugs.java.com/bugdatabase/view_bug?bug_id=6614957

好消息是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();
        } 
    }
}

这是当我运行程序时得到的结果,
checkServerTrusted =============
hostnameVerifier =============
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
......

正如你所见,SSLContext和hostnameVerifier都被调用了。只有当主机名与证书不匹配时,才会涉及到HostnameVerifier。我使用了"www.verisign.net"来触发这个情况。

如果我使用本文中讨论的SSLTunnelSocketFactory类,则会覆盖之前设置的默认TrustManager内容。我还需要吗? - Jeff Hillman
信任是在浏览器和最终服务器之间建立的,与隧道无关。你仍然需要它。 - ZZ Coder
我看不到任何方法可以与SSLTunnelSocketFactory类一起使用它。看起来我只能使用其中一个。 - Jeff Hillman
@ZZ Coder:我认为您的编辑没有添加任何我尚未尝试过的内容。我使用了相同的HostnameVerifier,只有一点不同:我调用了静态方法,而您调用了实例方法(您的代码使用了静态方法的名称)。但是,似乎都没有起到作用,因为在“return true”行上设置断点从未被触发,并且仍然抛出相同的异常。 - Jeff Hillman
我在Java 6上尝试了一下,一切都正常。我认为你应该尝试一个新的HttpClient。我建议使用这个http://www.innovation.ch/java/HTTPClient/,它支持HttpsURLConnection,因此您不必重写代码。 - ZZ Coder
显示剩余11条评论

2
尝试使用Apache Commons HttpClient库,而不是自己编写代码: http://hc.apache.org/httpclient-3.x/index.html 以下是他们的示例代码:
  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();
  }
  */

上周我的一个朋友向我推荐了这个。我已经下载了它,但是还没有机会尝试。明天早上我会第一时间尝试它。 - Jeff Hillman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接