博客记录-day056-面向对象+HTTPS 优化

75 阅读15分钟

一、Java全栈知识体系-Java基础

1、面向对象

1.1 三大特性

1.1.1 封装

利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。

优点:

  • 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改
  • 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
  • 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能
  • 提高软件的可重用性
  • 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的

以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。

注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。

public class Person {

    private String name;
    private int gender;
    private int age;

    public String getName() {
        return name;
    }

    public String getGender() {
        return gender == 0 ? "man" : "woman";
    }

    public void work() {
        if (18 <= age && age <= 50) {
            System.out.println(name + " is working very hard!");
        } else {
            System.out.println(name + " can't work any more!");
        }
    }
}
1.1.2 继承

继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。

继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。

Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型

Animal animal = new Cat();
1.1.3 多态

多态分为编译时多态和运行时多态:

  • 编译时多态主要指方法的重载
  • 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定

运行时多态有三个条件:

  • 继承
  • 覆盖(重写)
  • 向上转型

下面的代码中,乐器类(Instrument)有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。

public class Instrument {
    public void play() {
        System.out.println("Instrument is playing...");
    }
}

public class Wind extends Instrument {
    public void play() {
        System.out.println("Wind is playing...");
    }
}

public class Percussion extends Instrument {
    public void play() {
        System.out.println("Percussion is playing...");
    }
}

public class Music {
    public static void main(String[] args) {
        List<Instrument> instruments = new ArrayList<>();
        instruments.add(new Wind());
        instruments.add(new Percussion());
        for(Instrument instrument : instruments) {
            instrument.play();
        }
    }
}

1.2 类图

1.2.1 泛化关系 (Generalization)

用来描述继承关系,在 Java 中使用 extends 关键字

image.png

1.2.2 实现关系 (Realization)

用来实现一个接口,在 Java 中使用 implements 关键字

image.png

1.2.3 聚合关系 (Aggregation)

表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在

image.png

1.2.4 组合关系 (Composition)

和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。

image.png

1.2.5 关联关系 (Association)

表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。

image.png

1.2.6 依赖关系 (Dependency)

和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:

  • A 类是 B 类中的(某中方法的)局部变量;
  • A 类是 B 类方法当中的一个参数;
  • A 类向 B 类发送消息,从而影响 B 类发生变化;

image.png

二、小林-图解网络-HTTPS 如何优化

由裸数据传输的 HTTP 协议转成加密数据传输的 HTTPS 协议,给应用数据套了个「保护伞」,提高安全性的同时也带来了性能消耗。

因为 HTTPS 相比 HTTP 协议多一个 TLS 协议握手过程,目的是为了通过非对称加密握手协商或者交换出对称加密密钥,这个过程最长可以花费掉 2 RTT,接着后续传输的应用数据都得使用对称加密密钥来加密/解密。

为了数据的安全性,我们不得不使用 HTTPS 协议,至今大部分网址都已从 HTTP 迁移至 HTTPS 协议,因此针对 HTTPS 的优化是非常重要的。

这次,就从多个角度来优化 HTTPS。

1、分析性能损耗

既然要对 HTTPS 优化,那得清楚哪些步骤会产生性能消耗,再对症下药。

产生性能消耗的两个环节:

  • 第一个环节, TLS 协议握手过程
  • 第二个环节,握手后的对称加密报文传输

对于第二环节,现在主流的对称加密算法 AES、ChaCha20 性能都是不错的,而且一些 CPU 厂商还针对它们做了硬件级别的优化,因此这个环节的性能消耗可以说非常地小。

而第一个环节,TLS 协议握手过程不仅增加了网络延时(最长可以花费掉 2 RTT),而且握手过程中的一些步骤也会产生性能损耗,比如:

  • 对于 ECDHE 密钥协商算法,握手过程中会客户端和服务端都需要临时生成椭圆曲线公私钥;
  • 客户端验证证书时,会访问 CA 获取 CRL 或者 OCSP,目的是验证服务器的证书是否有被吊销;
  • 双方计算 Pre-Master,也就是对称加密密钥;

为了更清楚这些步骤在 TLS 协议握手的哪一个阶段,这幅图:

2、协议优化

协议的优化就是对「密钥交换过程」进行优化。

2.1 密钥交换算法优化

TLS 1.2 版本如果使用的是 RSA 密钥交换算法,那么需要 4 次握手,也就是要花费 2 RTT,才可以进行应用数据的传输,而且 RSA 密钥交换算法不具备前向安全性。

总之使用 RSA 密钥交换算法的 TLS 握手过程,不仅慢,而且安全性也不高

因此如果可以,尽量选用 ECDHE 密钥交换算法替换 RSA 算法,因为该算法由于支持「False Start」,它是“抢跑”的意思,客户端可以在 TLS 协议的第 3 次握手后,第 4 次握手前,发送加密的应用数据,以此将 TLS 握手的消息往返由 2 RTT 减少到 1 RTT,而且安全性也高,具备前向安全性

