HttpURLConnection解析https请求重定向http无效原因

一、问题描述

最近将http向https迁移过程中遇到一个问题,当我使用HttpURLConnection发送了一个Https网络请求时候,返回网络状态码为3xx。大家都知道这个状态码表示重定向,理论上客户端会重新发起一个重定向的请求,但通过抓包分析(Https如何抓包的问题,相信聪明的你可以搞定),发现客户端并没有重新发起请求从而导致App故障。当我切回到Http的时候故障就消失了。

二、问题分析

根据上边的问题,我们先通过Fidder去抓包,然后分析包里边的内容来定位。

  1. 根据抓包,我们得知第一个请求无论是https还是http,返回状态码都是一样的307。说明这个接口的服务地址是没有问题的。
  2. 查看重定向地址,第一次无论是https还是http,返回的重定向地址是一样的,都是http,该重定向地址在浏览器里边访问后,返回状态码是200。
    从上边两点可以得出服务后端的接口是没有问题的,后来就查询http协议文档,文档也没有说明https不能重定向http或者http重定向https。然后就怀疑HttpURLConnection这个请求工具有问题,如何找出问题呢?源码

三、问题探索

首先从发起网络端开始:

 HttpURLConnection conn = (HttpURLConnection)url.openConnection();

url是一个URL类型,查看他的OpenConnection()方法:

public final class URL implements java.io.Serializable {
    /**
     * The URLStreamHandler for this URL.
     */
    transient URLStreamHandler handler;
   .......//省略n行代码
    public URLConnection openConnection() throws java.io.IOException {
        return handler.openConnection(this);
    }

}

我们发现执行openConnection()操作交给了URLStreamHandler handler这个变量,我们看下这个变量如何初始化的:

public final class URL implements java.io.Serializable {
.......//省略n行代码


    public URL(URL context, String spec, URLStreamHandler handler)
        throws MalformedURLException
    {
        String original = spec;
        int i, limit, c;
        int start = 0;
        String newProtocol = null;
        boolean aRef=false;
        boolean isRelative = false;

        // Check for permission to specify a handler
        if (handler != null) {
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                checkSpecifyHandler(sm);
            }
        }

        try {
            limit = spec.length();
            while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) {
                limit--;        //eliminate trailing whitespace
            }
            while ((start < limit) && (spec.charAt(start) <= ' ')) {
                start++;        // eliminate leading whitespace
            }

            if (spec.regionMatches(true, start, "url:", 0, 4)) {
                start += 4;
            }
            if (start < spec.length() && spec.charAt(start) == '#') {
                /* we're assuming this is a ref relative to the context URL.
                 * This means protocols cannot start w/ '#', but we must parse
                 * ref URL's like: "hello:there" w/ a ':' in them.
                 */
                aRef=true;
            }
            for (i = start ; !aRef && (i < limit) &&
                     ((c = spec.charAt(i)) != '/') ; i++) {
                if (c == ':') {

                    String s = spec.substring(start, i).toLowerCase();
                    if (isValidProtocol(s)) {
                        newProtocol = s;
                        start = i + 1;
                    }
                    break;
                }
            }

            // Only use our context if the protocols match.
            protocol = newProtocol;   //根据我们的需求 我们的newProtocol 内容是一个https的字串
            if ((context != null) && ((newProtocol == null) ||
                            newProtocol.equalsIgnoreCase(context.protocol))) {
                // inherit the protocol handler from the context
                // if not specified to the constructor
                if (handler == null) {
                    handler = context.handler;
                }

                // If the context is a hierarchical URL scheme and the spec
                // contains a matching scheme then maintain backwards
                // compatibility and treat it as if the spec didn't contain
                // the scheme; see 5.2.3 of RFC2396
                if (context.path != null && context.path.startsWith("/"))
                    newProtocol = null;

                if (newProtocol == null) {
                    protocol = context.protocol;
                    authority = context.authority;
                    userInfo = context.userInfo;
                    host = context.host;
                    port = context.port;
                    file = context.file;
                    path = context.path;
                    isRelative = true;
                }
            }

            if (protocol == null) {
                throw new MalformedURLException("no protocol: "+original);
            }

