方法简介

本篇博文以Okhttp 4.6.0来解析hostnameVerfier的作用,顾名思义,该方法的主要作用就是鉴定hostnname的合法性。Okhttp在初始化的时候我们可以自己配置hostnameVerfier

new OkHttpClient.Builder()
    .connectTimeout(20, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .writeTimeout(35, TimeUnit.SECONDS)  
    .hostnameVerifier(new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            //注意这里在生产环境中千万不要直接写死true          
            return true;
        }
    })
    .build();

但是网上好多资料将verfiy直接返回true是十分危险的。当然如果vertify返回fasle意味着hostname验证不通过,http请求无法成功,比如我以自己的博客地址发起http请求,错误信息如下:
在这里插入图片描述

{http errorCode=-500, mErrorMsg=Hostname yanchen.blog.csdn.net not verified:
    certificate: sha256/tlnf6pbfeu257hnJ9e6j4A1ZWH3vVMzn3Zn3F9kLHdg=
    DN: CN=*.blog.csdn.net
    subjectAltNames: [*.blog.csdn.net]}

执行vertify的地方是在RealConnection里面,执行之后。
在这里插入图片描述

除了自定义hostnameVerfier之外,Okhttp提供了默认实现,现在就分析下起内部原理。

核心原理

在Okhttp内置了OkHostnameVerifier,该方法通过session.peerCertificates[0] as X509Certificate获取证书的对象

override fun verify(host: String, session: SSLSession): Boolean {
    return try {
      verify(host, session.peerCertificates[0] as X509Certificate)
    } catch (_: SSLException) {
      false
    }
  }

fun verify(host: String, certificate: X509Certificate): Boolean {
    return when {
      host.canParseAsIpAddress() -> verifyIpAddress(host, certificate)
      else -> verifyHostname(host, certificate)
    }
  }

通过X509Certificate对象提供了一系列get方法可以获取到证书的公钥,序列号等一系列信息。见下图:
在这里插入图片描述
最终会调用verifyHostname方法,通过certificate获取getSubjectAltNames拿到SubjectAltName之后,将hostname与SubjectAltName进行比对,如果符合就返回true,否则就返回fasle.

private fun verifyHostname(hostname: String, certificate: X509Certificate): Boolean {
    val hostname = hostname.toLowerCase(Locale.US)
    return getSubjectAltNames(certificate, ALT_DNS_NAME).any {
      verifyHostname(hostname, it)
    }
  }

//hostname和SubjectAltName比对
private fun verifyHostname(hostname: String?, pattern: String?): Boolean {
    var hostname = hostname
    var pattern = pattern
    //检验客户端域名的有效性
    if (hostname.isNullOrEmpty() ||
        hostname.startsWith(".") ||
        hostname.endsWith("..")) {
      // Invalid domain name
      return false
    }
    //检验证书中SubjectAltName的有效性
    if (pattern.isNullOrEmpty() ||
        pattern.startsWith(".") ||
        pattern.endsWith("..")) {
      // Invalid pattern/domain name
      return false
    }

    // Normalize hostname and pattern by turning them into absolute domain names if they are not
    // yet absolute. This is needed because server certificates do not normally contain absolute
    // names or patterns, but they should be treated as absolute. At the same time, any hostname
    // presented to this method should also be treated as absolute for the purposes of matching
    // to the server certificate.
    //   www.android.com  matches www.android.com
    //   www.android.com  matches www.android.com.
    //   www.android.com. matches www.android.com.
    //   www.android.com. matches www.android.com
    if (!hostname.endsWith(".")) {
      hostname += "."
    }
    if (!pattern.endsWith(".")) {
      pattern += "."
    }
    // Hostname and pattern are now absolute domain names.

    pattern = pattern.toLowerCase(Locale.US)
    // Hostname and pattern are now in lower case -- domain names are case-insensitive.

    if ("*" !in pattern) {
      // Not a wildcard pattern -- hostname and pattern must match exactly.
      return hostname == pattern
    }

    // Wildcard pattern

    // WILDCARD PATTERN RULES:
    // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
    //    only character in that label (i.e., must match the whole left-most label).
    //    For example, *.example.com is permitted, while *a.example.com, a*.example.com,
    //    a*b.example.com, a.*.example.com are not permitted.
    // 2. Asterisk (*) cannot match across domain name labels.
    //    For example, *.example.com matches test.example.com but does not match
    //    sub.test.example.com.
    // 3. Wildcard patterns for single-label domain names are not permitted.

    if (!pattern.startsWith("*.") || pattern.indexOf('*', 1) != -1) {
      // Asterisk (*) is only permitted in the left-most domain name label and must be the only
      // character in that label
      return false
    }

    // Optimization: check whether hostname is too short to match the pattern. hostName must be at
    // least as long as the pattern because asterisk must match the whole left-most label and
    // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters.
    if (hostname.length < pattern.length) {
      return false // Hostname too short to match the pattern.
    }

    if ("*." == pattern) {
      return false // Wildcard pattern for single-label domain name -- not permitted.
    }

    // Hostname must end with the region of pattern following the asterisk.
    val suffix = pattern.substring(1)
    if (!hostname.endsWith(suffix)) {
      return false // Hostname does not end with the suffix.
    }

    // Check that asterisk did not match across domain name labels.
    val suffixStartIndexInHostname = hostname.length - suffix.length
    if (suffixStartIndexInHostname > 0 &&
        hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1) {
      return false // Asterisk is matching across domain name labels -- not permitted.
    }

    // Hostname matches pattern.
    return true
  }

那么SubjectAltName是什么?我们可以通过如下方法获取:

new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
              try {
                X509Certificate x509Certificate= (X509Certificate) session.getPeerCertificates()[0];
                Collection<List<?>> subjectAltNames = x509Certificate.getSubjectAlternativeNames();
                for (List<?> subjectAltName : subjectAltNames) {
                    if (subjectAltName == null || subjectAltName.size() < 2) continue;
                    int type = (int)subjectAltName.get(0);
                    if (type!= 2) continue;
                    String altName = (String)subjectAltName.get(1);
                    LogUtil.logD("hostnameVerifier","x509Certificate altName=="+altName);
                }
            } catch (Exception e) {

            }     
            return true;
        }
    }

在这里插入图片描述
在这里插入图片描述
Okhttp 内置的hostname校验逻辑很简单,大家可以自行查看起源码即可。

参考资料

  1. Android CertificateSource系统根证书的检索和获取
  2. Android https TrustManager checkServerTrusted 详解
  3. Android RootTrustManager 证书校验简单分析
  4. Android CertificateSource系统根证书的检索和获取
  5. Android AndroidNSSP的简单说明
  6. Okhttp之RealConnection建立链接简单分析

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部