Url 统一域名及 DNS 劫持的相关问题

为什么做Url配置?

  • Url 格式不统一。

由于公司的 Url 设计的不合理同时没有统一的规范。每个功能对应着 2 条 Url ,一条正式,一条测试。而且正式接口和测试接口是的域名是有很大差别的(Url 保密 ,有些内容用数字和*代替,文中皆是)。

例如:
搜索的接口正式:http://***.****.net/json.php?do=Search
搜索的接口测试:http://***.test.****.cn/json.php?do=Search

有的接口是可以划分为同一模块,但是并没有。

如下:
搜索的接口:http://123.****.net/json.php?do=Search.Bar
搜索首页信息的接口:http://234.****.com/json.php?do=Main

二者都属于搜索,但是接口差别却很大。域名没有统一。

而且公司中有些不同的功能用的还是同一接口,这样会导致一个接口出问题了等于两个功能同时不能使用,影响很大。

  • 不方便客户端实现代码。
    查看关于 url 的配置这块代码,发现新添加一个功能,需要在很多地方修改代码,根据开闭原则,对于程序应该是对扩展开放,对修改封闭。很明显是违背了这一原则,像我这种菜鸡第一次配置肯定会由于配置混乱而导致Bug出现。

怎么去做?

  • 分析现有 Url 的结构,对所有Url进行模块划分
    上述问题主要是 Url 的域名以及域名之后的参数不够明确对应的功能和域名不够统一,由于 Url 的不规则,导致客户端的实现容易出 Bug,因此我们要做的就是将 Url 进行模块化划分,就是将属于同一个模块的Url进行统一设计和分类。

例如搜索模块
之前的域名由于历史原因不方便修改,所以就不对之前的域名进行修改,以后新添加的功能对应的域名就应该按模块化划分。可以是域名一致,当功能点不同时可以在域名之后添加不同的参数。例如:
搜索功能一的 Url 如下表示:http://search.****.com/function1/...,
搜索模块二的 Url 如下表示:http://search.****.com/function2/...,

  • 对代码进行优化。
    通过分析原有代码发现,添加一个新功能比较繁琐。代码冗余,每添加一个新功能都要添加一个静态方法,静态方法过多会导致不容易做单元测试,而且每产生一个静态方法就会占用一点内存,当静态方法过多时会占用更多内存,严重时会导致内存溢出。

    经上分析,我们目前的优化方式是:
    控制开关( 判断是采用正式接口还是测试接口 )和 Url 接口分别写在 XML 文件中,在 Java 类中只写一个方法去调用开关和 Url 接口然后判断是使用正式接口还是测试接口。

Retrofit 简介

  • 一个类型安全的Android和Java的HTTP客户端
    Retrofit是基于异步线程的网络请求框架,支持线程安全,开发者无需关心线程问题。
  • 使用注解去声明HTTP请求
    例如:
    @GET(“group/{id}/users”)
    Call> groupList(@Path(“id”) int groupId);
    可以通过注解实现Url的拼接,更好管理Url。
  • 由于 Retrofit 是目前(2017.3)地表最强的网络请求框架,对 Restful API 格式的数据具有很好的使用效果。而且 Retrofit 又是采用注解形式发起 HTTP 请求,它的这一特点可以很好的解决一条域名多个参数形式的 Url 。目前存在的 Url 就可以利用 Retrofit 的以上特点去配置 Url 。

DNS劫持和Url配置的关系

  • 起因
    App 中曾经出现过一个地区的用户大面积访问不了页面的后果,经过调查发现是 App 中发起请求的 Url 被劫持了。也就是说用户点击一个功能模块或者进入一个模块查看内容,但是看不到。

造成这种现象可能是本地运营商将我们的 Url 重定向到其他假的ip地址又或者黑客将我们的数据屏蔽掉了,这我们不得而知。但是给我们用户造成的后果就是用户访问不了数据,用户不开心了,体验超级不好!所以为了给用户更好地饿体验,同时提高我们App的容错性,出现DNS防劫持的问题。

  • DNS 劫持解析
    • DNS:Domain Name System 域名系统
      用于将客户端请求的域名解析为对应的 ip 返回给客户端,客户端用返回的 ip 去请求真实的服务器数据。
    • 原理
      本地运营商(移动,联通,电信)将我们请求的 Url 替换为其他的 Url 返回给客户端,从而使客户端访问了虚假的网站,导致用户访问不到真实的服务器数据。

劫持原理

  • 如何去模拟Url被劫持?

    • 利用抓包工具 Fiddler 提供的修改 host 方式模拟被劫持
      设置HOST

      • 可以配置多条被劫持的域名,另起一行添加跳转ip和被劫持的域名即可。
    • 通过将手机和Fiddler连接,然后操作手机App上的功能,在Fiddler的界面中就可以查看到对应的Url,在连接好手机和Fiddler后进入App主页,在Fiddler中可以观察到如下:

由于域名不方便贴出,不理解的回复我

被劫持后的数据是红色的数据,红色代表访问网络失败。当被劫持后主题管家会切换备用域名进行防劫持,之后请求数据会一直使用备用域名进行访问数据。

  • 在Fiddler中分析可以看出劫持后和未被劫持的返回内容是不一样的
    第一张是劫持后的截图,第二张是未被劫持的正常返回内容

第一张
第二张

DNS劫持和Http劫持的区别

  • Http劫持原理
    Http 劫持后返回的内容还是原来的内容,但是运营商会在返回的数据中添加一些广告或者其他内容,以达到盈利的目的。

原理

  • 效果如下:
    在浏览器中点击博客链接浏览博客内容时,会在博客页面的右下角显示广告

网页中的逛广告