            // Get the protocol handler if not specified or the protocol
            // of the context could not be used
            if (handler == null &&
                (handler = getURLStreamHandler(protocol)) == null) {//本地变量handler初始化 然后复制给全局handler  该方法在下边
                throw new MalformedURLException("unknown protocol: "+protocol);
            }

            this.handler = handler; // handler 在这一行进行初始化

            i = spec.indexOf('#', start);
            if (i >= 0) {
                ref = spec.substring(i + 1, limit);
                limit = i;
            }

            /*
             * Handle special case inheritance of query and fragment
             * implied by RFC2396 section 5.2.2.
             */
            if (isRelative && start == limit) {
                query = context.query;
                if (ref == null) {
                    ref = context.ref;
                }
            }

            handler.parseURL(this, spec, start, limit);

        } catch(MalformedURLException e) {
            throw e;
        } catch(Exception e) {
            MalformedURLException exception = new MalformedURLException(e.getMessage());
            exception.initCause(e);
            throw exception;
        }
    }



...//省略n行代码

static URLStreamHandler getURLStreamHandler(String protocol) {

        URLStreamHandler handler = (URLStreamHandler)handlers.get(protocol);//一个Map缓存 先走缓存取 
        if (handler == null) { //没有命中情况下

            boolean checkedWithFactory = false;

            // Use the factory (if any)
            if (factory != null) { //factory == null
                handler = factory.createURLStreamHandler(protocol);
                checkedWithFactory = true;
            }

            // Try java protocol handler
            if (handler == null) {//尝试java协议的handler 
                final String packagePrefixList = System.getProperty(protocolPathProp,"");
                StringTokenizer packagePrefixIter = new StringTokenizer(packagePrefixList, "|");

                while (handler == null &&
                       packagePrefixIter.hasMoreTokens()) {

                    String packagePrefix = packagePrefixIter.nextToken().trim();
                    try {
                        String clsName = packagePrefix + "." + protocol +  ".Handler";
                        Class cls = null;
                        try {
                            ClassLoader cl = ClassLoader.getSystemClassLoader();
                            cls = Class.forName(clsName, true, cl);
                        } catch (ClassNotFoundException e) {
                            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
                            if (contextLoader != null) {
                                cls = Class.forName(clsName, true, contextLoader);
                            }
                        }
                        if (cls != null) {
                            handler  =
                              (URLStreamHandler)cls.newInstance();
                        }
                    } catch (ReflectiveOperationException ignored) {
                    }
                }
            }

            // Fallback to built-in stream handler.
            // Makes okhttp the default http/https handler
            if (handler == null) {//在Android平台 handler == null 应该会走到改语句块内
                try {
                    if (protocol.equals("file")) {
                        handler = (URLStreamHandler)Class.
                            forName("sun.net.www.protocol.file.Handler").newInstance();
                    } else if (protocol.equals("ftp")) {
                        handler = (URLStreamHandler)Class.
                            forName("sun.net.www.protocol.ftp.Handler").newInstance();
                    } else if (protocol.equals("jar")) {
                        handler = (URLStreamHandler)Class.
                            forName("sun.net.www.protocol.jar.Handler").newInstance();
                    } else if (protocol.equals("http")) {
                        handler = (URLStreamHandler)Class.
                            forName("com.android.okhttp.HttpHandler").newInstance();
                    } else if (protocol.equals("https")) {//我们的需求 进入https
                        handler = (URLStreamHandler)Class.
                            forName("com.android.okhttp.HttpsHandler").newInstance();//反射生成com.android.okhttp.HttpsHandler实例
                    }
                } catch (Exception e) {
                    throw new AssertionError(e);
                }
            }

            synchronized (streamHandlerLock) {

                URLStreamHandler handler2 = null;

                // Check again with hashtable just in case another
                // thread created a handler since we last checked
                handler2 = (URLStreamHandler)handlers.get(protocol);

                if (handler2 != null) {
                    return handler2;
                }

                // Check with factory if another thread set a
                // factory since our last check
                if (!checkedWithFactory && factory != null) {
                    handler2 = factory.createURLStreamHandler(protocol);
                }

                if (handler2 != null) {
                    // The handler from the factory must be given more
                    // importance. Discard the default handler that
                    // this thread created.
                    handler = handler2;
                }

                // Insert this handler into the hashtable
                if (handler != null) {
                    handlers.put(protocol, handler);//缓存该实例  
                }

            }
        }

        return handler;// 返回实例

    }

}

