热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

关于Https协议和HttpClient的实现详解

一、背景HTTP是一个传输内容有可读性的公开协议,客户端与服务器端的数据完全通过明文传输。在这个背景之下,整个依赖于Http协议的互联网数据都

一、背景

HTTP是一个传输内容有可读性的公开协议,客户端与服务器端的数据完全通过明文传输。在这个背景之下,整个依赖于Http协议的互联网数据都是透明的,这带来了很大的数据安全隐患。想要解决这个问题有两个思路:

  • C/S端各自负责,即客户端与服务端使用协商好的加密内容在Http上通信
  • C/S端不负责加解密,加解密交给通信协议本身解决

关于Https协议和HttpClient的实现详解

第一种在现实中的应用范围其实比想象中的要广泛一些。双方线下交换密钥,客户端在发送的数据采用的已经是密文了,这个密文通过透明的Http协议在互联网上传输。服务端在接收到请求后,按照约定的方式解密获得明文。这种内容就算被劫持了也不要紧,因为第三方不知道他们的加解密方法。然而这种做法太特殊了,客户端与服务端都需要关心这个加解密特殊逻辑。

第二种C/S端可以不关心上面的特殊逻辑,他们认为发送与接收的都是明文,因为加解密这一部分已经被协议本身处理掉了。

从结果上看这两种方案似乎没有什么区别,但是从软件工程师的角度看区别非常巨大。因为第一种需要业务系统自己开发响应的加解密功能,并且线下要交互密钥,第二种没有开发量。

HTTPS是当前最流行的HTTP的安全形式,由NetScape公司首创。在HTTPS中,URL都是以https://开头,而不是http://。使用了HTTPS时,所有的HTTP的请求与响应在发送到网络上之前都进行了加密,这是通过在SSL层实现的。

关于Https协议和HttpClient的实现详解 

二、加密方法

通过SSL层对明文数据进行加密,然后放到互联网上传输,这解决了HTTP协议原本的数据安全性问题。一般来说,对数据加密的方法分为对称加密与非对称加密。

2.1 对称加密

对称加密是指加密与解密使用同样的密钥,常见的算法有DES与AES等,算法时间与密钥长度相关。

关于Https协议和HttpClient的实现详解

  对称密钥最大的缺点是需要维护大量的对称密钥,并且需要线下交换。加入一个网络中有n个实体,则需要n(n-1)个密钥。

2.2 非对称加密

非对称加密是指基于公私钥(public/private key)的加密方法,常见算法有RSA,一般而言加密速度慢于对称加密。

关于Https协议和HttpClient的实现详解

对称加密比非对称加密多了一个步骤,即要获得服务端公钥,而不是各自维护的密钥。

整个加密算法建立在一定的数论基础上运算,达到的效果是,加密结果不可逆。即只有通过私钥(private key)才能解密得到经由公钥(public key)加密的密文。

在这种算法下,整个网络中的密钥数量大大降低,每个人只需要维护一对公司钥即可。即n个实体的网络中,密钥个数是2n。

其缺点是运行速度慢。

2.3 混合加密

周星驰电影《食神》中有一个场景,黑社会火并,争论撒尿虾与牛丸的底盘划分问题。食神说:“真是麻烦,掺在一起做成撒尿牛丸那,笨蛋!”

对称加密的优点是速度快,缺点是需要交换密钥。非对称加密的优点是不需要交互密钥,缺点是速度慢。干脆掺在一起用好了。

混合加密正是HTTPS协议使用的加密方式。先通过非对称加密交换对称密钥,后通过对称密钥进行数据传输。

由于数据传输的量远远大于建立连接初期交换密钥时使用非对称加密的数据量,所以非对称加密带来的性能影响基本可以忽略,同时又提高了效率。

三、HTTPS握手

关于Https协议和HttpClient的实现详解

可以看到,在原HTTP协议的基础上,HTTPS加入了安全层处理:

  • 客户端与服务端交换证书并验证身份,现实中服务端很少验证客户端的证书
  • 协商加密协议的版本与算法,这里可能出现版本不匹配导致失败
  • 协商对称密钥,这个过程使用非对称加密进行
  • 将HTTP发送的明文使用3中的密钥,2中的加密算法加密得到密文
  • TCP层正常传输,对HTTPS无感知

四、HttpClient对HTTPS协议的支持

4.1 获得SSL连接工厂以及域名校验器