ECDHE 算法是基于椭圆曲线实现的,不同的椭圆曲线性能也不同,应该尽量选择 x25519 曲线,该曲线是目前最快的椭圆曲线。

比如在 Nginx 上,可以使用 ssl_ecdh_curve 指令配置想使用的椭圆曲线,把优先使用的放在前面:

对于对称加密算法方面,如果对安全性不是特别高的要求,可以选用 AES_128_GCM,它比 AES_256_GCM 快一些,因为密钥的长度短一些。

比如在 Nginx 上,可以使用 ssl_ciphers 指令配置想使用的非对称加密算法和对称加密算法,也就是密钥套件,而且把性能最快最安全的算法放在最前面:

2.2 TLS 升级

当然,如果可以,直接把 TLS 1.2 升级成 TLS 1.3,TLS 1.3 大幅度简化了握手的步骤,完成 TLS 握手只要 1 RTT,而且安全性更高。

在 TLS 1.2 的握手中,一般是需要 4 次握手,先要通过 Client Hello (第 1 次握手)和 Server Hello(第 2 次握手) 消息协商出后续使用的加密算法,再互相交换公钥(第 3 和 第 4 次握手),然后计算出最终的会话密钥,下图的左边部分就是 TLS 1.2 的握手过程:

上图的右边部分就是 TLS 1.3 的握手过程,可以发现 TLS 1.3 把 Hello 和公钥交换这两个消息合并成了一个消息,于是这样就减少到只需 1 RTT 就能完成 TLS 握手

怎么合并的呢?具体的做法是,客户端在 Client Hello 消息里带上了支持的椭圆曲线,以及这些椭圆曲线对应的公钥。

服务端收到后,选定一个椭圆曲线等参数,然后返回消息时,带上服务端这边的公钥。经过这 1 个 RTT,双方手上已经有生成会话密钥的材料了,于是客户端计算出会话密钥,就可以进行应用数据的加密传输了。

而且,TLS1.3 对密码套件进行“减肥”了, 对于密钥交换算法,废除了不支持前向安全性的 RSA 和 DH 算法,只支持 ECDHE 算法

之所以 TLS1.3 仅支持这么少的密码套件,是因为 TLS1.2 由于支持各种古老且不安全的密码套件,中间人可以利用降级攻击,伪造客户端的 Client Hello 消息,替换客户端支持的密码套件为一些不安全的密码套件,使得服务器被迫使用这个密码套件进行 HTTPS 连接,从而破解密文。

3、证书优化

为了验证的服务器的身份,服务器会在 TLS 握手过程中,把自己的证书发给客户端,以此证明自己身份是可信的。

对于证书的优化,可以有两个方向:

  • 一个是证书传输
  • 一个是证书验证

3.1 证书传输优化

要让证书更便于传输,那必然是减少证书的大小,这样可以节约带宽,也能减少客户端的运算量。所以,对于服务器的证书应该选择椭圆曲线(ECDSA)证书,而不是 RSA 证书,因为在相同安全强度下, ECC 密钥长度比 RSA 短的多

3.2 证书验证优化

客户端在验证证书时,是个复杂的过程,会走证书链逐级验证,验证的过程不仅需要「用 CA 公钥解密证书」以及「用签名算法验证证书的完整性」,而且为了知道证书是否被 CA 吊销,客户端有时还会再去访问 CA, 下载 CRL 或者 OCSP 数据,以此确认证书的有效性。

这个访问过程是 HTTP 访问,因此又会产生一系列网络通信的开销,如 DNS 查询、建立连接、收发数据等。

3.2.1 CRL

CRL 称为证书吊销列表(Certificate Revocation List),这个列表是由 CA 定期更新,列表内容都是被撤销信任的证书序号,如果服务器的证书在此列表,就认为证书已经失效,不在的话,则认为证书是有效的。

但是 CRL 存在两个问题:

  • 第一个问题,由于 CRL 列表是由 CA 维护的,定期更新,如果一个证书刚被吊销后,客户端在更新 CRL 之前还是会信任这个证书,实时性较差
  • 第二个问题,随着吊销证书的增多,列表会越来越大,下载的速度就会越慢,下载完客户端还得遍历这么大的列表,那么就会导致客户端在校验证书这一环节的延时很大,进而拖慢了 HTTPS 连接。
3.2.2 OCSP

因此,现在基本都是使用 OCSP ,名为在线证书状态协议(Online Certificate Status Protocol)来查询证书的有效性,它的工作方式是向 CA 发送查询请求,让 CA 返回证书的有效状态

不必像 CRL 方式客户端需要下载大大的列表,还要从列表查询,同时因为可以实时查询每一张证书的有效性,解决了 CRL 的实时性问题。