通过上述代码,我们发现会通过反射去生成一个com.android.okhttp.HttpsHandler实例,按照该路径我在Andorid 源码里边查找该类,结果没找到,路线再次没有了。

然后google了一下,从回答中探索答案,发现HttpURLConnection从Android 5.0后其http和https是使用okHttp实现的,引用作者的回答:

I can not find *com.android.okhttp.internal.http.HttpURLConnectionImpl * in android aosp5.11 (http://androidxref.com/),
but find *com.squareup.okhttp.internal.huc.HttpURLConnectionImpl *
OkHttp in Android is here: [https://android.googlesource.com/platform/external/okhttp/+/master]
(https://android.googlesource.com/platform/external/okhttp/+/master). The reason the package name is com.android.okhttp is because there are jarjar rules which repackage it under that name.

根据上边英文,我们知道Android 重新导出了新的jar,jar中将 com.squareup这两级目录改成com.android。所以我们直接找com.squareup.okhttp.HttpsHandler,在源码中果真找到了改java文件:

package com.squareup.okhttp;
import java.net.Proxy;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.HttpsURLConnection;
public final class HttpsHandler extends HttpHandler {
    /**
     * The connection spec to use when connecting to an https:// server. Note that Android does
     * not set the cipher suites or TLS versions to use so the socket's defaults will be used
     * instead. When the SSLSocketFactory is provided by the app or GMS core we will not
     * override the enabled ciphers or TLS versions set on the sockets it produces with a
     * list hardcoded at release time. This is deliberate.
     */
    private static final ConnectionSpec TLS_CONNECTION_SPEC = ConnectionSpecs.builder(true)
            .allEnabledCipherSuites()
            .allEnabledTlsVersions()
            .supportsTlsExtensions(true)
            .build();
    private static final List HTTP_1_1_ONLY =
            Collections.singletonList(Protocol.HTTP_1_1);
    private final ConfigAwareConnectionPool configAwareConnectionPool =
            ConfigAwareConnectionPool.getInstance();
    @Override protected int getDefaultPort() {
        return 443;
    }
    @Override
    protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
        OkUrlFactory okUrlFactory = createHttpsOkUrlFactory(proxy);
        // For HttpsURLConnections created through java.net.URL Android uses a connection pool that
        // is aware when the default network changes so that pooled connections are not re-used when
        // the default network changes.
        okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
        return okUrlFactory;
    }
    /**
     * Creates an OkHttpClient suitable for creating {@link HttpsURLConnection} instances on
     * Android.
     */
    // Visible for android.net.Network.
    public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
        // The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
        OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
        // All HTTPS requests are allowed.
        OkUrlFactories.setUrlFilter(okUrlFactory, null);
        OkHttpClient okHttpClient = okUrlFactory.client();
        // Only enable HTTP/1.1 (implies HTTP/1.0). Disable SPDY / HTTP/2.0.
        okHttpClient.setProtocols(HTTP_1_1_ONLY);
        okHttpClient.setConnectionSpecs(Collections.singletonList(TLS_CONNECTION_SPEC));
        // Android support certificate pinning via NetworkSecurityConfig so there is no need to
        // also expose OkHttp's mechanism. The OkHttpClient underlying https HttpsURLConnections
        // in Android should therefore always use the default certificate pinner, whose set of
        // {@code hostNamesToPin} is empty.
        okHttpClient.setCertificatePinner(CertificatePinner.DEFAULT);
        // OkHttp does not automatically honor the system-wide HostnameVerifier set with
        // HttpsURLConnection.setDefaultHostnameVerifier().
        okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
        // OkHttp does not automatically honor the system-wide SSLSocketFactory set with
        // HttpsURLConnection.setDefaultSSLSocketFactory().
        // See https://github.com/square/okhttp/issues/184 for details.
        okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
        return okUrlFactory;
    }
}

我们发现它继承自HttpHandler,查看HttpHandler源码如下:


package com.squareup.okhttp;
import com.squareup.okhttp.internal.URLFilter;
import libcore.net.NetworkSecurityPolicy;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ResponseCache;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class HttpHandler extends URLStreamHandler {
    private final static List CLEARTEXT_ONLY =
        Collections.singletonList(ConnectionSpec.CLEARTEXT);
    private static final CleartextURLFilter CLEARTEXT_FILTER = new CleartextURLFilter();
    private final ConfigAwareConnectionPool configAwareConnectionPool =
            ConfigAwareConnectionPool.getInstance();
    @Override protected URLConnection openConnection(URL url) throws IOException {
        return newOkUrlFactory(null /* proxy */).open(url);
    }
    @Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
        if (url == null || proxy == null) {
            throw new IllegalArgumentException("url == null || proxy == null");
        }
        return newOkUrlFactory(proxy).open(url);
    }
    @Override protected int getDefaultPort() {
        return 80;
    }
    protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
        OkUrlFactory okUrlFactory = createHttpOkUrlFactory(proxy);
        // For HttpURLConnections created through java.net.URL Android uses a connection pool that
        // is aware when the default network changes so that pooled connections are not re-used when
        // the default network changes.
        okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
        return okUrlFactory;
    }
    /**
     * Creates an OkHttpClient suitable for creating {@link java.net.HttpURLConnection} instances on
     * Android.
     */
    // Visible for android.net.Network.
    public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
        OkHttpClient client = new OkHttpClient();
        // Explicitly set the timeouts to infinity.
        client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
        client.setReadTimeout(0, TimeUnit.MILLISECONDS);
        client.setWriteTimeout(0, TimeUnit.MILLISECONDS);
        // Set the default (same protocol) redirect behavior. The default can be overridden for
        // each instance using HttpURLConnection.setInstanceFollowRedirects().
        client.setFollowRedirects(HttpURLConnection.getFollowRedirects());
        // Do not permit http -> https and https -> http redirects.
        client.setFollowSslRedirects(false);
        // Permit cleartext traffic only (this is a handler for HTTP, not for HTTPS).
        client.setConnectionSpecs(CLEARTEXT_ONLY);
        // When we do not set the Proxy explicitly OkHttp picks up a ProxySelector using
        // ProxySelector.getDefault().
        if (proxy != null) {
            client.setProxy(proxy);
        }
        // OkHttp requires that we explicitly set the response cache.
        OkUrlFactory okUrlFactory = new OkUrlFactory(client);
        // Use the installed NetworkSecurityPolicy to determine which requests are permitted over
        // http.
        OkUrlFactories.setUrlFilter(okUrlFactory, CLEARTEXT_FILTER);
        ResponseCache responseCache = ResponseCache.getDefault();
        if (responseCache != null) {
            AndroidInternal.setResponseCache(okUrlFactory, responseCache);
        }
        return okUrlFactory;
    }
    private static final class CleartextURLFilter implements URLFilter {
        @Override
        public void checkURLPermitted(URL url) throws IOException {
            String host = url.getHost();
            if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(host)) {
                throw new IOException("Cleartext HTTP traffic to " + host + " not permitted");
            }
        }
    }
}

通过上边两个java文件我们发现在执行openConnectionpublic()方法过程中,在 HttpsHandler会执行到

public final class HttpsHandler extends HttpHandler {
......
 public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
     // The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
     OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
  ......
 }
}




public class HttpHandler extends URLStreamHandler {

 // Visible for android.net.Network.
 public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
     OkHttpClient client = new OkHttpClient();
    ......
     // Do not permit http -> https and https -> http redirects.//这行代码的注释说明了一切
     client.setFollowSslRedirects(false);
   ......
 }
}

通过HttpHandler.createHttpOkUrlFactory(proxy)方法中的注解我们知道HttpURLConnection请求过程中,https不能重定向到http,相反,http也不能重定向到https也成立。

分析到这这个问题就明朗了,这应该算是HttpURLConnection中的一个bug吧,毕竟在http协议中并没有这么规定,希望大家后边在用到该网络工具时候注意下该问题。

感谢你的耐心阅读,如有错误,欢迎指正。如果本文对你有帮助,记得点赞。欢迎关注我的微信公众号。


qrcode_for_gh_84a02a29fedd_430.jpg

你可能感兴趣的:(HttpURLConnection解析https请求重定向http无效原因)