作为一名软件工程师,我们关心的是“HTTPS协议”在代码上是怎么实现的呢?探索HttpClient源码的奥秘,一切都要从HttpClientBuilder开始。

public CloseableHttpClient build() {
 //省略部分代码
 HttpClientConnectionManager cOnnManagerCopy= this.connManager;
 //如果指定了连接池管理器则使用指定的,否则新建一个默认的
 if (cOnnManagerCopy== null) {
  LayeredConnectionSocketFactory sslSocketFactoryCopy = this.sslSocketFactory;
  if (sslSocketFactoryCopy == null) {
  //如果开启了使用环境变量,https版本与密码控件从环境变量中读取
  final String[] supportedProtocols = systemProperties ? split(
   System.getProperty("https.protocols")) : null;
  final String[] supportedCipherSuites = systemProperties ? split(
   System.getProperty("https.cipherSuites")) : null;
  //如果没有指定,使用默认的域名验证器,会根据ssl会话中服务端返回的证书来验证与域名是否匹配
  HostnameVerifier hostnameVerifierCopy = this.hostnameVerifier;
  if (hostnameVerifierCopy == null) {
   hostnameVerifierCopy = new DefaultHostnameVerifier(publicSuffixMatcherCopy);
  }
  //如果制定了SslContext则生成定制的SSL连接工厂,否则使用默认的连接工厂
  if (sslContext != null) {
   sslSocketFactoryCopy = new SSLConnectionSocketFactory(
    sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifierCopy);
  } else {
   if (systemProperties) {
   sslSocketFactoryCopy = new SSLConnectionSocketFactory(
    (SSLSocketFactory) SSLSocketFactory.getDefault(),
    supportedProtocols, supportedCipherSuites, hostnameVerifierCopy);
   } else {
   sslSocketFactoryCopy = new SSLConnectionSocketFactory(
    SSLContexts.createDefault(),
    hostnameVerifierCopy);
   }
  }
  }
  //将Ssl连接工厂注册到连接池管理器中,当需要产生Https连接的时候,会根据上面的SSL连接工厂生产SSL连接
  @SuppressWarnings("resource")
  final PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(
   RegistryBuilder.create()
   .register("http", PlainConnectionSocketFactory.getSocketFactory())
   .register("https", sslSocketFactoryCopy)
   .build(),
   null,
   null,
   dnsResolver,
   connTimeToLive,
   connTimeToLiveTimeUnit != null ? connTimeToLiveTimeUnit : TimeUnit.MILLISECONDS);
  //省略部分代码
 }
}

上面的代码将一个Ssl连接工厂SSLConnectionSocketFactory创建,并注册到了连接池管理器中,供之后生产Ssl连接使用。连接池的问题参考:https://www.jb51.net/article/141015.htm

这里在配置SSLConnectionSocketFactory时用到了几个关键的组件,域名验证器HostnameVerifier以及上下文SSLContext。

其中HostnameVerifier用来验证服务端证书与域名是否匹配,有多种实现,DefaultHostnameVerifier采用的是默认的校验规则,替代了之前版本中的BrowserCompatHostnameVerifier与StrictHostnameVerifier。NoopHostnameVerifier替代了AllowAllHostnameVerifier,采用的是不验证域名的策略。

注意,这里有一些区别,BrowserCompatHostnameVerifier可以匹配多级子域名,"*.foo.com"可以匹配"a.b.foo.com"。StrictHostnameVerifier不能匹配多级子域名,只能到"a.foo.com"。

而4.4之后的HttpClient使用了新的DefaultHostnameVerifier替换了上面的两种策略,只保留了一种严格策略及StrictHostnameVerifier。因为严格策略是IE6与JDK本身的策略,非严格策略是curl与firefox的策略。即默认的HttpClient实现是不支持多级子域名匹配策略的。

SSLContext存放的是和密钥有关的关键信息,这部分与业务直接相关,非常重要,这个放在后面单独分析。

4.2 如何获得SSL连接

如何从连接池中获得一个连接,这个过程之前的文章中有分析过,这里不做分析,参考连接:https://www.jb51.net/article/141015.htm。

在从连接池中获得一个连接后,如果这个连接不处于establish状态,就需要先建立连接。

DefaultHttpClientConnectionOperator部分的代码为:

public void connect(
   final ManagedHttpClientConnection conn,
   final HttpHost host,
   final InetSocketAddress localAddress,
   final int connectTimeout,
   final SocketConfig socketConfig,
   final HttpContext context) throws IOException {
  //之前在HttpClientBuilder中register了http与https不同的连接池实现,这里lookup获得Https的实现,即SSLConnectionSocketFactory 
  final Lookup registry = getSocketFactoryRegistry(context);
  final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());
  if (sf == null) {
   throw new UnsupportedSchemeException(host.getSchemeName() +
     " protocol is not supported");
  }
  //如果是ip形式的地址可以直接使用,否则使用dns解析器解析得到域名对应的ip
  final InetAddress[] addresses = host.getAddress() != null ?
    new InetAddress[] { host.getAddress() } : this.dnsResolver.resolve(host.getHostName());
  final int port = this.schemePortResolver.resolve(host);
  //一个域名可能对应多个Ip,按照顺序尝试连接
  for (int i = 0; i  0) {
    sock.setReceiveBufferSize(socketConfig.getRcvBufSize());
   }
   if (socketConfig.getSndBufSize() > 0) {
    sock.setSendBufferSize(socketConfig.getSndBufSize());
   }

   final int linger = socketConfig.getSoLinger();
   if (linger >= 0) {
    sock.setSoLinger(true, linger);
   }
   conn.bind(sock);

   final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);
   if (this.log.isDebugEnabled()) {
    this.log.debug("Connecting to " + remoteAddress);
   }
   try {
    //通过SSLConnectionSocketFactory建立连接并绑定到conn上
    sock = sf.connectSocket(
      connectTimeout, sock, host, remoteAddress, localAddress, context);
    conn.bind(sock);
    if (this.log.isDebugEnabled()) {
     this.log.debug("Connection established " + conn);
    }
    return;
   } 
   //省略一些代码
  }
 }

在上面的代码中,我们看到了是建立SSL连接之前的准备工作,这是通用流程,普通HTTP连接也一样。SSL连接的特殊流程体现在哪里呢?

SSLConnectionSocketFactory部分源码如下:

@Override
 public Socket connectSocket(
   final int connectTimeout,
   final Socket socket,
   final HttpHost host,
   final InetSocketAddress remoteAddress,
   final InetSocketAddress localAddress,
   final HttpContext context) throws IOException {
  Args.notNull(host, "HTTP host");
  Args.notNull(remoteAddress, "Remote address");
  final Socket sock = socket != null ? socket : createSocket(context);
  if (localAddress != null) {
   sock.bind(localAddress);
  }
  try {
   if (connectTimeout > 0 && sock.getSoTimeout() == 0) {
    sock.setSoTimeout(connectTimeout);
   }
   if (this.log.isDebugEnabled()) {
    this.log.debug("Connecting socket to " + remoteAddress + " with timeout " + connectTimeout);
   }
   //建立连接
   sock.connect(remoteAddress, connectTimeout);
  } catch (final IOException ex) {
   try {
    sock.close();
   } catch (final IOException ignore) {
   }
   throw ex;
  }
  // 如果当前是SslSocket则进行SSL握手与域名校验
  if (sock instanceof SSLSocket) {
   final SSLSocket sslsock = (SSLSocket) sock;
   this.log.debug("Starting handshake");
   sslsock.startHandshake();
   verifyHostname(sslsock, host.getHostName());
   return sock;
  } else {
   //如果不是SslSocket则将其包装为SslSocket
   return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context);
  }
 }

 @Override
 public Socket createLayeredSocket(
   final Socket socket,
   final String target,
   final int port,
   final HttpContext context) throws IOException {
   //将普通socket包装为SslSocket,socketfactory是根据HttpClientBuilder中的SSLContext生成的,其中包含密钥信息
  final SSLSocket sslsock = (SSLSocket) this.socketfactory.createSocket(
    socket,
    target,
    port,
    true);
  //如果制定了SSL层协议版本与加密算法,则使用指定的,否则使用默认的
  if (supportedProtocols != null) {
   sslsock.setEnabledProtocols(supportedProtocols);
  } else {
   // If supported protocols are not explicitly set, remove all SSL protocol versions
   final String[] allProtocols = sslsock.getEnabledProtocols();
   final List enabledProtocols = new ArrayList(allProtocols.length);
   for (final String protocol: allProtocols) {
    if (!protocol.startsWith("SSL")) {
     enabledProtocols.add(protocol);
    }
   }
   if (!enabledProtocols.isEmpty()) {
    sslsock.setEnabledProtocols(enabledProtocols.toArray(new String[enabledProtocols.size()]));
   }
  }
  if (supportedCipherSuites != null) {
   sslsock.setEnabledCipherSuites(supportedCipherSuites);
  }

  if (this.log.isDebugEnabled()) {
   this.log.debug("Enabled protocols: " + Arrays.asList(sslsock.getEnabledProtocols()));
   this.log.debug("Enabled cipher suites:" + Arrays.asList(sslsock.getEnabledCipherSuites()));
  }

  prepareSocket(sslsock);
  this.log.debug("Starting handshake");
  //Ssl连接握手
  sslsock.startHandshake();
  //握手成功后校验返回的证书与域名是否一致
  verifyHostname(sslsock, target);
  return sslsock;
 }

