面试官:工作中和服务端走的都是https协议吧?为啥要使用https?
我:因为https是安全的传输协议
面试官:HTTPS是安全的吗?我看你简历写掌握okhttp,那使用okhttp有啥方案可以避免网络劫持?
我:网络安全这一块我不太了解。。。
内心oos:平时天天打交道的http和https,但是确实没有去管他是否安全,那是安全部门做的,我一个螺丝钉,业务都搞不过来,哪有时间了解o(╥﹏╥)o。
😎😎😎如果之前没有时间,现在就收藏一下也不迟。
一、正确理解http协议
HTTP发展历程
1989 年 3 月 12 日,欧洲粒子物理研究所(CERN)的计算机科学家蒂姆·伯纳斯·李在其一份提案《InformationManagement: A Proposal》中提出了一个构想:创建一个以超文本系统为基础的项目,允许在不同计算机之间分享信息,其目的是方便研究人员分享及更新信息。伯纳斯·李的提案包含了网络的基本概念并逐步建立了所有必要的工具:
-
提出HTTP (Hypertext Transfer Protocol) 超文本传输协议,允许用户通过单击超链接访问资源;
-
提出使用HTML超文本标记语言(Hypertext Markup Language)作为创建网页的标准;
-
创建了统一资源定位器URL (Uniform Resource Locator)作为网站地址系统,就是沿用至今的
http://www
URL格式; -
创建第一个Web浏览器,称为万维网浏览器,这也是一个Web编辑器;
-
创建第一个HTTP 服务器软件,后来称为CERNhttpd;
-
创建第一个Web服务器(
http://info.cern.ch
)以及描述项目本身的第一个Web页面。
1993年4月30日,欧洲粒子物理研究所(CERN)将万维网软件开源,发布了一个开放式许可证,使得万维网得到最大化的传播。
HTTP的出现主要解决应用层的解析混乱的局面,原始在TCP/IP模型下。每个公司都需要开发一套软件,来帮助客户端和自己服务通讯。这就得让用户安装不同的软件才能使用不同公司的服务和信息。而万维网的出现,使用浏览器就可以查看获取不同公司的信息。
HTTP(Hypertext Transfer Protocol)是一种应用层协议,用于传输超文本文档。它是用于在客户端和服务器之间传输数据的协议,通常在Web浏览器和Web服务器之间进行通信使用text/html
格式。此外还有比如我们自己应用app使用application/json
。
我们应该非常熟悉这个TCP/IP模型和OSI模型。这两个模型是有不同的两个权威组织IEEE和OSI制定的标准,但是TCP/IP模型适应性更广。 HTTP协议属于应用层的协议。 HTTP 是无状态协议,每个请求-响应周期都是相互独立的,服务器不保存客户端之间的状态信息。目前HTTP 使用 TCP 作为其传输层协议,在 80 端口上使用普通的文本形式传输数据,或者在 443 端口上使用加密(HTTPS)传输数据。
客户端发送一个 HTTP 请求到服务器,请求由请求行、请求头、空行和请求体组成,比如请求行中GET
表示从服务器获取数据,POST
表示向服务器发送数据。服务器接收并解析请求,然后发送回一个 HTTP 响应,响应由状态行、响应头、空行和响应体组成。每个HTTP响应包含一个状态码,表示请求的状态,常见的状态码包括 200(成功)、404(未找到)、500(服务器内部错误)等。
HTTP/1.0 是第一个被广泛采用的版本,后来被 HTTP/1.1 取代。HTTP/1.1比 HTTP/1.0增加了持久连接、管线化、分块传输编码等功能,提高了性能和效率。比如声明Connection:keep-alive
建立持久连接,但是虽然可以复用TCP,但是一个TCP连接的请求还得按顺序一个一个处理, HTTP/2的出现解决这个问题, HTTP/2其实不是官方发布的,是Google发布最先在自己的浏览器使用,倒闭市场都开始适配。HTTP/2引入了多路复用、Header压缩、服务器推送等功能,复用TCP连接同时处理多个请求。加快了页面加载速度,提升了性能。我们知道TCP为了可靠性建立连接必须有3次握手。对于请求响应肯定有延迟。所以出现了HTTP/3,HTTP/3 是基于QUIC协议,使用UDP然后自己做连接管理和流量控制以及可靠性,减少了延迟,特别是对移动网络连接更为友好。但是目前官方还未正式发布的HTTP/3。
URL和URI的区别
- URL (Uniform Resource Locator) 是用于标识互联网资源位置的字符串。它包含了资源的协议(如HTTP或HTTPS)、主机名、端口号、资源路径以及可能的查询参数。URL是一个具体的地址,可以直接指向网络上的特定资源。
- URI (Uniform Resource Identifier) 是用于标识资源的字符串,可以表示任何资源的唯一标识符。URI是一个抽象的概念,而URL是其中的一种形式。URL是URI的子集,包括所有的URL也是URI。URI还可以包括URN(Uniform Resource Name),用于对资源进行唯一命名,但不提供资源的位置信息。
因此,可以说URL是URI的一个特例,是一种特定形式的统一资源标识符,用于定位资源,并且包含了网络传输所需要的位置信息。
二、HTTPS协议的工作原理
基于的HTTP协议,客户端向服务端发送一个请求,服务端响应的过程如图:
这过程中http中的数据都是明文的,类似我们发一个快递,这个包裹上贴着的裸奔信息,发件人姓名,地址,电话这些信息被窃取,就会出现各种网络诈骗😭。后面快递公司有信息安全意识,在单号上都对信息打码或**显示。同样的在巨大万维网中,通讯过程数据裸奔很容易截取,相当的不安全。比如之前出现的运营商劫持事件,于是有了HTTTPS的出现。
如图是HTTPS的协议栈,使用SSL/TLS协议进行加密传输,提供数据的机密性和完整性。
那么SSL/TLS协议主要做的就是对于数据客户端和服务端怎么约定加密和解密。
对称加密
对称加密是指只使用一个密钥,可以使得明文加密为密文,密文解密为明文。 但是这种方式是难点在于对于密钥的管理,让服务端管理并不简单,毕竟一个服务端要面对成千上万的客户端,所以好的办法是可以让客户端自己生成一个密钥,建立tcp连接之后,告诉服务端双方将使用的密钥。然后服务端应答以后客户端使用密钥加密请求,服务端使用这个密钥解密。以此密文通讯。
但是这种方式也容易被劫持。中间人在劫持密文的同时,也能接活密钥。所以需要对密钥进行加密。
非对称加密 非对称加密额外会生成两个密钥,一个是"公钥",一个是"私钥"。他们是配对的。有服务器生成。服务器将公钥发送给客户端,私钥自己保留,客户端收到公钥以后,使用公钥加密自己的随机生成的密钥,发给服务端,服务端收到以后使用私钥解密客户端的密钥,然后使用这个密钥加密消息告诉客户端已收到。然后后面就可以以此密钥通讯了。这种方式对称加密安全性上一个级别,但是还是可以被中间人劫持。
假如中间人黑客了你的路由器,你请求将会被中间人代理。他可以生成自己的"公钥"和"私钥"。当客户端发送请求,中间人返回它的"私钥",客户端使用这个私钥加密自己的密钥,中间人劫持以后使用自己的"公钥"解密密钥,后面中间人继续向服务端发起请求,获取服务端的私钥,然后再将截获的客户端密钥使用服务端的密钥加密,继续和服务端通讯。中间人完美隐形。你和客户端所以通信信息(账号,密码),他都知道。
证书机制
对于这种伪造"公钥"的安全漏洞,为了保证"公钥"的合法性,出现了证书机制。有权威机构颁发CA证书,这个证书包含服务器的公钥和其他信息。客户端获取到服务端的证书,首先会验证证书的合法性。如非法,拒绝再次发起请求。这种方式比非对称加密安全性又上了一个级别,毕竟证书不是那么容易伪造的。
通过证书验证,保证通信双方的身份认证,提高安全性。缺点TLS协议中的4次握手,还有需要加密和解密过程,通信速度可能略慢一些。如图所示,建立连接需要3次TCP握手和4次TLS握手。
SSL/TLS协议有好多版本,图中的4次握手是tls 1.2版本,tls 1.3版本可以做到2次,我就不在此赘述了,小伙伴本可以去看小林codeing 博客,对于https讲解的非常详细。
但是即使有证书机制机制,即客户端验证服务端的证书。依然可以被中间人劫持。只是代价高一点,自己必须要有一个合法的证书,比如:
比如有一个假的通讯基站,或者这个基站被黑了,设置了一个中间人服务器,客户端向服务端发送请求,中间人服务器拦截返回他的合法证书,客户端使用中间人的证书中的公钥加密自己的随机秘钥,中间人服务器就可以解密,然后中间人服务器再向服务器发送请求,获取服务器的证书,使用截获客户端请求,再次向服务端发送请求,然后服务端响应返回数据,这个过程数据已经被窃取但是客户端和服务端都是无感的。
那么怎么应对这种劫持呢,有的,有两种方案,一种是SSLPinning ,即固定证书,还有一种需要服务器支持的,就是客户端也持有证书,服务端需要验证客户端的证书,也就是双向验证。
- SSLPinning 是一种增强安全性的技术,用于防止中间人攻击(Man-in-the-Middle attacks)。它通过比较服务器返回的 SSL 证书和应用程序内置的信任证书来验证服务器的身份,确保应用程序连接到预期的服务器。
- 双向验证一般适用于对安全性要求较高的场景,其中需要双方进行身份验证的情况。在银行或支付应用程序中,用户和服务器之间的交互可能需要双向验证,以确保交易的安全性和准确性。
三、Okhttp实现安全传输
纯说理论,可能实际开发中还是一脸懵,我们结合实际,加深记忆,我们Android开发基本是大多都是使用okhttp,我们就以Okhttp为例,来实现一个证书固定和双向验证的例子。我们先理解如下一些类的作用:
-
CertificatePinner
OkHttp
中的CertificatePinner
类是用来实现SSLPinning
的,它可以帮助应用程序限制与特定服务器的SSL/TLS
证书交互。CertificatePinner
允许您定义一组预期的服务器证书公钥或哈希值,以确保应用程序只连接到具有这些证书之一的服务器。 -
X509TrustManager
X509TrustManager
是Android API 中的一个接口,用于管理 X.509 证书的信任。它是用于验证服务器证书是否受信任,并做出相应决定的关键组件之一。X509TrustManager
的主要作用是验证 SSL 服务器证书,以确定连接是否可以被信任或被拒绝。它通常在与服务器建立安全连接时使用,比如进行 HTTPS 通信时,可以用来验证服务器证书链的有效性,并决定是否允许连接。 -
KeyManager
KeyManager
是Java中的一个接口,它是 Java 密钥管理框架中的关键组件之一。KeyManager 接口负责管理密钥库,并选择和配置密钥,以供SSL Context
使用。 -
SSLContext
SSLContext
是 Java 中用于创建和配置 SSL/TLS 安全通信的类。SSLContext
类提供了用于设置 SSL/TLS 参数的方法,以及用于创建SSLSocketFactory
实例的工厂方法。SSLContext
类允许开发人员配置安全套接字协议的算法参数、密钥管理器以及信任管理器,以定制 SSL/TLS 安全连接的行为。 -
SSLSocket
SSLSocket
是一个安全套接字类,用于在客户端和服务器之间建立 SSL/TLS 安全连接。SSLSocket
继承自普通的Socket
类,并在其基础上添加了 SSL/TLS 安全通信功能。通过SSLSocket
,客户端和服务器可以在双方进行数据传输时实现加密、认证和数据完整性验证。 -
SSLSocketFactory
SSLSocketFactory
是用于创建客户端SSLSocket
实例的工厂类。SSLSocketFactory
可以通过SSLContext
创建并配置,以便客户端能够和服务器端建立安全的 SSL/TLS 连接。SSLSocketFactory
帮助客户端创建安全的SSLSocket
对象,用于与服务器进行加密通信。 -
HostnameVerifier
主机名验证器,用于验证与SSL套接字建立连接时服务器的主机名是否与服务器的证书中的主机名匹配,以确保客户端与服务器之间的连接是安全的。默认情况下,
HttpsURLConnection.getDefaultHostnameVerifier()
类使用默认主机名验证器来验证主机名就可以了。
java
public class OkHttpClientExample {
public static void main(String[] args) throws Exception {
//不使用KeyManager就是单向验证。使用时需要服务端支持
KeyManager[] keyManagers = loadKeyManagers();
//服务端证书管理器
X509TrustManager trustManager = loadX509TrustManager();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, new TrustManager[]{trustManager}, new SecureRandom());
CertificatePinner certificatePinner = createCertificatePinner();
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
HostnameVerifier hv =
HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(hostname, session);
}
};
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner);// 预期的服务器证书公钥或哈希值配置(证书固定)
.sslSocketFactory(sslContext.getSocketFactory())//客户端和服务器可以在双方进行数据传输时实现加密、认证和数据完整性验证
.hostnameVerifier(hostnameVerifier);//默认主机名验证器
.build();
Request request = new Request.Builder()
.url("https://example.com")
.build();
Response response = client.newCall(request).execute();
System.out.println(response.body().string());
}
public static CertificatePinner createCertificatePinner(){
//此处举例,实际过程已解析一个加密文件。
List<String> domains = new ArrayList<>(Arrays.asList("your_domain1.com", "your_domain2.com"));
List<String> certificateHashes = new ArrayList<>(Arrays.asList("your_certificate_hash1", "your_certificate_hash2"));
CertificatePinner.Builder certificatePinnerBuilder = new CertificatePinner.Builder();
for (int i = 0; i < domains.size(); i++) {
certificatePinnerBuilder.add(domains.get(i), certificateHashes.get(i));
}
CertificatePinner certificatePinner = certificatePinnerBuilder.build();
return certificatePinner;
}
public static X509TrustManager loadX509TrustManager( ) {
try {
// 加载证书文件
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
FileInputStream fis = new FileInputStream("xxx.crt");
Certificate cert = certFactory.generateCertificate(fis);
fis.close();
// 创建 KeyStore 并将证书载入
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("XXCertificate", cert);
// 创建 TrustManagerFactory 并初始化 KeyStore
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
// 获取 TrustManager 数组
TrustManager[] trustManagers = tmf.getTrustManagers();
for (TrustManager trustManager : trustManagers) {
if (trustManager instanceof X509TrustManager) {
return (X509TrustManager) trustManager;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static KeyManager[] loadKeyManagers(){
try {
//假设客户端证书为client.jks
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(OkHttpClientExample.class.getResourceAsStream("client.jks"), "password".toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "password".toCharArray());
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
return keyManagers
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
文章中的客户端固定证书,只是示例,一般商业级app中会加密,或者打包成so文件到jni层去做客户端的证书校验,此外证书还有更新机制,当发现客户端的证书过期,应当可以去服务端更新。
五、总结
传统http通讯都是裸奔,数据随随便便就可以截获,为了实现信息的安全传输,在TCP上面加了一层SSL/TSL协议也就是HTTPS,通讯过程数据加密,避免数据被截获,保证安全,加密过程从对称加密,到非对称加密,证书校验机制安全级别在逐步加大,但是即使是证书校验机制也还是不能保证安全,还是会被恶意的服务器中间人窃取数据。
再来回顾开头的问题:HTTPS是安全的吗?有啥方案可以避免网络劫持?
答 :不是,虽然证书校验机制,客户端会校验服务端的证书合法性,但是避免不了一个也有合法证书的中间人劫持。客户端向服务端发送请求,中间人服务器拦截返回他的合法证书,客户端使用中间人的证书中的公钥加密自己的随机秘钥,中间人服务器就可以解密,然后中间人服务器再向服务器发送请求,获取服务器的证书,使用截获客户端请求,再次向服务端发送请求,然后服务端响应返回数据,这个过程客户端和服务端通信的数据完全透明,客户端和服务端都是无感的。类似Fidder的抓包原理,手机主动设置代理的情况下,Fidder就可以成功抓包。那么怎么应对这种劫持呢,有的,有两种方案,一种是SSLPinning 固定证书方案,还有一种需要服务器支持的,就是客户端也持有证书,服务端需要验证客户端的证书,也就是双向验证 。目前使用较多的是SSLPinning,即客户端固定证书方案。