如何防范DNS劫持

  • 其他厂家如何防范?

    • 目前主流的形式是利用第三方提供的HttpDNS服务,该服务的作用是将客户端发起请求的域名解析成ip返回给客户端,客户端拿返回的ip重新发起请求。这种方式绕过了local DNS 服务可以有效避免被劫持。
    • 自己实现 HttpDns 服务,新浪开源了他们自己实现的HttpDNS 服务( https://github.com/CNSRE/HTTPDNSLib )。这种方式是小公司实现起来是比较困难的,因为这需要实现一些算法,还有关于线程安全也会涉及到,还有很多问题处理起来比较麻烦。因此大部分公司还是采用第三方完善的服务。
    • 快手防劫持的做法未知,但是它的域名一旦被劫持就会自动切换新的域名或者ip,立刻请求数据。它的这种做法能让用户在没察觉到数据被劫持时切换新的域名去访问数据,能给用户更好的体验。
  • 目前打算如何去做?

    • 采用Retrofit + OkHttp 请求网络,利用第三方提供的HttpDNS服务绕过localDNS 服务,获取域名对应的ip,返回给客户端然后客户端利用OkHttp提供的Interceptor(劫持器)利用拿到ip去重新发起网络请求,实现防劫持。
    • 利用OkHttp 提供的 Dns接口去创建一个类,在类中利用Java提供的InetAddress类获取ip,该操作是直接获取ip后利用ip立刻发起网络请求,实现防劫持

两种方案的具体实现

  • 采用第三方提供的HttpDNS服务

    • 利用DNSPod D+ 提供的域名解析服务获取到请求的域名对应的ip。
      http://119.29.29.29/d?dn=www.baidu.com,前面是DNSPod D+提供的固定格式dn=”这里是要解析的域名 ”,通过如下形式即可返回域名对应的ip,有可能是一个域名对应多个ip,因此这里是在写代码实现的时候是需要考虑多个ip的情况。
    • 创建一个 类去实现Interceptor 通过chain.request()方法拿到请求的信息,在信息对象中拿到Url,通过Url拿到对应的域名,通过一个方法将域名转为ip,利用 String 的 replace 方法将获取到的 ip 将域名替换掉,并设置到原来的请求头中,通过chain.proceed(request)方法重新发起请求

    • 将实现了Interceptor的类通过addInterceptor方法设置到 OkHttp 的请求中去,在用户被劫持后可以通过该劫持器去替换ip重新发起请求。

  • 采用OkHttp 提供的Dns接口

    • 创建实现一个类实现 OkHttp 的 Dns 接口
    • 在第一步创建的类中利用 Java 提供的 InetAddress.getAllByName( hostname ) ,拿到 ip 地址

      1
      List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(hostname));
    • 在初始化 OkHttp 的时候将 Dns 替换为 HttpDns 对象

      1
      2
      3
      OkHttpClient builder = new OkHttpClient.Builder()
      .dns(new HttpDns())
      .build();
  • 两种方案的对比

    • 采用HttpDNS服务

      • 优点
        方便做容灾(容错性比较好),比如ip请求失败时使用域名进行重试
      • 缺点
        a) https场景下ip直连出现的证书校验问题
        https是在一种安全的在应用层的传输协议,它的访问是需要校验证书的合法性,同时根据中国的法律是不允许采用ip直连的。
        b) 第三方代理场景下的HttpDNS问题
        由于采用的是第三方提供的服务,控制权不在我们手上,当第三方提供的服务出现问题,我们没法控制。同时当我们的需求很大时,免费的第三方服务是有可能会进行收费服务的。
        c) ip访问的时候Cookie(缓存在本地的数据)的问题
        由于采用第三方的服务,咱们的域名转换的ip可能会存在第三方的数据库中,那么咱们的ip第三方就会轻易知道,不利于我们的保密工作开展。
    • 采用OkHttp 提供的Dns接口

      • 优点
        a) 在https下不会存在任何问题
        证书校验依然使用域名进行校验;
        ip 直接采用的是底层的 udp 传输协议证书校验是在应用层校验,而 udp 协议是在传输层,因此不会出现检验证书问题。
        b) Cookie的问题也自然不存在。
        由于采用的是 Java 自带的类,是在自己的系统中实现,不会将 ip 泄露出去。
      • 缺点
        a) 过于底层,容灾不好做,除非强制关闭HttpDNS。
        不能通过劫持器去切换域名,或者ip再次发起请求。它只能根据返回的ip进行轮询,如果返回的ip都不对,那肯定是不能请求到数据。
        b) 服务器返回的ip如果不正确,这次请求就挂了,甚至下次也可能挂了。
        由于它只能根据返回的ip进行轮询,不能将域名和ip都进行轮询,因此ip不正确就访问全部挂了。
        c) OkHttp默认对解析结果有一定时间的缓存,万一ttl(数据包在网络中存放的时间)过期了,OkHttp可能依然会去使用,这时候也是有风险的。
        TTL是数据包在网路中存放的时间,如果这个时间过期了,但是OkHttp不能去判断它已经过期,那么OkHttp就会去使用这个TTL提供的数据包去访问数据,得到的数据也不是最新数据,所以还是有问题。

结论

首先是将所有Url进行模块化划分,然后项目老旧的 Url 可能不好修改,所以利用 Retrofit 提供注解请求 HTTP 的特性进行优化,后期添加的功能就按照新制定的规则去设计Url。在防DNS劫持这里,通过比较发现还是使用比较成熟的HttpDNS 方案去实现,然后利用OkHttp 提供的Interceptor 劫持器去实现域名向ip的切换,同时可以在Interceptor中可以利用加权轮询算法实现服务器的负载均衡以及ip的正确分配。

小额支持我写出更好的文章~