HTTPS URL 的基本代理身份验证返回 HTTP/1.0 407 需要代理身份验证

Posted

技术标签:

【中文标题】HTTPS URL 的基本代理身份验证返回 HTTP/1.0 407 需要代理身份验证【英文标题】:Basic proxy authentication for HTTPS URLs returns HTTP/1.0 407 Proxy Authentication Required 【发布时间】:2016-04-24 23:11:45 【问题描述】:

我想在 Java 中使用具有基本身份验证(用户名、密码)的代理进行连接(并且仅此连接)。以下代码适用于 HTTP URL(例如“http://www.google.com”):

URL url = new URL("http://www.google.com");
HttpURLConnection httpURLConnection = null;
InetSocketAddress proxyLocation = new InetSocketAddress(proxyHost, proxyPort);
Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyLocation);
httpURLConnection = (HttpURLConnection) url.openConnection(proxy);
// Works for HTTP only! Doesn't work for HTTPS!
String encoded = new sun.misc.BASE64Encoder().encodeBuffer((proxyUserName + ":" + proxyPassword).getBytes()).replace("\r\n", "");
httpURLConnection.setRequestProperty("Proxy-Authorization", "Basic " + encoded);
InputStream is = httpURLConnection.getInputStream();
InputStreamReader isr = new InputStreamReader(is); 
int data = isr.read();
while(data != -1)
  char c = (char) data;
  data = isr.read();
  System.out.print(c);

isr.close();

不过,该代码不适用于 HTTPS URL(例如“https://www.google.com”)!当我尝试访问 HTTPS URL 时,我得到了java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.0 407 Proxy Authentication Required"

此代码适用于 HTTP 和 HTTPS:

URL url = new URL("https://www.google.com");
HttpURLConnection httpURLConnection = null;
InetSocketAddress proxyLocation = new InetSocketAddress(proxyHost, proxyPort);
Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyLocation);
httpURLConnection = (HttpURLConnection) url.openConnection(proxy);
// Works for HTTP and HTTPS, but sets a global default!
Authenticator.setDefault(new Authenticator() 
  protected PasswordAuthentication getPasswordAuthentication() 
    return new PasswordAuthentication(proxyUserName, proxyPassword.toCharArray());
  
);
InputStream is = httpURLConnection.getInputStream();
InputStreamReader isr = new InputStreamReader(is); 
int data = isr.read();
while(data != -1)
  char c = (char) data;
  data = isr.read();
  System.out.print(c);

isr.close();

第二个代码的问题是它设置了一个新的默认值Authenticator,我不想这样做,因为这个代理只被应用程序的一部分使用,而应用程序的不同部分可能是使用不同的代理。我不想为整个应用程序设置全局默认值。有没有办法让第一个代码与 HTTPS 一起使用,或者在不将其设置为默认值的情况下使用 Authenticator 的方法?

我必须使用java.net.HttpURLConnection,因为我重写了一个必须返回HttpURLConnection 的类的方法,所以我不能使用Apache HttpClient。

【问题讨论】:

你可以自己扩展 HttpURLConnection,并使用像 Apache HTTP CLient has done in the past 这样的库来实现每个方法,但是这样做需要很长时间,我已经用了 2 天的黑客攻击没有结果的解决方案跨度> HttpURLConnection + HTTPS + 代理身份验证已在 Oracle JDK 中默认禁用,稍后在这篇文章之后,请参阅 bugs.openjdk.java.net/browse/JDK-8210814 【参考方案1】:

您可以扩展ProxiedHttpsConnection 并自己处理所有低级相关的东西。

需要执行以下步骤才能通过 HTTP 代理连接到 https 网站:

注意:与代理和http服务器的通信应该在ASCII7。

    向代理发送CONNECT ***.com:443 HTTP/1.0\r\n 发送您的身份验证:Proxy-Authorization: Basic c2F5WW91SGF2ZVNlZW5UaGlzSW5UaGVDb21tZW50cw==\r\n。 结束第一个请求:\r\n 读取来自代理的响应,直到您看到组合“\r\n\r\n”。 解析您从代理获得的响应的第一行,并检查它是否以 HTTP/1.0 200 开头。 在现有连接上启动 SSL 会话。 发送一个http请求的开始:GET /questions/3304006/persistent-httpurlconnection-in-java HTTP/1.0\r\n 设置正确的主机头:Host: ***.com\r\n 结束对http服务器的请求:\r\n 读到\r\n 并将第一行解析为状态消息 读到请求正文的流结束

