Https协议与HttpClient的实现
一、背景
HTTP是一个传输内容有可读性的公开协议,客户端与服务器端的数据完全通过明文传输。在这个背景之下,整个依赖于Http协议的互联网数据都是透明的,这带来了很大的数据安全隐患。想要解决这个问题有两个思路:
- C/S端各自负责,即客户端与服务端使用协商好的加密内容在Http上通信
- C/S端不负责加解密,加解密交给通信协议本身解决
第一种在现实中的应用范围其实比想象中的要广泛一些。双方线下交换密钥,客户端在发送的数据采用的已经是密文了,这个密文通过透明的Http协议在互联网上传输。服务端在接收到请求后,按照约定的方式解密获得明文。这种内容就算被劫持了也不要紧,因为第三方不知道他们的加解密方法。然而这种做法太特殊了,客户端与服务端都需要关心这个加解密特殊逻辑。
第二种C/S端可以不关心上面的特殊逻辑,他们认为发送与接收的都是明文,因为加解密这一部分已经被协议本身处理掉了。
从结果上看这两种方案似乎没有什么区别,但是从软件工程师的角度看区别非常巨大。因为第一种需要业务系统自己开发响应的加解密功能,并且线下要交互密钥,第二种没有开发量。
HTTPS是当前最流行的HTTP的安全形式,由NetScape公司首创。在HTTPS中,URL都是以https://开头,而不是http://。使用了HTTPS时,所有的HTTP的请求与响应在发送到网络上之前都进行了加密,这是通过在SSL层实现的。
二、加密方法
通过SSL层对明文数据进行加密,然后放到互联网上传输,这解决了HTTP协议原本的数据安全性问题。一般来说,对数据加密的方法分为对称加密与非对称加密。
2.1 对称加密
对称加密是指加密与解密使用同样的密钥,常见的算法有DES与AES等,算法时间与密钥长度相关。
对称密钥最大的缺点是需要维护大量的对称密钥,并且需要线下交换。加入一个网络中有n个实体,则需要n(n-1)个密钥。
2.2 非对称加密
非对称加密是指基于公私钥(public/private key)的加密方法,常见算法有RSA,一般而言加密速度慢于对称加密。
对称加密比非对称加密多了一个步骤,即要获得服务端公钥,而不是各自维护的密钥。
整个加密算法建立在一定的数论基础上运算,达到的效果是,加密结果不可逆。即只有通过私钥(private key)才能解密得到经由公钥(public key)加密的密文。
在这种算法下,整个网络中的密钥数量大大降低,每个人只需要维护一对公司钥即可。即n个实体的网络中,密钥个数是2n。
其缺点是运行速度慢。
2.3 混合加密
周星驰电影《食神》中有一个场景,黑社会火并,争论撒尿虾与牛丸的底盘划分问题。食神说:“真是麻烦,掺在一起做成撒尿牛丸那,笨蛋!”
对称加密的优点是速度快,缺点是需要交换密钥。非对称加密的优点是不需要交互密钥,缺点是速度慢。干脆掺在一起用好了。
混合加密正是HTTPS协议使用的加密方式。先通过非对称加密交换对称密钥,后通过对称密钥进行数据传输。
由于数据传输的量远远大于建立连接初期交换密钥时使用非对称加密的数据量,所以非对称加密带来的性能影响基本可以忽略,同时又提高了效率。
三、HTTPS握手
可以看到,在原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.<ConnectionSocketFactory>create()
- .register("http", PlainConnectionSocketFactory.getSocketFactory())
- .register("https", sslSocketFactoryCopy)
- .build(),
- null,
- null,
- dnsResolver,
- connTimeToLive,
- connTimeToLiveTimeUnit != null ? connTimeToLiveTimeUnit : TimeUnit.MILLISECONDS);
- //省略部分代码
- }
- }
上面的代码将一个Ssl连接工厂SSLConnectionSocketFactory创建,并注册到了连接池管理器中,供之后生产Ssl连接使用。连接池的问题参考:http://www.cnblogs.com/kingszelda/p/8988505.html
这里在配置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连接
如何从连接池中获得一个连接,这个过程之前的文章中有分析过,这里不做分析,参考连接:http://www.cnblogs.com/kingszelda/p/8988505.html。
在从连接池中获得一个连接后,如果这个连接不处于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<ConnectionSocketFactory> 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 < addresses.length; i++) {
- final InetAddress address = addresses[i];
- final boolean last = i == addresses.length - 1;
- //这里只是生成一个socket,还并没有连接
- Socket sock = sf.createSocket(context);
- //设置一些tcp层的参数
- sock.setSoTimeout(socketConfig.getSoTimeout());
- sock.setReuseAddress(socketConfig.isSoReuseAddress());
- sock.setTcpNoDelay(socketConfig.isTcpNoDelay());
- sock.setKeepAlive(socketConfig.isSoKeepAlive());
- if (socketConfig.getRcvBufSize() > 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<String> enabledProtocols = new ArrayList<String>(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源码继续分析。
五、本文总结
- https协议是http的安全版本,做到了传输层数据的安全,但对服务器cpu有额外消耗
- https协议在协商密钥的时候使用非对称加密,密钥协商结束后使用对称加密
- 有些场景下,即使通过了https进行了加解密,业务系统也会对报文进行二次加密与签名
- HttpClient在build的时候,连接池管理器注册了两个SslSocketFactory,用来匹配http或者https字符串
- https对应的socket建立原则是先建立,后验证域名与证书一致性
- ssl层加解密由jdk自身完成,不需要httpClient进行额外操作
Https协议与HttpClient的实现的更多相关文章
- 使用HttpClient携带pfx证书调用HTTPS协议的WebService
调用第三方服务时,厂商提供了一个WSDL文件.调用的地址和一个后缀为pfx的证书文件,通过SOUPUI记载证书是可以正常调用WebService服务,那么如何将该服务转换为代码呢? 咨询了厂商的支持, ...
- 【原】cocos2d-x 2.0.4 不支持https协议 CURLE_UNSUPPORTED_PROTOCOL
我们项目组用的cocos2d-x版本还比较老,各种好的功能不能用. 今天就让我遇到一个问题,使用CCHttpClient发送http请求的时候,https协议的不支持,返回失败信息如下 errorco ...
- https 协议下服务器根据网络地址下载上传文件问题
https 协议下服务器根据网络地址下载上传文件遇到(PKIX:unable to find valid certification path to requested target 的问题) 使用h ...
- webservice的接口协议(HTTPClient 、RestTemplate HttpURLConnection)
HTTP协议时Internet上使用的很多也很重要的一个协议,越来越多的java应用程序需要通过HTTP协议来访问网络资源. HTTPClient提供的主要功能: 1.实现了所有HTTP的方法(GET ...
- HTTP协议和HTTPS协议初探
概况 HTTP是hypertext transfer protocol(超文本传输协议)的简写.它是TCP/IP协议的一个应用层协议,用于定义WEB浏览器与WEBserver之间交换数据的过程. HT ...
- HttpUtil工具类,发送Get/Post请求,支持Http和Https协议
HttpUtil工具类,发送Get/Post请求,支持Http和Https协议 使用用Httpclient封装的HttpUtil工具类,发送Get/Post请求 1. maven引入httpclien ...
- 网站使用https协议
了解https HTTPS 是以安全为目标的 HTTP 通道,即 HTTP 下加入 SSL 加密层.HTTPS 不同于 HTTP 的端口,HTTP默认端口为80,HTTPS默认端口为443. SSL ...
- 服务器开启https协议
开启Tomcat https服务 发布企业级应用的时候遇到一个问题,就是IOS7.1之后app的下载地址URL必须是https开头的协议,所以服务器必须支持https协议. 实验环境:Mac OSX ...
- 【转】Tomcat启用HTTPS协议配置过程
转载请注明出处: http://blog.csdn.net/gane_cheng/article/details/53001846 http://www.ganecheng.tech/blog/530 ...
随机推荐
- R实战 第七篇:网格(grid)
grid包是R底层的图形系统,可以绘制几乎所有的图形.除了绘制图形之外,grid包还能对图形进行布局.在绘图时,有时候会遇到这样一种情景,客户想把多个代表不同KPI的图形分布到同一个画布(Page)上 ...
- linux下redis单机版搭建
1.1.什么是redis Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库.它通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下: ...
- PhpStorm服务激活
日期 服务地址 状态 2018-03-15 http://idea.singee77.com/ 使用中
- 基于DP的LCS(最长公共子序列)问题
最长公共子序列,即给出两个序列,给出最长的公共序列,例如: 序列1 understand 序列2 underground 最长公共序列undernd,长度为7 一般这类问题很适合使用动态规划,其动态规 ...
- Java中浮点数的精度问题 【转】
当您在计算Money的时候,请看好了!!!要不损失了别后悔!!! 现象1: public static void main(String[] args) { System.out.println(0. ...
- 洛谷 P1879 解题报告
P1879 [USACO06NOV]玉米田Corn Fields 题目描述 农场主\(John\)新买了一块长方形的新牧场,这块牧场被划分成\(M\)行\(N\)列\((1 ≤ M ≤ 12; 1 ≤ ...
- hdu-2683 TCE-frep number system---完全数+二项展开式
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=2683 题目大意: g(n)是n的因子和 两种操作: A a b 查询a b区间有多少个n满足上式. ...
- SpringBoot开发案例从0到1构建分布式秒杀系统
前言 最近,被推送了不少秒杀架构的文章,忙里偷闲自己也总结了一下互联网平台秒杀架构设计,当然也借鉴了不少同学的思路.俗话说,脱离案例讲架构都是耍流氓,最终使用SpringBoot模拟实现了部分秒杀场 ...
- facenet 进行人脸识别测试
1.简介:facenet 是基于 TensorFlow 的人脸识别开源库,有兴趣的同学可以扒扒源代码:https://github.com/davidsandberg/facenet 2.安装和配置 ...
- Codeforces Round #483 (Div. 2) D. XOR-pyramid
D. XOR-pyramid time limit per test 2 seconds memory limit per test 512 megabytes input standard inpu ...