OCSP 需要向 CA 查询,因此也是要发生网络请求,而且还得看 CA 服务器的“脸色”,如果网络状态不好,或者 CA 服务器繁忙,也会导致客户端在校验证书这一环节的延时变大。

3.2.3 OCSP Stapling

于是为了解决这一个网络开销,就出现了 OCSP Stapling,其原理是:服务器向 CA 周期性地查询证书状态,获得一个带有时间戳和签名的响应结果并缓存它。

当有客户端发起连接请求时,服务器会把这个「响应结果」在 TLS 握手过程中发给客户端。由于有签名的存在,服务器无法篡改,因此客户端就能得知证书是否已被吊销了,这样客户端就不需要再去查询。

4、会话复用

TLS 握手的目的就是为了协商出会话密钥,也就是对称加密密钥,那我们如果我们把首次 TLS 握手协商的对称加密密钥缓存起来,待下次需要建立 HTTPS 连接时,直接「复用」这个密钥,不就减少 TLS 握手的性能损耗了吗?

这种方式就是会话复用TLS session resumption),会话复用分两种:

  • 第一种叫 Session ID;
  • 第二种叫 Session Ticket;

4.1 Session ID

Session ID 的工作原理是,客户端和服务器首次 TLS 握手连接后,双方会在内存缓存会话密钥,并用唯一的 Session ID 来标识,Session ID 和会话密钥相当于 key-value 的关系。

当客户端再次连接时,hello 消息里会带上 Session ID,服务器收到后就会从内存找,如果找到就直接用该会话密钥恢复会话状态,跳过其余的过程,只用一个消息往返就可以建立安全通信。当然为了安全性,内存中的会话密钥会定期失效。

但是它有两个缺点:

  • 服务器必须保持每一个客户端的会话密钥,随着客户端的增多,服务器的内存压力也会越大
  • 现在网站服务一般是由多台服务器通过负载均衡提供服务的,客户端再次连接不一定会命中上次访问过的服务器,于是还要走完整的 TLS 握手过程;

4.2 Session Ticket

为了解决 Session ID 的问题,就出现了 Session Ticket,服务器不再缓存每个客户端的会话密钥,而是把缓存的工作交给了客户端,类似于 HTTP 的 Cookie。

客户端与服务器首次建立连接时,服务器会加密「会话密钥」作为 Ticket 发给客户端,交给客户端缓存该 Ticket。

客户端再次连接服务器时,客户端会发送 Ticket,服务器解密后就可以获取上一次的会话密钥,然后验证有效期,如果没问题,就可以恢复会话了,开始加密通信。

对于集群服务器的话,要确保每台服务器加密 「会话密钥」的密钥是一致的,这样客户端携带 Ticket 访问任意一台服务器时,都能恢复会话。

Session ID 和 Session Ticket 都不具备前向安全性,因为一旦加密「会话密钥」的密钥被破解或者服务器泄漏「会话密钥」,前面劫持的通信密文都会被破解。

同时应对重放攻击也很困难,这里简单介绍下重放攻击工作的原理。

假设 Alice 想向 Bob 证明自己的身份。 Bob 要求 Alice 的密码作为身份证明,爱丽丝应尽全力提供(可能是在经过如哈希函数的转换之后)。与此同时,Eve 窃听了对话并保留了密码(或哈希)。

交换结束后,Eve(冒充 Alice )连接到 Bob。当被要求提供身份证明时,Eve 发送从 Bob 接受的最后一个会话中读取的 Alice 的密码(或哈希),从而授予 Eve 访问权限。

重放攻击的危险之处在于,如果中间人截获了某个客户端的 Session ID 或 Session Ticket 以及 POST 报文,而一般 POST 请求会改变数据库的数据,中间人就可以利用此截获的报文,不断向服务器发送该报文,这样就会导致数据库的数据被中间人改变了,而客户是不知情的。

避免重放攻击的方式就是需要对会话密钥设定一个合理的过期时间

4.3 Pre-shared Key

前面的 Session ID 和 Session Ticket 方式都需要在 1 RTT 才能恢复会话。

而 TLS1.3 更为牛逼,对于重连 TLS1.3 只需要 0 RTT,原理和 Ticket 类似,只不过在重连时,客户端会把 Ticket 和 HTTP 请求一同发送给服务端,这种方式叫 Pre-shared Key

同样的,Pre-shared Key 也有重放攻击的危险。

如上图,假设中间人通过某种方式,截获了客户端使用会话重用技术的 POST 请求,通常 POST 请求是会改变数据库的数据,然后中间人就可以把截获的这个报文发送给服务器,服务器收到后,也认为是合法的,于是就恢复会话,致使数据库的数据又被更改,但是此时用户是不知情的。

所以,应对重放攻击可以给会话密钥设定一个合理的过期时间,以及只针对安全的 HTTP 请求如 GET/HEAD 使用会话重用。