当我们要实现 HttpUrlConnection 类时,我们还需要考虑以下几点:

在构造类时,该类应存储数据以供将来连接,但不能直接建立 可以按任何顺序调用任何方法 OutputStream 的关闭意味着数据传输完成,而不是连接必须完成 每个 api 以不同的顺序使用方法 HTTP 标头不区分大小写,Java 映射区分大小写。

快说,有很多陷阱

在我设计的类中,它使用布尔标志来记住是否调用了connect 方法和afterPostClosure 方法,它还支持在OutputStream 关闭之前调用getInputStream()

这个类还对套接字返回的流使用尽可能少的包装,以防止变得非常复杂。

public class ProxiedHttpsConnection extends HttpURLConnection 

    private final String proxyHost;
    private final int proxyPort;
    private static final byte[] NEWLINE = "\r\n".getBytes();//should be "ASCII7"

    private Socket socket;
    private final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private final Map<String, List<String>> sendheaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private final Map<String, List<String>> proxyheaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private final Map<String, List<String>> proxyreturnheaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private int statusCode;
    private String statusLine;
    private boolean isDoneWriting;

    public ProxiedHttpsConnection(URL url,
            String proxyHost, int proxyPort, String username, String password)
            throws IOException 
        super(url);
        socket = new Socket();
        this.proxyHost = proxyHost;
        this.proxyPort = proxyPort;
        String encoded = Base64.encode((username + ":" + password).getBytes())
                .replace("\r\n", "");
        proxyheaders.put("Proxy-Authorization", new ArrayList<>(Arrays.asList("Basic " + encoded)));
    

    @Override
    public OutputStream getOutputStream() throws IOException 
        connect();
        afterWrite();
        return new FilterOutputStream(socket.getOutputStream()) 
            @Override
            public void write(byte[] b, int off, int len) throws IOException 
                out.write(String.valueOf(len).getBytes());
                out.write(NEWLINE);
                out.write(b, off, len);
                out.write(NEWLINE);
            

            @Override
            public void write(byte[] b) throws IOException 
                out.write(String.valueOf(b.length).getBytes());
                out.write(NEWLINE);
                out.write(b);
                out.write(NEWLINE);
            

            @Override
            public void write(int b) throws IOException 
                out.write(String.valueOf(1).getBytes());
                out.write(NEWLINE);
                out.write(b);
                out.write(NEWLINE);
            

            @Override
            public void close() throws IOException 
                afterWrite();
            

        ;
    

    private boolean afterwritten = false;

    @Override
    public InputStream getInputStream() throws IOException 
        connect();
        return socket.getInputStream();

    

    @Override
    public void setRequestMethod(String method) throws ProtocolException 
        this.method = method;
    

    @Override
    public void setRequestProperty(String key, String value) 
        sendheaders.put(key, new ArrayList<>(Arrays.asList(value)));
    

    @Override
    public void addRequestProperty(String key, String value) 
        sendheaders.computeIfAbsent(key, l -> new ArrayList<>()).add(value);
    

    @Override
    public Map<String, List<String>> getHeaderFields() 
        return headers;
    

    @Override
    public void connect() throws IOException 
        if (connected) 
            return;
        
        connected = true;
        socket.setSoTimeout(getReadTimeout());
        socket.connect(new InetSocketAddress(proxyHost, proxyPort), getConnectTimeout());
        StringBuilder msg = new StringBuilder();
        msg.append("CONNECT ");
        msg.append(url.getHost());
        msg.append(':');
        msg.append(url.getPort() == -1 ? 443 : url.getPort());
        msg.append(" HTTP/1.0\r\n");
        for (Map.Entry<String, List<String>> header : proxyheaders.entrySet()) 
            for (String l : header.getValue()) 
                msg.append(header.getKey()).append(": ").append(l);
                msg.append("\r\n");
            
        

        msg.append("Connection: close\r\n");
        msg.append("\r\n");
        byte[] bytes;
        try 
            bytes = msg.toString().getBytes("ASCII7");
         catch (UnsupportedEncodingException ignored) 
            bytes = msg.toString().getBytes();
        
        socket.getOutputStream().write(bytes);
        socket.getOutputStream().flush();
        byte reply[] = new byte[200];
        byte header[] = new byte[200];
        int replyLen = 0;
        int headerLen = 0;
        int newlinesSeen = 0;
        boolean headerDone = false;
        /* Done on first newline */
        InputStream in = socket.getInputStream();
        while (newlinesSeen < 2) 
            int i = in.read();
            if (i < 0) 
                throw new IOException("Unexpected EOF from remote server");
            
            if (i == '\n') 
                if (newlinesSeen != 0) 
                    String h = new String(header, 0, headerLen);
                    String[] split = h.split(": ");
                    if (split.length != 1) 
                        proxyreturnheaders.computeIfAbsent(split[0], l -> new ArrayList<>()).add(split[1]);
                    
                
                headerDone = true;
                ++newlinesSeen;
                headerLen = 0;
             else if (i != '\r') 
                newlinesSeen = 0;
                if (!headerDone && replyLen < reply.length) 
                    reply[replyLen++] = (byte) i;
                 else if (headerLen < reply.length) 
                    header[headerLen++] = (byte) i;
                
            
        

        String replyStr;
        try 
            replyStr = new String(reply, 0, replyLen, "ASCII7");
         catch (UnsupportedEncodingException ignored) 
            replyStr = new String(reply, 0, replyLen);
        

        // Some proxies return http/1.1, some http/1.0 even we asked for 1.0
        if (!replyStr.startsWith("HTTP/1.0 200") && !replyStr.startsWith("HTTP/1.1 200")) 
            throw new IOException("Unable to tunnel. Proxy returns \"" + replyStr + "\"");
        
        SSLSocket s = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory.getDefault())
                .createSocket(socket, url.getHost(), url.getPort(), true);
        s.startHandshake();
        socket = s;
        msg.setLength(0);
        msg.append(method);
        msg.append(" ");
        msg.append(url.toExternalForm().split(String.valueOf(url.getPort()), -2)[1]);
        msg.append(" HTTP/1.0\r\n");
        for (Map.Entry<String, List<String>> h : sendheaders.entrySet()) 
            for (String l : h.getValue()) 
                msg.append(h.getKey()).append(": ").append(l);
                msg.append("\r\n");
            
        
        if (method.equals("POST") || method.equals("PUT")) 
            msg.append("Transfer-Encoding: Chunked\r\n");
        
        msg.append("Host: ").append(url.getHost()).append("\r\n");
        msg.append("Connection: close\r\n");
        msg.append("\r\n");
        try 
            bytes = msg.toString().getBytes("ASCII7");
         catch (UnsupportedEncodingException ignored) 
            bytes = msg.toString().getBytes();
        
        socket.getOutputStream().write(bytes);
        socket.getOutputStream().flush();
    

    private void afterWrite() throws IOException 
        if (afterwritten) 
            return;
        
        afterwritten = true;
        socket.getOutputStream().write(String.valueOf(0).getBytes());
        socket.getOutputStream().write(NEWLINE);
        socket.getOutputStream().write(NEWLINE);
        byte reply[] = new byte[200];
        byte header[] = new byte[200];
        int replyLen = 0;
        int headerLen = 0;
        int newlinesSeen = 0;
        boolean headerDone = false;
        /* Done on first newline */
        InputStream in = socket.getInputStream();
        while (newlinesSeen < 2) 
            int i = in.read();
            if (i < 0) 
                throw new IOException("Unexpected EOF from remote server");
            
            if (i == '\n') 
                if (headerDone) 
                    String h = new String(header, 0, headerLen);
                    String[] split = h.split(": ");
                    if (split.length != 1) 
                        headers.computeIfAbsent(split[0], l -> new ArrayList<>()).add(split[1]);
                    
                
                headerDone = true;
                ++newlinesSeen;
                headerLen = 0;
             else if (i != '\r') 
                newlinesSeen = 0;
                if (!headerDone && replyLen < reply.length) 
                    reply[replyLen++] = (byte) i;
                 else if (headerLen < header.length) 
                    header[headerLen++] = (byte) i;
                
            
        

        String replyStr;
        try 
            replyStr = new String(reply, 0, replyLen, "ASCII7");
         catch (UnsupportedEncodingException ignored) 
            replyStr = new String(reply, 0, replyLen);
        

        /* We asked for HTTP/1.0, so we should get that back */
        if ((!replyStr.startsWith("HTTP/1.0 200")) && !replyStr.startsWith("HTTP/1.1 200")) 
            throw new IOException("Server returns \"" + replyStr + "\"");
        
    

    @Override
    public void disconnect() 
        try 
            socket.close();
         catch (IOException ex) 
            Logger.getLogger(ProxiedHttpsConnection.class.getName()).log(Level.SEVERE, null, ex);
        
    

    @Override
    public boolean usingProxy() 
        return true;
    