可以看到,对于一个SSL通信而言。首先是建立普通socket连接,然后进行ssl握手,之后验证证书与域名一致性。之后的操作就是通过SSLSocketImpl进行通信,协议细节在SSLSocketImpl类中体现,但这部分代码jdk并没有开源,感兴趣的可以下载相应的openJdk源码继续分析。

五、本文总结

  1. https协议是http的安全版本,做到了传输层数据的安全,但对服务器cpu有额外消耗
  2. https协议在协商密钥的时候使用非对称加密,密钥协商结束后使用对称加密
  3. 有些场景下,即使通过了https进行了加解密,业务系统也会对报文进行二次加密与签名
  4. HttpClient在build的时候,连接池管理器注册了两个SslSocketFactory,用来匹配http或者https字符串
  5. https对应的socket建立原则是先建立,后验证域名与证书一致性
  6. ssl层加解密由jdk自身完成,不需要httpClient进行额外操作

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对编程笔记的支持。


推荐阅读
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文分享了一位Android开发者多年来对于Android开发所需掌握的技能的笔记,包括架构师基础、高级UI开源框架、Android Framework开发、性能优化、音视频精编源码解析、Flutter学习进阶、微信小程序开发以及百大框架源码解读等方面的知识。文章强调了技术栈和布局的重要性,鼓励开发者做好学习规划和技术布局,以提升自己的竞争力和市场价值。 ... [详细]
  • 玩转直播系列之消息模块演进(3)
    一、背景即时消息(IM)系统是直播系统重要的组成部分,一个稳定的,有容错的,灵活的,支持高并发的消息模块是影响直播系统用户体验的重要因素。IM长连接服务在直播系统有发挥着举足轻重的 ... [详细]
  • 加密、解密、揭秘
    谈PHP中信息加密技术同样是一道面试答错的问题,面试官问我非对称加密算法中有哪些经典的算法?当时我愣了一下,因为我把非对称加密与单项散列加 ... [详细]
  • 最近手上在进行一个性能测试项目,脚本是java语言使用httpClient实现http请求。并发用户数线程只有40个,但是服务器端启动的线程出现了400多个,是哪里平白无故出现这么多线程呢?肯定是有问 ... [详细]
  • 在Java领域,谈到网络编程,可能大家脑海里第一反应就是MINA,NETTY,GRIZZLY等优秀的开源框架。没错,不过在深入探究这些框架之前,我们需要先从最original的技 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 集成电路企业在进行跨隔离网数据交换时面临着安全性问题,传统的数据交换方式存在安全性堪忧、效率低下等问题。本文以《Ftrans跨网文件安全交换系统》为例,介绍了如何通过丰富的审批流程来满足企业的合规要求,保障数据交换的安全性。 ... [详细]
  • GreenDAO快速入门
    前言之前在自己做项目的时候,用到了GreenDAO数据库,其实对于数据库辅助工具库从OrmLite,到litePal再到GreenDAO,总是在不停的切换,但是没有真正去了解他们的 ... [详细]
  • 转自:http:www.phpweblog.netfuyongjiearchive200903116374.html一直对字符的各种编码方式懵懵懂懂,什 ... [详细]
  • MybatisPlus入门系列(13) MybatisPlus之自定义ID生成器
    数据库ID生成策略在数据库表设计时,主键ID是必不可少的字段,如何优雅的设计数据库ID,适应当前业务场景,需要根据需求选取 ... [详细]
  • 如何使用.NET CORE HttpClient
    小编这次要给大家分享的是如何使用.NETCOREHttpClient,文章内容丰富,感兴趣的小伙伴可以来了解一下,希望大家阅读完这篇文章之后能够有所收获。前 ... [详细]
author-avatar
tanhuixi135_414
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有