上述代码的当前错误:

在发布期间不会因错误而关闭流 在与代理的初始联系出错期间不会关闭流 不支持 http 重定向 它不支持 http 1.1 的东西,比如 chunked 和 gzip 编码,但这没问题,因为我们宣布自己是 http1.0 客户端。

上面的代码可以这样使用:

    ProxiedHttpsConnection n = new ProxiedHttpsConnection(
            new URL("https://***.com:443/questions/3304006/persistent-httpurlconnection-in-java"), 
            "proxy.example.com", 8080, "root", "flg83yvem#");
    n.setRequestMethod("GET");
    n.addRequestProperty("User-Agent", "Java test https://***.com/users/1542723/ferrybig");
    //try (OutputStream out = n.getOutputStream()) 
    //  out.write("Hello?".getBytes());
    //
    try (InputStream in = n.getInputStream()) 
        byte[] buff = new byte[1024];
        int length;
        while ((length = in.read(buff)) >= 0) 
            System.out.write(buff, 0, length);
        
    

如果你打算将它与一种代理选择器一起使用,你应该检查 url 的协议,看看它是 http 还是 https,如果是 http,不要使用这个类,而是手动附加标头喜欢:

httpURLConnection.setRequestProperty("Proxy-Authorization", "Basic " + encoded);

为什么不使用httpsUrlConnection.setSSLSocketFactory

虽然 java 有这种方法,但尝试使用它会告诉你为什么它不起作用,java 只是不断调用 createSocket(Socket s, String host, int port, boolean autoClose) 并使用已经打开的连接,因此无法手动执行代理操作。

【讨论】:

谢谢!我注意到读取 CONNECT 响应标头和读取隧道响应标头实现在一处有所不同。可能读取 CONNECT 响应头应该与另一个对齐,即 if (i == '\n') if (newlinesSeen != 0) 应该更正为 if (i == '\n') if (headerDone) 。在 Java 8 中,两者都可以合并到一个 impl. 中,将 proxyreturnheaders::computeIfAbsentheaders::computeIfAbsent 作为 arg 类型传递:BiFunction&lt;String, Function&lt;String, List&lt;String&gt;&gt;, List&lt;String&gt;&gt;【参考方案2】:

好的,这就是你需要做的,

public class ProxyAuth extends Authenticator 
    private PasswordAuthentication auth;

    ProxyAuth(String user, String password) 
        auth = new PasswordAuthentication(user, password == null ? new char[] : password.toCharArray());
    

    protected PasswordAuthentication getPasswordAuthentication() 
        return auth;
    

.

public class ProxySetup 
    public HttpURLConnection proxySetup(String urlInput)
    
        URL url;
        try 
            url = new URL(urlInput);

            Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.66.182.100", 80)); // or whatever your proxy is
            HttpURLConnection uc = (HttpURLConnection)url.openConnection(proxy);
            System.setProperty("https.proxyHost", "10.66.182.100");
            System.setProperty("https.proxyPort", "80");
            System.setProperty("http.proxyHost", "10.66.182.100");
            System.setProperty("http.proxyPort", "80");
            String encoded = new String(Base64.encodeBase64(("domain\\Username" + ":" + "Password").getBytes()));

            uc.setRequestProperty("Proxy-Authorization", "Basic " + encoded);
            Authenticator.setDefault(new ProxyAuth("domain\\Username", "Password"));

            System.out.println("ProxySetup : proxySetup");
            return uc;
         catch (Exception e) 
            // TODO Auto-generated catch block
            System.out.println("ProxySetup : proxySetup - Failed");
            e.printStackTrace();
        
        return null;
    

像这样使用它。

HttpURLConnection conn = new ProxySetup().proxySetup(URL)

【讨论】:

这只是设置默认身份验证器的一种奇特方式。将 https.proxyPort property is only meaningful if the proxy server can handle the HTTPS protocol. In your example you are setting http.proxyPortAND https.proxyPort 设置为 80。代理如何处理同一端口上的两种协议(不同的服务)?如果您设置了身份验证器,则设置 Proxy-Authorization 标头毫无意义。 我遇到了一个复杂的场景。我在其中一次处理两种类型的代理。无论如何,该代码仅供参考,并非生产就绪。 否决,因为它设置了默认身份验证器,然后在 jvm 范围内有效。 OP 要求“仅连接”【参考方案3】:

不幸的是,对于您想要实现的目标没有简单的解决方案。您的第一个代码不适用于 HTTPS,因为您直接设置身份验证标头。由于客户端对所有数据进行加密,因此代理服务器无法从请求中提取任何信息。

事实上,HTTPS 和代理服务器以相反的方式工作。代理服务器希望查看在客户端和最终服务器之间流动的所有数据,并根据它看到的内容采取行动。另一方面,HTTPS 协议对所有数据进行加密,因此在数据到达最终目的地之前没有人可以看到数据。加密算法在客户端和最终目的地之间协商,因此代理服务器无法解密任何信息,实际上它甚至无法知道客户端使用的是哪种协议。

要在 HTTPS 连接上使用代理服务器,客户端必须建立隧道。为此,它必须直接向代理发出 CONNECT 命令,例如:

CONNECT www.google.com:443 HTTP/1.0

并发送凭据以通过代理服务器进行身份验证。

如果连接成功,客户端可以通过连接发送和接收数据。代理服务器对数据完全视而不见。数据只在客户端和服务器之间通过它。

当你在 HTTP URL 上执行 url.openConnection(proxy) 时,它会返回一个 HttpURLConnection 的实例,当你在 HTTPS URL 上运行时,就像在你的第二个代码中一样,它会返回一个 HttpsURLConnection 的实例。

您收到 407 错误代码是因为代理服务器无法从您发送的标头中提取身份验证信息。查看异常堆栈,我们可以看到异常被抛出sun.net.www.protocol.http.HttpURLConnection.doTunneling(),它发出 CONNECT 命令以通过代理建立 HTTPS 隧道。在sun.net.www.protocol.http.HttpURLConnection的源代码中我们可以看到:

/* We only have a single static authenticator for now.
 * REMIND:  backwards compatibility with JDK 1.1.  Should be
 * eliminated for JDK 2.0.
 */
private static HttpAuthenticator defaultAuth;

因此,默认身份验证器似乎是提供代理凭据的唯一方法。

为了做你想做的事,你必须深入到连接级别并自己处理 HTTP 协议,因为你必须与代理服务器而不是直接与 Google 服务器对话。

【讨论】:

难道 HttpsUrlConnection 不应该足够聪明地理解“Proxy-Authentication”标头是针对代理而不是针对 Web 服务器的吗?它不应该只在“CONNECT”请求中而不是在对 Web 服务器的请求中发送未加密的“Proxy-Authentication”标头吗?我认为 HttpsUrlConnection 应该在内部处理这个以启用对代理的抢先身份验证。【参考方案4】:

你可以使用 HttpsUrlConnection 吗?它扩展了 HttpUrlConnection,因此从类返回时转换为 HttpUrlConnection 可能没问题。

代码类似,使用名称中带有https的HttpUrlConnection代替。

使用以下代码:

if (testUrlHttps.getProtocol().toLowerCase().equals("https")) 
   trustAllHosts();
   HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
   https.setHostnameVerifier(DO_NOT_VERYFY);
   urlCon = https;
 else 
   urlCon = (HttpURLConnection) url.openConnection();

来源:

[1]https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/HttpsURLConnection.html

[2] HttpURLConnection - "https://" vs. "http://" (sn-p)

【讨论】:

我应该指出,这并不能回答问题。类型转换是无用的,因为它不会改变(也不能)改变openConnection 调用的返回值。 @John 想要的是通过代理服务器连接到 HTTPS 服务器,这个答案没有解决。

以上是关于HTTPS URL 的基本代理身份验证返回 HTTP/1.0 407 需要代理身份验证的主要内容,如果未能解决你的问题,请参考以下文章

Ruby:摘要代理身份验证

yum repo 的 url 中带有 cntlm 代理和基本身份验证参数的空服务器响应

无法通过代理进行隧道传输。代理通过 https 返回“HTTP/1.1 407”

无法通过代理进行隧道传输。代理通过 https 返回“HTTP/1.1 407”

带有 HTTP 基本身份验证的 Angular 6 HTTP Get 请求

通过添加从另一个请求返回的身份验证令牌来修改 http 代理请求