diff --git a/SUMMARY.md b/SUMMARY.md index 330c4fa..fd35c33 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,8 +1,16 @@ # 刘青的技术博客 [我的个人博客](README.md) +## 架构设计 +- [构建微服务](./architecture_design/building_microservices.md) +- [设计模式](./architecture_design/design_patterns.md) + ## 计算机科学 -- [架构设计]() - - [构建微服务](./computer_science/architecture_design/building_microservices.md) - - [设计模式](./computer_science/architecture_design/design_patterns.md) - [分布式简述](./computer_science/distributed.md) + +## 计算机网络 +- [tls](./network/tls/1.overview.md) + - [安全问题指的是什么](./network/tls/2.secure_issue.md) + - [tls的基本流程](./network/tls/3.basic_processing.md) + - [tls1.3](./network/tls/4.tls1.3.md) + - [tls实践检验](./network/tls/5.practices.md) \ No newline at end of file diff --git a/architecture_design/building_microservices.md b/architecture_design/building_microservices.md new file mode 100755 index 0000000..3b11117 --- /dev/null +++ b/architecture_design/building_microservices.md @@ -0,0 +1,101 @@ + + +### 微服务 +> 微服务:一些协同工作的小而自治的服务。 + +- 很小:服务专注某个边界内的业务。越小,独立性的好处就越大,同时管理大量服务也会越复杂。 +- 自治性: + - 一个微服务就是一个独立的实体,可以独立地部署在PAAS上,也可作为一个操作系统进程存在。 + - 服务之间均通过网络调用进行通信,解耦。 + +#### 微服务的好处 +- 技术异构性:因为每个服务都通过网络通信,良好的API设计就可以满足业务需求,每个服务内部具体实现可以迥然不同; +- 弹性:单服务的系统可以通过在不同机器上部署多个实例来减少功能的完全不用概率,而微服务系统本身就能处理好服务不可用和功能降级问题。 +- 扩展:单服务只能作为整体进行扩展,多个微服务则只需要对需要扩展的服务进行扩展。 +- 简化部署:整体部署,局部部署。 +- 与组织结构想匹配 +- 可组合性 +- 可替代性的优化 + + +#### 面向服务的架构 +SOA(Service-Oriented Architecture): +- 包含多个服务 +- 服务组合最终提供一系列功能 +- 一个服务一独立的形式存在于操作系统进程中 +- 服务之间通过网络调用 + +它在实施中遇到的问题: +- 通信协议的选择 +- 第三方中间件的选择 +- 服务的粒度的确定 + +其实可以认为微服务是SOA的一种特定方法。 + +#### 其他分解技术 +- 共享库:所有的语言都支持共享库,这个是代码层面上的复用。但是无法选择异构技术,非动态链接库部署也不方便; +- 模块:有的语言提供了模块分解技术,运行模块在不停止整个进程的情况下进行局部替换。但是强调模块生命周期,非常复杂; + + +### 没有银弹 +微服务不是免费的午餐,更不是银弹。如果想得到一条通用准则,微服务是错误的选择。一个最大的挑战是需要面对所有分布式系统的复杂性。 + + +-------------------------------- + +### 演化式架构师 +与建造建筑物相比,在软件中我们会面临大量的需求变更,使用的工具和技术也具有多样性。绝大多数情况下软件交付后还需要响应用户的变更需求。 + +> 架构师必须改变从一开始就要设计出完美产品的想法,相反,我们应该设计出一个合理的框架,在这个框架下可以慢慢演化出正确的系统。 + +作为一个架构师,不应该过多的观众区域内发生的事情,而应该多关注区域总监的事情,或者时保证我们能对整个系统的健康状态进行监控。 + + +### 如何搭建服务 +什么服务是好的服务: +- 松耦合:服务之间松耦合,修改一个服务不需要修改另一个服务; +- 搞内聚:将相关的细纹聚集在一起,当需要修改某个行为,只需要修改一个地方; + +> 限界上下文:一个由显式边界限定的特定职责。 + +限界上下文有明确的对外接口,也有明确的对外接口。 +思考组织内的限界上下文时,不应该从共享数据的角度来考虑,而应该从这些上下文能够听过的功能来考虑。 + +### 集成 +> 集成是微服务相关技术中最重要的一个。 + +理想的集成技术应该: +- 避免破坏性修改:消费方不随着生产方的修改而修改; +- 保证API的技术无关性:微服务之间的通信方式的技术无关性很重要; +- 服务易于消费方使用; +- 隐藏内部实现细节; + + +#### 服务写作的方式:同步与异步 + + +### 分解单块系统 + +### 部署 + +### 测试 + +### 监控 + +### 安全 + +### 康威定律和系统设计 + +### 规模化微服务 + +### 总结 + diff --git a/architecture_design/db_middleware_with_biz_logic.md b/architecture_design/db_middleware_with_biz_logic.md new file mode 100644 index 0000000..8d38afb --- /dev/null +++ b/architecture_design/db_middleware_with_biz_logic.md @@ -0,0 +1,305 @@ + + +最近我们团队要做一个数据库中间件访问系统,因为现在业务线中已经开始存在数据库共享的现象。 + + +### 需求描述 +大概的功能需求是: +- 所有的数据库访问都来访问该系统 +- 该系统负责和底层的各个数据库表打交道 +- 系统要有字段映射能力:不同的接口访问不同的字段其实可能指向同一个数据库字段 +- 系统要有简单的鉴权,入参校验的能力:同一个数据库字段不同的接口的参数检查逻辑可能不一样 +- 可能后续会对某些数据加缓存 +- 新增修改不提供同时操作的能力,但是查询后期要支持跨库|表的关联查询 +- 系统暂时不通过聚合数据查询(sum|agv等函数的运算结果),当然,如果这些数据被缓存在统计表中,也可以直接CURD + +举个例子: +现在对于A使用方; +``` +API: /db_middleware/address/a_query?id=1 +Params: + { + fields: ["id", "user_id", "status", "address"] + } + +API: /db_middleware/address/a_create +Params: + { + "user_id": 123, + "address": "some where", + "status": 1 + } +``` + +现在对于B使用方; +``` +API: /db_middleware/address/b_query?id=1 +Params: + { + fields: ["id", "user_id", "user_address"] + } +只返回status=1 的记录,且 address 的key 是user_address + +API: /db_middleware/address/b_create +Params: + { + "user_id": 123, + "address": "some where", + } +新增的所有的记录的status 都是默认值0 表示待审核。 +``` + + + +非功能需求包括: +- 系统的高可用性:作为底层系统,挂了会影响整个业务线的业务 +- 系统的高扩展性:新增接口要尽可能代价小,尽可能没有编码就可以实现,实现使用方的快速低成本接入(可配置性) +- 日志记录:追查操作历史 + +### 思路 +最常规的思路其实和传统的web API开发没有什么差别,对应一张表,提供CRUD四个接口,在代码分层上也可以是: + +``` +|-----------------------| +| controller | 参数校验 +|-----------------------| + | + ↓ +|-----------------------| +| model | 数据整合 +|-----------------------| + | + ↓ +|-----------------------| +| dao | cache | 数据访问 +|-----------------------| +``` + +代码结构可能是: +``` +├── cache +├── controller +│   ├── address +│   │   ├── create.go +│   │   ├── list.go +│   │   ├── one.go +│   │   └── udpate.go +│   └── user +│   ├── create.go +│   ├── list.go +│   ├── one.go +│   └── udpate.go +├── dao +│   ├── address +│   │   ├── create.go +│   │   ├── list.go +│   │   ├── one.go +│   │   └── udpate.go +│   └── user +│   ├── create.go +│   ├── list.go +│   ├── one.go +│   └── udpate.go +├── model +│   ├── address +│   │   ├── create.go +│   │   ├── list.go +│   │   ├── one.go +│   │   └── udpate.go +│   └── user +│   ├── create.go +│   ├── list.go +│   ├── one.go +│   └── udpate.go +``` + +这个不需要设计的设计有以下缺陷: +- 无法快速开发,和业务耦合太紧,也对使用方有较强的限制 +- 重复代码太多,作为数据库访问中间件,每张表的查询其实都是CURD,代码逻辑基本一致 + +> 解决问题的能力取决于抽象问题的能力。 + +我们其实可以把整个系统的功能进行抽象: +- 参数校验:不同的接口的key的枚举值是不一样的,不同接口对于同一个key的value的枚举值可能是不一样的 +- 可能的cache访问:同一个接口的cache策略基本上是一样的 +- 数据库访问:根据参数进行访问数据库的sql其实基本上是一样的 +- 数据格式化返回:这个和参数校验相关 + +如果我们把每个环节的都做出和接口相关的抽象实现,具体的逻辑放到配置文件中,新增功能其实就成了新增配置。 + +### 可配置化的编码逻辑 +可配置化编程这个概念很美好,但是真正做起来难度其实还是挺高的。 + +#### 数据库表的抽象 + +我们先从数据库来抽象,看其如何可配置,数据库表的抽象其实最完整的是DDL。但是我们基于业务去做抽象。 + +假设我们有 address 表: + +``` +id int 自增ID +user_id string +address varchar(1024) +status int8 +``` + +我们用数据库对其进行描述: +``` +address_table.json +{ + "parttition_key": "user_id", // 分表字段为 user_id + "partition_strategy": "crc32_1024", // crc32 后 1024张表 + "auto_key": "id", + "fields" { + "id": { + "name": "id", + "type": "int" + }, + "user_id" : { + "name": "user_id", + "type": "string", + "min_len": 1, // 和业务相关,不能为空 + "max_len": 50 + }, + "address": { + "name": "address", + "type": "string", + "min_len": 1, + "max_len": 1024 // 和业务相关的配置 + }, + "status": { + "name": "status", + "type": "int8", + "enums": [0, 1, 2, 3] // 和业务相关的描述 + } + } +} +``` + +当前的描述基本够用了: +- 如果分表,用 parrtition_* 指明 +- 如果有自增字段,使用auto_key 指定,数据插入时需要使用 +- 每张表会有一系列的字段(当前不考虑跨表查询),每个字段有 + - 名字和类型 + - 和业务相关的校验,比如可能的值,范围等 + +数据库是固定的,是API的基石: +- API 的所有的接口对应的都是底层数据库的0-N 张表 +- API的所有的接口的字段对应的都是表的0-N个字段 + +#### 接口配置 +为什么先从数据库抽象能?因为API是和用户相关的,该变动其实和上层业务相关,无法穷尽但可抽象。 + +为了减少配置,我们要求所有的参数通过json传递,content-type = application/json + + +先说明一下每个请求配置的字段: +- type指定这个接口的类型,当前包括:one, list, update, del, create。 +- conditions指定各个字段的过滤条件,当前只支持一维的 and 查询 + - source 指定当前字段的值的来源,默认为input, 有:{fixed: 固定值, input:入参} + - validtor 指定对这个参数的校验逻辑,默认为none,有: {none: 不做校验, not_empty: 不为空...} + - modifier 指定对入参的格式化方式,默认为 none + - db_field 指定数据库的字段,不填写表示和数据库一样 +- fields 是返回参数列表,key是入参的key,val对应数据库的内容, 置空时表示和数据库一样 + - appear 指定是否在返回中出,默认为true + - modifier 指定对入参的格式化方式,默认为 none + - db_field 指定数据库的字段,不填写表示和数据库一样 + + +我们使用上面的实例进行说明: + +``` +address/a_query.json +{ + "type": "one", + "conditions": { + "id": { + "source": "input", + "validtor": "not_empty", + }, + "uid": { + "source": "input", + "validtor": "not_empty", + "db_field": "user_id" + } + }, + "fields": { + "uid": { + "db_field": "user_id" + "modifier": "none" + } + } +} + +对于请求: +{ + "conditions": { + id: 1, + uid: "1234" + }, + fields: [id, address, status] +} +系统先根据接口配置进行参数校验和数据转换 +再根据表配置进行参数校验(业务校验要不比数据库校验轻松,但是程序员可能偷懒) +生成sql: SELECT id, address, status FROM crc(user_id) % 1024 WHERE id = 1 AND user_id = "1234" LIMIT 1 +根据接口配置修饰数据库的返回数据,进行返回 + + +address/b_query.json +{ + "type": "one", + "conditions": { + "id": { + "source": "input", + "validtor": "not_empty", + }, + "user_id": { + "source": "input", + "validtor": "not_empty", + }, + "status": { + "source": "fixed", + "value": 1 + } + }, + "fields": { + "uid": { + "db_field": "user_id" + "modifier": "none" + }, + "status": { + "appear": false + } + } +} +步骤基本上差不多,但是B不能查看 status 字段,而且它是用uid来表示user_id的。 +``` + +CRUD 一共会有五种接口,全系统一共会有五种接口。 + +还有的是 列表查询的 分页等问题,不进一步阐释了。 + +### 实现语言 + +> 用简单问题解决复杂问题是一种很重要的能力。 + +这篇文档讲述的是设计思路,和语言本身无关。 +另外,用最简单的数据结构好像就可以把它实现出来,而不用动不动反射啊,标注啊。 + +### 接口协议本身 +我们现在用的接口协议其实很简单,以至于有点土。它能解决我们绝大多数情况下的需求,但是如果涉及到跨表查询什么的,其实写起来也不太方便(其实任何形式都不太发表)。我们可以参考es的查询语言。 + +这这里抛砖引玉,Facebook开源的一个叫 `GrophQL` 的 API查询语言,它的核心思想是: + +> 一切皆图。 + +有需求的同学可以去看看: http://graphql.cn/ diff --git a/architecture_design/design_patterns.md b/architecture_design/design_patterns.md new file mode 100644 index 0000000..34b7cad --- /dev/null +++ b/architecture_design/design_patterns.md @@ -0,0 +1,62 @@ + + + +# 基本的设计原则 +设计模式其实有无数个,而不是23个。但是它们的源于七个基本的设计原则。 +1. Single Responsibility Principle:不要存在多于一个的导致类变更的原因 + - 变更是必然的,但是改动应该尽可能少的地方,降低对其他组件的影响(高内聚低耦合) +2. Open/Close Principle:软件实体(如模块、组件、类、方法等)应该是可扩展(对扩展开放)而不可修改(对修改关闭) + - 通过扩展软件实体来应对变化,满足新需求,而不是修改旧的代码 +3. Liskov Substitution Principle:子类对象能够替换程序中任何地方出现的父类对象 + - 就是子类对象不要覆写父类的方法,保持子类和父类的行为一致,降低维护成本 +4. Interface Segregation Principle:客户端不应该依赖它不需要的接口 + - 接口要尽可能小 +5. Dependency Inversion:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖具体实现,具体实现应该依赖抽象 + - 细节是多变的,而抽象梗稳定。当细节变化发生时,调用方应该尽可能不要动 +6. The Least Knowledge Principle(The Law of Demeter):一个对象应该对其他对象保持最小的了解 + - 调用方应该尽可能小范围的去访问被调用方的成员,以减少依赖 +7. Composite/Aggregation Reuse Principle:尽量使用组合/聚合 而不是 继承来达到复用目的 + - 继承更不好理解,也暴露了更多的细节 + +前面五个简称 SOLID,由 Martin在论文《设计原则和设计模式》中提出来的。后面两个是其他人提的。 +3和7是从可维护的角度考虑,其他则是从变更发生时尽可能减少修改的角度考虑。 + + +# 设计模式概述 +什么是设计模式:每个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样就能一次又一次的使用该方案而不必做重复劳动。 + +设计模式是面向对象软件设计的经验,使用UML描述。 + +设计模式的基本要素: +- 名字:标识; +- 问题:使用该模式的处境; +- 方案:描述模式的各个部分之间的职责和协助方式; +- 效果:模式应用的效果及使用模式应权衡的问题 + +模式的分类:有很多种分类类型。 + +按照目的分有: +- 创建型:和对象的创建有关; +- 结构型:处理类|对象间的关系; +- 行为型:描述对类|对象交互方式和和职责分配 + +按照范围有: +- 类:通过继承建立关系,静态的; +- 对象:处理对象的关系,动态的 + +我们说常用的设计模式有23个,分类如下: +|范围||目的|| +|-|-|-|-| +| |创建型|结构型|行为型| +|类|Factory Method|Adapter(类)|Interpreter
Template Method| +|对象|Abstract Factory
Builder
Prototype
Singleton|Adapter(对象)
Bridge
Composite
Decorator
Facade
Flyweight
Proxy|Chain of Responsibility
Command
Iterator
Mediator
Memento
Observers
State
Strategy
Visitor| + +## TODO pattern basic elements one by one \ No newline at end of file diff --git a/architecture_design/evolving.md b/architecture_design/evolving.md new file mode 100644 index 0000000..56ace6f --- /dev/null +++ b/architecture_design/evolving.md @@ -0,0 +1,166 @@ +# 流量还在涨:所有服务都部署在一台机器上 +在想到一个好点子后,终于找到程序员小D把项目开发完成、上线了。看看整体情况: +- 一台服务器,LNMP架构 + +> 单机模式是最初始的状态 + +注:LNMP 指的是 Linux + Nginx + MySQL + PHP,是早期的非常流行的Web服务选型 + +# 流量还在涨:单台机器有压力了 +流量还在涨,这时候单台机器开始报警了:内存、磁盘、CPU 都有点扛不住了。 + +小D发现 MySQL 服务占用了不少资源,把它放到单独的服务器上部署会好一点。于是进行了第一次调整: +- 两台服务器:L + M 和 L + N + P + +> 业务服务和基础服务拆开 是比较早期的运维手段 + +# 流量还在涨:MySQL服务扛不住了 +流量还在涨,MySQL被请求过多,扛不住了。 + +小D想,MySQL服务很重要,该省省该花花,可以上好点的服务器啊。于是将MySQL的服务器配置调高好几个档次。 + +> 纵向扩容 提高单机的能力是最简单有效的手段。钞能力有超能力 + +# 流量还在涨:MySQL服务又扛不住了 +小D分析了一下业务现状,PHP业务里面做了很多很重的查询,但是很多内容其实都是静态的。 + +那将这些内容缓存到内存中,不要去服务数据库就好了啊。 + +> 内存缓存可以极大的提高访问速度、减少存储系统的压力 + +注:内存缓存有多种算法,比如: +- LRU:淘汰的是最长时间没有被使用的数据> 经常被使用 +- LFU:淘汰的是最少次被使用的数据 +- ARC:这个算法平衡了 LRU 和 LFU > TODO 详细说明 + +# 流量还在涨:Web服务的磁盘报警了 +现有的业务实现,所有的图片文件都直接保持在Web服务当前的文件夹下面。数据一多,磁盘就满了。 + +小D将静态资源单独的存储到一台IO密集型的机器上,它有超大的磁盘空间,而且还不贵。 + +> 静态资源单独存放 极具性价比 + +# 流量还在涨:Web服务CPU报警了 +用户量是缓慢上涨的,上面的事情按部就班的处理了也就好了。Web服务的CPU扛不住了,小D想着这次就不要纵向扩容了,因为好东西确实贵,而且按照这趋势,顶配的配置也很快会扛不住,于是他选择了多部署Web服务器,将Web服务器的IP加入到域名中。 + +他发现通过前期的数据库改造、文件内容改造,Web服务已经是无状态的了。更多的部署轻而易举。 + +> 横向扩容:通过增加服务器的个数,可以低成本无上限的扩展能力 + +# 流量还在涨:新来的程序员只会写前端 +小D一开始用的是 PHP 的模板,从数据库找时间后渲染成HTML返回。一个人忙不过来了就招了个前端小F。前端表示: +- 两个人在一个仓库里面同时改代码比较麻烦 +- 他也不想学模板的语法,不好调样式啥的 + +小D也发现为了同时为Web端和手机端提供服务,现在的实现方案确实也有问题: +- 服务端逻辑有重复 +- 服务端还要根据客户端的类型写多个模板 + +于是两人一拍即合: +- 服务端对外通过Restful API,供各个端调用 +- 客户端调用服务端的API,自己写前端内容实现赏心悦目的交互体验 + +> 前后端分离 将程序员的职责区分开来,让具体的事情被专业的人做到极致 + +# 流量还在涨:文件服务器报警了 +文件服务器除了存储了用户数据,还存储了前端需要的 html、css 文件等。 +通用的文件比如 jQuery.js 可以白嫖公开的CDN,自己的 html 什么的就需要用相同的思路处理一下了。 + +小D让小F将自己的开发编译后的产物放到CDN上,更快更便宜啊。这个事情需要前端: +- 将写在HTML文件里面的css和js放到外部文件 +- 调整现有的 css和js 的引用路径 + +> 文件服务也是一个具体的基础服务,CDN 能够低成本的让用户实现就近访问 + +# 流量还在涨:用户抱怨访问失败的时间有点长 +小D收到用户吐槽,网站打不开了。小D自己没有复现,但是经过分析后找到了原因:有服务器宕机了,但是DNS系统始终没有更新。 +小D也感觉不合理,每次水平扩容都需要购买新的IP,修改DNS的解析记录;有服务宕机了也需要去修改DNS的解析记录。随着实例越来越多,这些事情发生的概率也越来越高。 +于是,他搭建了一个负载均衡集群,专门进行流量的分发。所有的请求先到负载均衡集群,再交给Web服务处理。 + +> 负载均衡 是服务集群化的实施要素,可以将负载分摊熬多个操作单元上执行 + +# 流量还在涨:数据库读压力太大了 +Web实例越来越多,数据库的读压力也是越来越大了。是时候上数据库读写分离了。小D给数据库按照了几个从库,它们同步主库的数据,只对外提供数据查询服务。应用程序在查询的时候,只访问从库。 + +> 数据库读写分离是处理数据库读压力的常规手段 + +# 流量还在涨:数据库读压力太大了2 +Web实例越来越多,数据库的读压力也是越来越大了。是时候上多级缓存了。 +小D使用了Redis,作为一级> 内存缓存的补充,充当二级缓存。 + +> 多级缓存在不同的场景有不同的实现,比如静态资源会有 客户端本地缓存 -> 客户端内存缓存 -> CDN -> OSS, 动态资源有内存缓存 -> Redis缓存 -> 数据库 + +注:只要用到了缓存,都会有数据一致性的问题,需要考虑缓存过期/更新策略。一般的方案有: +- Cache Aside模式:程序自行维护。在更新后删除缓存 +- 使用中间件去维护缓存:数据库和缓存的维护都交给缓存代理,应用程序将缓存代理当做唯一的数据源 +- 基于MySQL的binlog异步删除缓存 + +# 流量还在涨:数据库读压力太大了3 +在一些场景下,数据库被打挂了: +- 缓存雪崩:大量的Key集体过期,大量请求请求到数据库 +- 缓存击穿:某个热点数据过期后、热点数据重新载入缓存前, 大量请求请求到数据库 +- 缓存穿透:访问到的是一个不存在的记录 + +问题分析是困难的,问题处理则很简单: +- 缓存有效期加上随机值 +- 热点数据永不过期 +- 逻辑过期与异步更新:设置一个逻辑过期时间T1 和 物理过期时间 T2,在T1到T2期间程序异步更新缓存,同步返回缓存 +- 布隆过滤器可以确认某个值是不是不存在> TODO 说明细节 + +> 缓存在使用过程中需要特别关注过期策略 + +# 流量还在涨:新实例启动会让整体服务不稳定 +每次新实例启动时,因为缓存数据为空,都会让系统有个波动。小D需要处理这个问题:缓存预热。手段大概有: +- 应用程序启动时自己请求需要的数据 +- 预热系统通过访问应用程序让应用程序完成数据缓存 + +> 缓存预热也是缓存使用过程中常用的实践。 + +# 流量还在涨:程序开发、上线冲突 +业务真是壮大了,也陆陆续续有新的员工加入,分别负责不同的子模块。但是每次上线光是合代码就很痛苦。 +小D和大家讨论,要不大家拆开吧,自己维护自己的。各服务有自己明确的系统边界,服务之间通过远程调用通信。 + +> 业务复杂后,无论是为了满足业务架构还是组织机构,单体应用拆分为微服务都势在必行 + +注:这一步实际上也完成了数据库拆库。 + +# 流量还在涨:数据库压力太大了 +拆分了项目后,小D负责的模块也被单独的拆分到了自己的数据库实例上。这一步完成了垂直分库:每个业务将自己的表放入到自己的库中,不同的业务的库不一样。 + +但是业务确实很壮大,数据库又扛不住了:单表的数据太大,写入和查询都很慢 +小D做了如下的调整: +- 垂直分表:将一张表拆分为多张表,比如商品表拆分为商品摘要表和商品描述表,不同的场景查询/更新的表不同 +- 水平分表:将同一张表的数据分散到同一个库的不同的表中,减少单表的数据量(可以按主键hash,也可以按照用途,比如历史冷数据,近一个月热数据拆分) +- 水平分库:将数据物理的分散到不同的存储实例上,减少单库的数据量 + +> 分库分表是解决海量数据的常规手段 + +# 流量还在涨:特殊时期资源不够用 +运营说在一月一号这一天给所有的用户打折,预计流量会涨3倍。 +小D表示系统扛不住,需要加机器 +财务表示没有钱,而且后期机器都闲置了 +小D问运营:那把评论、退货服务关一天,可以接受不? +运营咬咬牙,同意了。 + +服务降级没有统一的标准,需要具体情况具体分析。 +- 核心服务通常不可降级的,非核心服务可能能部分降级 +- 服务内部可以通过减少日志输出来完成降级 +- 降级的执行可以是事先的,也可以是应急的;可以是自动的也可以是手动的 + +# 流量还在涨:非核心服务导致了系统崩溃 +小D负责的系统出了一个大Case:这个系统依赖了一个小的服务,平时没有什么问题,但是那次不能响应了,导致他负责的系统整体耗时、资源使用都飙升。 +通过分析,小D认为其实对方不响应也问题不大,给个近似值也可以的。于是开始了系统改造:引入熔断。 + +熔断机制为:统计被依赖的服务的可用性,如果不达标,一段时间内就不访问它了,使用预设的 fallback 逻辑;服务恢复了就继续访问。 + +> 熔断是将局部故障隔离、不影响系统其他服务的常规手段 + +# 流量还在涨:为什么有业务方不顾我的死活疯狂请求 +小D又遇到一个服务调用方,它不知道为什么,在疯狂的请求小D负责的业务,导致集群差点崩溃了。 +小D想着还是要自我保护一下,毕竟对其他的服务都有SLA承诺的,他加上了限流:对于单实例有上限,超了直接返回500;对于每个业务方,约定好配额,超了也返回500。 + +> 限流是自我保护的重要手段 + +# 流量还在涨:... + +流量永不休,本文完。 \ No newline at end of file diff --git a/architecture_design/inversion_of_containers_and_di.md b/architecture_design/inversion_of_containers_and_di.md new file mode 100644 index 0000000..e9f35a6 --- /dev/null +++ b/architecture_design/inversion_of_containers_and_di.md @@ -0,0 +1,427 @@ + + +[原文](http://martinfowler.com/articles/injection.html) + +创建一个将不同项目的组件整合到一个混合项目的轻量级的容器在Java社区中十分急迫。这些容器的背后是一个公共 的模式:如何将各个组件拼接——常常被称为`控制反转`。在这篇文章中我深入探讨这个模式的内部原理,重点在`依赖注入`,并和`服务定位器`进行对比。比起到底选谁,使用时单独配置的原则更重要。 + +------------- + +在企业级Java世界中一件令人欣慰的事情就是,在主流J2EE技术以外,你有大量的构建选择,它们的大多数来着开源。它们中有许多的出现是因为主流的J2EE太笨重和复杂了,当然J2EE也在探索替代方案、提出创造性的想法。一个公共的问题是如何将将不同的元素拼接:你如何将不同的互不了解的团队开发的web的控制器架构和数据库接口整合起来。一些框架对这个问题进行了攻坚,其中一部分派生为将不同层的组件进行整合。它们存储被当做轻量级的容器被引用,例子有 [PicoContainer](http://picocontainer.com/),[Spring](http://www.springsource.org/)。 + +这些容器背后是一系列有趣的、不局限于特定容器的和Java平台的设计原则。我打算讨论这些原则。例子用的是Java语言,,但是大多数原则对于其他面向对象环境都是等价的,特别是 .NET。 + +### 组件和服务 +将元素拼接到一起的话题马上将我拉入了棘手的术语问题:`服务(service)`,`组件(component)`。在这些名词的定义上,你可以发现又长又对立的文章,而我之所以提出来这个,是因为我大量的使用这些术语。 + +我说的组件,是一系列打算被使用的软件,它的作者是无法控制的,代码不会改变。我说的不会改变是指应用不改变组件的源代码,虽然在作者的同意下你可以通过继承的方式改变组件的行为。 + +服务被外部应用使用,比组件更简单。主要的不同,我希望一个组件可以被本地的使用(想想 jar 文件,编译文件, dll 或者源代码引入),而服务可以通过一些远端接口或异步或同步的被远程调用(比如web服务,消息服务,RPC,还有socket)。 + +大多数情况下我在本文中使用服务但是大多数逻辑也可以被应用到组件。很多时候你学哟一些本地的组件框架来更容易的范围远端服务,但是写『组件或服务』只是尝试去读或者写,这个时候,使用『服务』就更时髦一些。 + +### 一个简单的例子 +我使用一个列子来让我们讨论的东西具体化。这个例子和我其他的例子一样都极其的简单。列子不切实际的简单,但是可以在不陷入真实列子的复杂性的前提下关注我们真正需要关注的点。 + +在这个例子中,我写了一个提供一系列由指定导演导演的电影。一个函数就可以实现了: +```java +class MovieLister +{ + public Movie[] moviesDirectedBy(String arg) { + List allMovies = finder.findAll(); + for (Iterator it = allMovies.iterator(); it.hasNext();) { + Movie movie = (Movie) it.next(); + if (!movie.getDirector().equals(arg)) it.remove(); + } + return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]); + } +} +``` +这个方法的实现极其的简单,它向 finder 对象请求所有的影片列表,然后返回列表中指定导演导演的电影。我不打算去处理其筛选逻辑,这个列子只是这篇文章的真实出发点的一个演示物。 + +我们真正关注的是 finder 对象,或者说将 lister 对象和 指定的 finder 对象联系起来的方式。我想让moviesDirectedBy 函数和电影的存储方式完全独立开来,那么所有的函数引用一个finder对象,所有的finder对象都知道如何响应findAll函数。我可以通过对finder定义一个接口而将其提炼出来。 +```java +public interface MovieFinder { + List findAll(); +} +``` + +到现在为止一切都很解耦,但是我生成一个具体的MovieFinder类。在这个案例中我把这个代码放到 lister 类的构造器中。 +```java +class MovieLister +{ + private MovieFinder finder; + + public MovieLister() { + finder = new ColonDelimitdMovieFinder("movies1.txt"); + } + +} +``` +我从一个冒号分割的的文本文件中得到列表,所以取这个名字。我不考虑具体的实现细节。 + +如果我自己使用这个类,这一切都很好。但是如果我的朋友看到了这么好的功能想要使用我的程序的副本呢?如果他们依旧将电影存储在冒号分割的命名为『movies1.txt』的文本文件中,一切都好。但如果文件名不一样呢?我们可以将文件名放入属性文件中解决这个问题。但是如果他们存储列表的形式完全不一样呢:比如SQL数据库,比如XML文件,或者web服务,又或者另外一种格式的文档。这种情况下我们需要另外一个类来获取文件。因为我已经定义了一个MovieFinder接口,moviesDirectedBy函数不会被影响。但是我依旧需要一些方法将正确的 finder 实例创建出来。 + +![Figure 1: The dependencies using a simple creation in the lister class|600*0](http://martinfowler.com/articles/naive.gif) + +Figure 1: The dependencies using a simple creation in the lister class + +上图展示了这种情况的依赖。 MovieLister 类同时依赖 MovieFinder 接口和其实现。我们更喜欢其只依赖接口,但问题是如何生成一个实例。 + +在我的 [P of EAA](http://martinfowler.com/books/eaa.html) 这本书中,我们将这种解决方案描述为 [插件](http://martinfowler.com/eaaCatalog/plugin.html) 。finder的实现类不是在编译时期和程序关联,因为我不知道我的朋友打算如何使用它。换言之我们想让 lister 和任何 finder 的实现正常工作,实现方式是在更晚的一些时间点将其『插』进去。所以问题变成:我如何建立中央的链接:lister 类不关心 finder 的实现类,但是依旧可以和该实例良好的协助以完成工作。 + +让我们延伸到真实系统,我们也许有很多服务和组件。在每种情况中我们可以通过一个接口(要是组件没有目标中的接口可以使用适配器)来和组件进行交流。如果我们想通过不同的方式部署这个系统,我们需要使用插件来集成这些服务,在不同的部署下有不同的实现。 + +所以问题的核心是**如何将插件集成进系统**。这是新出来的轻量级容器面对的一个重要的问题,他们通常都使用控制反转来处理这个问题。 + +### 控制反转 +当容器说他们因为实现了『依赖注入』而变得多么有用时我通常会迷惑不解。[依赖注入](http://martinfowler.com/bliki/InversionOfControl.html) 是框架的公共特征,所以说一个轻量级容器因为使用了依赖注入而变得特别就好像说汽车因为有了轮子而变得特别。 + +问题是:哪些方面的控制会被反转?我第一次接触控制反转时是用户界面的控制权。早期用户交互被应用程序控制。你会有一系列像『输入姓名』的命令,程序本身会进行提示和对每次的输入做出响应。后来图形界面和UI框架出现了,它们有一个主循环,而你的程序开始为屏幕上的各个字段提供事件处理。程序的主控制被反转了,从你被转移到框架了。 + +现在新种类的容器将寻找创建的实现进行了反转。在简单例子中 lister 通过直接实例化来寻找 finder 的实现。这让finder不能成为一个插件。 + +### 依赖注入的形式 +依赖注入会有一个独立的对象(待注入),一个装配器,在被注入类中预留一个字段用来存放待注入对象。他们的依赖关系为: + +![Figure 1: The dependencies using a simple creation in the lister class|600*0](http://martinfowler.com/articles/injector.gif) + +依赖注入主要有三种样式: +- Constructor Injection +- Setter Injection +- Interface Injection + +#### 使用 PicoContainier 进行构造器注入 +我使用一个名为『[PicoContainer](http://picocontainer.com/)』的容器来展示构造器注入的。 +PicoContainer使用构造器来确定如何将一个finder的实现注入lister类。为了实现这个,movie lister 需要定义一个包含所有需要被注入对象信息的构造器。 + +``` +class MovieLister... + public MovieLister(MovieFinder finder) { + this.finder = finder; + } +``` + +而 finder 本身也由pico容器管理,它的构造器为: +``` +class ColonMovieFinder... + + public ColonMovieFinder(String fileName){ + this.fileName = fileName; + } +``` + +pico 容器需要被告知响应的接口的实现类到顶是哪个,这样它才知道如何实例化: + +``` +private MutablePicoContainer configureContainer() { + MutablePicoContainer pico = new DefaultPicoContainer(); + Parameter[] finderParams = {new ConstantParameter("movies1.txt")}; + pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams); + pico.registerComponentImplementation(MovieLister.class); + return pico; +} +``` + +上面的配置的代码就可以参见不同的类了。当我的一个朋友想要使用我的lister,他只需要在他的类中写适当的配置代码就可以得到他需要的对象。当然,将这些配置信息放置到一个单独的文件中是更通用的做法。你可以写一个读取配置文件的类,然后用适当的方式启动容器。Pico本身不包括这个功能,但是NanoContainer却支持用XML配置。 + +#### 使用Spring进行 Setter 注入 +[Spring 框架](http://www.springsource.org/) 是在企业级Java开发中被广泛的使用。它包括事务的抽象层,持久化,web引用开发和JDBC。它倾向于提供setter注入。 + +为了让lister接受注入,需要先定义一个setting方法: +``` +class MovieLister... + private MovieFinder finder; + public void setFinder(MovieFinder finder){ + this.finder = finder; + } +``` + +对于Finder也是: +``` +class ColonMovieFinder... + public void setFilename(String filename) { + this.filename = filename; + } +``` + +然后就是要设置配置文件。Spring同时支持XML和代码: +``` + + + + + + + + + movies1.txt + + + +``` + +我们可以测试一下: +``` +public void testWithSpring() throws Exception { + ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml"); + MovieLister lister = (MovieLister) ctx.getBean("MovieLister"); + Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); + assertEquals("Once Upon a Time in the West", movies[0].getTitle()); +} +``` + +#### 接口注入 +第三种技术是通过定义并使用接口来实现注入。[Avalon](http://avalon.apache.org/) 就是一个使用这样技术的框架。 + +首先,要为被注入的对象定义一个接口。对于finder: +``` +public interface InjectFinder { + void injectFinder(MovieFinder finder) { + this.finder = finder; + } +} +``` + +对于ColonMovieFinder的 fileName 也是同理的: +``` +public interface InjectFinderFilename { + void injectFilename (String filename); +} + +class ColonMovieFinder implements MovieFinder, InjectFinderFilename... + public void injectFilename(String filename) { + this.filename = filename; + } +``` + +然后,用配置代码来表示真正的实现: +``` +class Tester... + + private Container container; + + private void configureContainer() { + container = new Container(); + registerComponents(); + registerInjectors(); + container.start(); + } + + private void registerComponents() { + container.registerComponent("MovieLister", MovieLister.class); + container.registerComponent("MovieFinder", ColonMovieFinder.class); + } + + private void registerInjectors() { + container.registerInjector(InjectFinder.class, container.lookup("MovieFinder")); + container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector()); + } +``` + +接口注入其实是将注入的依赖从具体的业务类移动到注入接口的具体实现类中。 + +### 使用服务定位器 +使用依赖注入的关键的好处是:它移除了MovieList类对MovieFinder的确切的实现的依赖。这让我可以将lister给其他人而他们只需要更加他们自己的环境使用相应的MovieFinder实现。 + +> 注入是打破依赖的一直方式,另外一种是服务定位。 + +服务定位的基本思想是:存在这么一个对象,它知道如何得到一个应用需要的所有的服务。所以这个应用的服务定位器应该有一个可以方法movie finder 的函数。它们的依赖关系为: + +![Figure 1: The dependencies using a simple creation in the lister class|600*0](http://martinfowler.com/articles/locator.gif) + +我们看具体的例子: +``` +class MovieLister... + //lister 使用服务定位器得到 finder + MovieFinder finder = ServiceLocator.movieFinder(); + +class ServiceLocator... + public static MovieFinder movieFinder() { + return soleInstance.movieFinder; + } + private static ServiceLocator soleInstance; + private MovieFinder movieFinder; +``` + +当然,上面的定位服务器需要配置。演示使用代码,当然也可以使用配置文件: +``` +class ServiceLocator... + public static void load(ServiceLocator arg) { + soleInstance = arg; + } + + public ServiceLocator(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + +class Tester... + private void configure() { + ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt"))); + } + + public void testSimple() { + configure(); + MovieLister lister = new MovieLister(); + Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); + assertEquals("Once Upon a Time in the West", movies[0].getTitle()); + } +``` + +我常常听到有人抱怨服务定位器不好,因为不能将它们的实现进行替代,导致不能测试。你其实可以避免将它们设计成这么糟糕。在示例中访问定位器就是一个简单的数据数据处理者。我可以轻易的创建一个服务定位器的测试实现。 + +对于更复杂的定位器我可以创建服务定位器的子类,将子类传入到被注入的类中。我可以改变改变静态函数来调用实例函数而不用直接去访问实例变量。我可以通过使用指定线程存储来指定线程定位器。在不改变服务定位器的客户端的情况下,这些都可以实现。 + +产生这样的看法的一个原因是服务定位器是一个注册而非单列。单列提供了实现注册的简单且容易修改的方式。 + +#### 为定位器使用独立的接口 +MovieLister依赖这个服务加载类,即便在只使用一个服务是,这是个争议。我们可以通过使用`角色接口`来建设这种情况,也就是不使用整个服务定位器,lister可以只定义一些它们需要的接口: + +在当前情况下,lister的提供者同时提供一个需要处理finder的定位器接口: +``` +public interface MovieFinderLocator { + public MovieFinder movieFinder(); +``` + +定位器需要实现这个接口来提供一个finder: +``` +MovieFinderLocator locator = ServiceLocator.locator(); +MovieFinder finder = locator.movieFinder(); +public static ServiceLocator locator() { + return soleInstance; + } + public MovieFinder movieFinder() { + return movieFinder; + } + private static ServiceLocator soleInstance; + private MovieFinder movieFinder; +``` + +你会注意到,自从我们使用了接口,我们再也不能通过具体方法直接访问服务了。我们必须使用一个类来获取一个服务定位器,然后去获取我们需要的服务。 + +#### 一个动态的访问定位器 +上面的例子中,每一个服务在服务定位器中都有一个方法与之对应。还有另外的方法来实现这个,你可以使用动态服务定位器来存如何你需要的服务,在运行时再取出来。 +这种情况下,访问定位器使用一个map,而不是一个字段来单独的存放每个服务,还会提供通用的函数来获取和加载服务: +``` +class ServiceLocator... + private static ServiceLocator soleInstance; + public static void load(ServiceLocator arg) { + soleInstance = arg; + } + private Map services = new HashMap(); + public static Object getService(String key){ + return soleInstance.services.get(key); + } + public void loadService (String key, Object service) { + services.put(key, service); + } +``` + +对每个服务使用一个对应的key: +``` +class Tester... + private void configure() { + ServiceLocator locator = new ServiceLocator(); + locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt")); + ServiceLocator.load(locator); + } +``` + +这样调用方式就是: +``` +class MovieLister... + MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder"); +``` + +我其实不喜欢这种方式虽然它确实很灵活,但是不明确。我找出一个服务的方法是一个文本字符串。我更喜欢明确的函数,张我可以通过实例的定义就容易的找到他们。 + +#### 同时使用定位器和注入 +依赖注入和服务定位器其实不是互斥的。Avalon框架就同时使用他们:它使用一个服务定位器,但使用注入来告诉组件如何找到定位器。 +``` +public class MyMovieLister implements MovieLister, Serviceable { + private MovieFinder finder; + + public void service( ServiceManager manager ) throws ServiceException { + finder = (MovieFinder)manager.lookup("finder"); + } +``` +服务器方法使用了接口注入,允许容器注入服务管理者到MyMovieLister。服务管理者就是一个服务定位器。listers不将manager保存下来,而是直接使用它来查找finder。 + + +### 到底用哪个? +我们已经讨论了各种模式以及它们的变种。现在开始讨论它们的优缺点,这样确定在何时用哪个比较合适。 + +#### 服务定位器 vs 依赖注入 +最基本的选择是确定到底使用服务定位器还是依赖注入。他们都实现解耦功能,上面的例子都讲具体的实现和服务接口独立开来。两个模式最重要的不同是如何将实现提供给应用的类:服务定位器是通过应用类发送消息来得到指定的类,而依赖注入是控制反转了,服务创先在应用类中。 + +控制反转是框架的公共特性,但也造成了一下代价:它更不好理解和调试。所以在万不得已我尽量不去用它。当然这不是说它是一个不好的东西,展示我认为它需要自我改良来让事情更加的明晰。 + +服务定位器的用户都会依赖定位器,这个是关键的不同。定位器可以将其他的实现的依赖隐藏,但是你必须面对定位器本身。所以是靠用来是不是个问题来决定使用的是定位器还是注入器。 + +使用依赖注入可以让查看组件依赖什么更加的容易。使用依赖注入器你可以通过查看注入机制,比如构造器来查看依赖。当使用服务定位器时,你就不得不来搜索源代码了。有查找引用功能的现代IDE虽然会让事情简单一些,但是这个仍然没有通过查看构造器或者Setter函数来得方便。 +当然这很大程度上使用服务的用户的情况。如果你使用一个服务来构建有多个类的应用,使用服务定位器就不是太大的问题。在我给的例子中,服务定位器就工作良好。我的朋友需要做的事情就是使用配置代码或者配置文件配置定位器去获取正确的服务的实现。这种情况下我不觉得注入器的反转有太多的优势。 + +要是lister是一个别人写的、我提供给应用的组件,情况就有所不同了。这种情况下我对我的客户将要使用的服务的API并不了解。每个客户可能都有自己的服务定位器,它们互不兼容。我可以使用独立的接口来处理这个事情:每个客户可以写一个匹配我的接口的适配器给它们的定位器。但无论如何,我的接口都要和定位器见面,定位器早晚要来查找我的接口。一旦适配器出现,联系到定位器的简洁就大打折扣了。 +当使用组件不依赖的注入器,注入器一旦被配置,组件就不能扩展服务了。 + +入门更喜欢依赖注入的一个公共的原因就是测试会更容易。当你做测试时,你需要能方便的使用替换对象来替换真正的服务。但实际上,依赖注入和服务定位器在这点上真的没有什么差别:使用替换对象都很容易。我怀疑这样的观点是从哪些开发人员不让他们的服务定位器便于被替换的团队传出来的。如果你不能在测试中轻易的替换服务对象,那么你的设计就有一系列的问题。 +当然测试问题会由于组件环境非常侵入而变得更严重,比如Java的 EJB 框架。我个人的看法是,这类框架应该对应用的上层代码影响越小越好,尤其不要做会让编辑-执行周期变慢的事情。使用插件来代替重量级的组件可以让这个过程好很多,对于像测试驱动开发(Test Driven Development)的实践尤其关键。 + +所以主要的问题都给了编写将会运行在非自己控制的应用的代码的人员身上。这种情况下,每一个对服务定位器的小小的假设都会成为问题。 + +#### 构造器注入 vs Setter注入 +在组合服务时总是要做一些转换。只需要非常简单的转换是注入的主要好处——至少对构造器注入和Setter注入。你不需要对你的组件做奇怪的事情,配置好它们非常的简单。 + +接口注入过于侵入,你需要些许多接口,还要好好归类它们。对于少数接口,这个还不是太坏。但是拼接组件和依赖太费事了,这也是当前大多数轻量级容器使用Setter注入和构造器注入的原因。 + +到底是选择Setter注入还是构造器注入是一个有趣的话题,它反射出面向对象中更普遍的话题:你应该通过构造器还是Setter函数进行字段填充。 + +我一直让对象在构造时期有尽可能多的默认状态。这个建议来着 Kent Beck 的 Smalltalk [佳实践模式](https://www.amazon.com/gp/product/013476904X?ie=UTF8&tag=martinfowlerc-20&linkCode=as2&camp=1789&creative=9325&creativeASIN=013476904X):构造器函数和构造器参数函数。有参数的构造器让你清楚的知道在构建游戏哦啊对象是发生了什么。如果有不止一种方式来创建对象,那就创建多个构造器提供不同的组合。 + +使用构造器初始化的另外一个好处就是它允许你通过不通过Setter函数来隐藏字段。我觉得这很重要:如果一些东西不应该被改变,不提供Setter函数就非常的好。如果使用Setter来初始化就会有问题了。(在这种情况下我避免使用常规的设置方式,我会使用比如initFoo这样的函数来强调只有在初始化时才去设置它) + +但是凡事都有例外。如果构造器的参数太多,它就会看起来很凌乱,尤其是在没有没有关键参数的语言中。当构造函数过多时通常代表着对象有过多的职责,需要被分离,但是也有可能你确实需要。 + +如果你有多个方式来创建一个对象,通过构造器来展示就比较困难了,因为构造器只能通过参数的数量和类型来区别。展示工厂方法就可以上场了,可以通过组合使用构造器和Setter来实现它们的工作。经典的工厂方法对组件的组合的问题在于它们通常是静态方法,你不能让它们出现在接口里面。你可以创建一个工厂类,但是它其实就是另外一个服务的实例。一个工厂服务常常有好的组织,但是你让人需要使用这里的某个技术来实例化这个工厂。 + +构造器对于简单的字符串参数也是广受诟病。Setter注入可以指明是哪个参数,但是构造函数就只能通过位置来区分了。 + +如果你有多个构造器和继承,事情就特别的尴尬了。为了初始化每个变量你必须为每个父类提供构造器,也添加你自己的参数。这个真的会让构造器越来越庞大。 + +忽略那先缺点我会先有构造器注入开始,一点遇到上述问题马上切换到Setter注入。 + +这个讨论引出了各个将依赖注入作为框架一部分的的开发小组之间争论。然而这些框架的开发人员意识到了同时支持这两种机制的重要性,即便最后会对其中一个会更侧重。 + +#### 代码 vs 配置文件 +将API封装成服务时,使用配置文件还是代码是一个问题。对于大多数汇报不是到多个地方的应用来说,一个单独的文件更合适,大多数时候是个XML文件。但是这也要看是否通过编码集成会更方便。一种情况就是你只有一个简单的项目,没有太多的部署变量,这时候代码就比配置文件简洁。 +一个对比鲜明的场景就是:装配很复杂,有许多可选的步骤。一旦你开始程序单XML崩溃了,你最好使用代码来写清除的程序。然后你会写一个构建类来做这个装配。当你有不同的装配场景,你可以写几个装配类,然后通过简单的配置文件来决定选哪个。 + +我常常觉得大家过度使用配置文件。编程语言一般都简洁有力的配置机制。显得永远可以方便的编译小的组件,在线组件可以作为大系统的创建。如果编译很复杂,脚本语言其实也可以运行良好。 + +常常有人说配置文件不应该有编程性,因为非编程人员会修改它。但是这个的频率多大呢?人吗真的希望非编程人员修改一个复杂的服务端应用的事务隔离等级吗?非编程型的文件只有在起足够简单时才会工作良好。要是它们很复杂,应该考虑属性编程语言。 + +在Java中,我们看见一个现象就是:每个组件都有自己的配置文件,每个人对于每个组件的配置还不一样。如果你有十个组件,你很快发现你不能讲这个十个配置文件保存同步。 +我的建议是提供一个见面来配置所有的配置,同时听过一个单独的配置文件作为默认项。你可以很容易通过界面创建配置文件。如果你写了一个组件就可以将界面留给你的用户。 + +#### 将使用和配置分离 +这一切的要点是:将服务的配置和使用分离开来。将接口和实现分离是基本的设计原则。在面向对象编程中,控制逻辑会确定实例化哪个类,而进一步的条件逻辑会通过多态而非明确的条件代码来实现。 + +如果在单代码中进行分离是有效的,在使用像组件和服务这样的外部服务时则是至关重要的。你是否希望将具体实现的选择推迟到特别的不是中呢?如果是你需要使用一些插件的实现。一旦你使用了插件,将插件和程序的其他部分分离就变得至关重要,这样你才能通过不同的配置进行不同的部署。至于你怎么实现这个反而是次要的。这个配置机制的实现既可以通过配置服务定位器,也可以使用注入直接配置对象。 + + +### 总结 +当前轻量级容器实现服务的插拔都有相同的模式——依赖注入。依赖注入是服务定位器的可替换的选择。构建应用时两者基本上是等价的,但我认为服务定位器因为其更简单的行为而略胜一筹。但如果你构建将在多个应用中被使用的类,依赖注入更合适一点。 + +如果你使用依赖注入,你可以选择形式。要是你没有特别的问题,我建议你使用构造器注入,不然就用Setter注入。如果你选择构建或者获取一个容器,要找同时可以注册构造器注入和Setting注入的那种。 + +而到底是选择服务定位器还是依赖注入,这个反倒没有在应用内部将服务的配置和服务的使用分离来得重要。 diff --git a/architecture_design/mvc_mvp_mvvm.md b/architecture_design/mvc_mvp_mvvm.md new file mode 100644 index 0000000..deb8e3c --- /dev/null +++ b/architecture_design/mvc_mvp_mvvm.md @@ -0,0 +1,120 @@ + + + +*译者按:*MV\*设计模式对MVC比较熟悉,听说iOS那边用了其他的。我觉得这些东西都是会者不难的,要静下心来深入了解一次。 + +原文: http://www.albertzuurbier.com/index.php/programming/84-mvc-vs-mvp-vs-mvvm + +-------------------------- +这是关于 `MVC` `MVP` `MVVM` 的研究。还有更多?对,我浏览了大量的网站、阅读了大量的博客但是没有找到一个满意的解释。比如说,有一个网站告诉我MVP和MVVM是微软版本的MVC。可能吧,但是它有不解释清楚其概念,也不说明什么情况下该用哪个。而且讨论大多数围绕的是.NET,因为.NET平台有很多这个模式框架,但是如果我们使用其他的框架的话我们又能学到什么呢?还有,这个和 Martin Fowler 的呈现模式(Presentation Pattern)有什么关系呢?我在研究这个模式,这篇文章是正在进行的工作。 + +### 现有问题 +我是开发图像应用的。我要创建的图像不包含其他图像:流程图,这个最基本的图表类型。我甚至不打算实现 ECMA Flowchart,我的流程图非常的简单。 +这个图并不难,我们只是用来编程和学习。我的学习方式是:先设置一个目标然后创建一个带解决的问题。我想要我的呈现模型和对象模型有所不同。我的对象模型没有关联(带有箭头的线),但是呈现模型有关联。呈现模型中的关联代表着引用。对有大多数人来说这个并不难,但是我想知道最佳的实践。当然,我们使用的是MVC模式。但是你可能更想知道MVC的变种,MVP和MVVM。这些模式是如何帮助我的呢?在阅读了大量的博客之后我让人不明白,反而问题更多了。 + +### MVC +MVC模式和面包一样平常易见。但是首先得有人提出,然后有些人写出来,再然后就是每个人都可以使用它。MVC在设计模式中讨论观察者模式时被提到,但是MVC模式并不在设计模式这本书中。通常将 Thing-View-Editor(Trygve Reenskaug, May 1979)这篇论文当做MVC模式的起源。这篇论文没有提到控制器(controller),但是描述了编辑器(editor)。几个月后在文章 Models-Views-Controllers (Trygve Reenskaug, December 1979) 控制器被提了出来。这篇文章也明晰了模型(model)的概念。 +> 模型:在计算机系统数据的抽象形式。 + +> 视图:在屏幕上呈现一到多个模型的表象的能力。 + +> 编辑器:用户和一到多个视图组件的接口。 + +在后面那篇文章中控制器取代了编辑器,编辑器重新进行了定义: +> 编辑器:一个特殊的控制器,允许用户修改被视图呈现的信息。 + +就我的理解:控制器控制整个视图;编辑器控制视图的一部分。控制器协调菜单,控制面板和其他像鼠标移动、手势这样的携带命令和数据的对象。编辑器只控制其中特定的任务,比如编辑器中的输入框,或者选择器中的下拉框。这篇论文对控制器也就没有清晰的定义。 + +在2003年, Martin Fowler 在 《Patterns of Enterprise Application Architecture》 这本书中写到:模型 视图 控制器(MVC)是被(误)引用的模式。他随后定义控制器的功能: +> 控制器接收用户的输入,操纵模型并将视图进行适当的更新。 + +这个是MVC模式的最通常的定义。 + +所以,MVC模式的流程是: +1. 用户输入 +2. 控制器转换输入为变化给模型 +3. 当改变完成,控制器调用视图更新并呈现模型的最新状态 + + +![MVC1|600*](http://www.albertzuurbier.com/images/stories/mvc1.png) + +我们对比观察者模式: +1. 用户输入 +2. 控制器转换输入为变化给模型 +3. 模型的任何改变将通过事件或者消息传递给视图(观察者),导致视图更新自己来呈现模型的最新状态 + + +![MVC with Observer|600*](http://www.albertzuurbier.com/images/stories/mvc2.png) + +Martin Fowler 在他的书里面提到了这个模式的各种各样的变种,每次都是控制器适应特定的目的或观点。我们信息视图和模型在特定的应用中可能更简单,但是控制器确要额外的研究。 + +### MVP +维基百科 将 MVP 称为 MVC模式的衍生物。了解这个模式的历史有助于理解。微软也想对这个模式的讨论是最清楚的,Martin Fowler 也有重要的推动作用。 + +在表单控制的框架比如 Visual-XXX编程语言家族和Java Swing 应用中随处可见。ASP.NET web页面和JFaces也提到它。 + +在 MVP 模式中,用户和视图交互。视图自动如何和用户的输入交互,也知道如何呈现模型中的数据。事件在呈现者(Presenter)方向上激发,它在视图和模型组件。呈现者协调改变到模型并返回模型的新的状态给视图。这个是MVP的积极的视图版本。第二个MVP的版本是协调控制器。 + +积极视图的 MVP 的工作流程是: +1. 用户进行输入 +2. 视图转换输入成事件发送给呈现者 +3. 呈现者将改变测定给模式 +4. 模型改变并将值返回给呈现者 +5. 呈现者将值返回给视图 + +![积极视图 MVP|center|600*](http://www.albertzuurbier.com/images/stories/mvppassiveview.png) + +协调控制器MVP稍微有点不同。这种类型的MVP中,呈现者只在需要其协调是才被使用。视图直接和模型联系进行模式的呈现。 + +![积极视图 MVP|center|600*](http://www.albertzuurbier.com/images/stories/mvpsupervisingcontroller.png) + +### MVVM +Model View View-Model 模式最晚被提出来。微软开发,Martin Fowler大力鼓吹。MVVM派生自Martin Fowler 的 呈现模型(Presentation Model) + +呈现模型的关键是将所有的行为从视图中移除。行为和状态放到呈现模型中。这意味着视图不会保存任何状态。视图模型包含状态。举个贴切的例子:当按钮被点击时,状态会从呈现模型保存到模型。如果有什么逻辑需要协调表单上看起来的不同,它发生在呈现模型中,而不是视图中。在MVP模式中视图逻辑可能会在视图中,但是PM 模式一定不会。 + +MVVM通过技术将步子迈得比PM还远:视图不存储任何状态或保存任何逻辑。在WPF中,UI的创建通过XAML这个xml医院,XAML文档不保存任何的状态,只允许和相关的类绑定。渲染引擎将XAML声明的视图渲染成UI并管理视图和视图模型的关系。 + +微软指出UI定义包括页面背后的代码让事情有点含糊不清。然而,页面后的代码理论上只包含不容易在XAML中定义的初始化逻辑和呈现。 + +当视图模型保存状态并绑定到模型,微软以外这个模式也叫 模型 视图 绑定者(Model View Binder MVB)。 + +MVVM的流程是: +1. 用户输入 +2. 视图转换这个为数据然后发送给 视图模型,视图模型处理数据 +3. 视图模型被调命令调用,视图模型将这个发送给模型 +4. 模型更新后返回自身的状态的通知给视图模型 +5. 视图模型将通知返回给视图 +6. 视图调用 视图模型来实现将改变出现处理 + + +![MVVM|center|600*](http://www.albertzuurbier.com/images/stories/mvvm.png) + + +### 总结 +MVC模式是一个伟大的实现,它将呈现,控制和数据分离。但是实现细节让其变得不好理解。比如,当一个用户和控制器交互,如果鼠标点击的信息都在视图中,控制器如何得到被修改的信息?这导致最初的文章包含迷你控制器的介绍:编辑器。在非常底层的操作系统中,MVC模式是可实现的,但是在图形界面中,每件事情都是一个编辑器就不可控了。于是MVC演变到MVP。我敢说MVP模式是三种模式中使用最广的。 + +MVP模式在可视化表单设计中被应用得最多,表单中所有的东西都叫做控件或者组件。这种模式将逻辑放到视图变得可能。许多图像程序都在视图中有大量逻辑。视图只有在图形化测试程序中才可测试。图形化测试很繁琐,而单元测试要轻松很多。越多的逻辑放到视图,测试就越麻烦。MVVM模式,虽然可能有将逻辑放到视图中,但是因为XML的使用,这种行为不被鼓励。 + +我不是说MVVM模式不能通过XAML之外的方式实现。如果MVP的视图功能很少而才能走处理状态,这种MVP的使用方式实际上就是 MVVM。 + + +### 选择 +选择哪个模式和创建应用时使用的框架强关联。在我例子中我使用 可视化IDE Pascal +环境。所以默认就选择了MVP模式。为了更容易测试,我将使用PM模式。 + +在 .NET 环境中,选择更加的清晰。技术的选择的结果在游泳模式的选择。或者说,模式解释了技术的选择。你需要理解模式的实现来连接逻辑是如何进行的。讨论应用的设计,设计模式和语言形成对比。 + +MVC, MVP还有MVVM因此更多的是框架模式而不是设计模式。它们不支持框架的讨论,他们支持框架的介绍。比如,在WPF框架中使用XAML来定义视图,视图类从控制器或者用户控制类推导出来,视图模型类不想上线观察者设计模式来和视图交流,而模型也需要实现观察者模式来和视图模型交流。 + +你必须理解MVC模式和它的派生模式,这样你才可以理解你使用的框架的解释。 diff --git a/computer_science/architecture_design/building_microservices.md b/computer_science/architecture_design/building_microservices.md old mode 100755 new mode 100644 index 3b11117..9dcc7f9 --- a/computer_science/architecture_design/building_microservices.md +++ b/computer_science/architecture_design/building_microservices.md @@ -1,101 +1 @@ - - -### 微服务 -> 微服务:一些协同工作的小而自治的服务。 - -- 很小:服务专注某个边界内的业务。越小,独立性的好处就越大,同时管理大量服务也会越复杂。 -- 自治性: - - 一个微服务就是一个独立的实体,可以独立地部署在PAAS上,也可作为一个操作系统进程存在。 - - 服务之间均通过网络调用进行通信,解耦。 - -#### 微服务的好处 -- 技术异构性:因为每个服务都通过网络通信,良好的API设计就可以满足业务需求,每个服务内部具体实现可以迥然不同; -- 弹性:单服务的系统可以通过在不同机器上部署多个实例来减少功能的完全不用概率,而微服务系统本身就能处理好服务不可用和功能降级问题。 -- 扩展:单服务只能作为整体进行扩展,多个微服务则只需要对需要扩展的服务进行扩展。 -- 简化部署:整体部署,局部部署。 -- 与组织结构想匹配 -- 可组合性 -- 可替代性的优化 - - -#### 面向服务的架构 -SOA(Service-Oriented Architecture): -- 包含多个服务 -- 服务组合最终提供一系列功能 -- 一个服务一独立的形式存在于操作系统进程中 -- 服务之间通过网络调用 - -它在实施中遇到的问题: -- 通信协议的选择 -- 第三方中间件的选择 -- 服务的粒度的确定 - -其实可以认为微服务是SOA的一种特定方法。 - -#### 其他分解技术 -- 共享库:所有的语言都支持共享库,这个是代码层面上的复用。但是无法选择异构技术,非动态链接库部署也不方便; -- 模块:有的语言提供了模块分解技术,运行模块在不停止整个进程的情况下进行局部替换。但是强调模块生命周期,非常复杂; - - -### 没有银弹 -微服务不是免费的午餐,更不是银弹。如果想得到一条通用准则,微服务是错误的选择。一个最大的挑战是需要面对所有分布式系统的复杂性。 - - --------------------------------- - -### 演化式架构师 -与建造建筑物相比,在软件中我们会面临大量的需求变更,使用的工具和技术也具有多样性。绝大多数情况下软件交付后还需要响应用户的变更需求。 - -> 架构师必须改变从一开始就要设计出完美产品的想法,相反,我们应该设计出一个合理的框架,在这个框架下可以慢慢演化出正确的系统。 - -作为一个架构师,不应该过多的观众区域内发生的事情,而应该多关注区域总监的事情,或者时保证我们能对整个系统的健康状态进行监控。 - - -### 如何搭建服务 -什么服务是好的服务: -- 松耦合:服务之间松耦合,修改一个服务不需要修改另一个服务; -- 搞内聚:将相关的细纹聚集在一起,当需要修改某个行为,只需要修改一个地方; - -> 限界上下文:一个由显式边界限定的特定职责。 - -限界上下文有明确的对外接口,也有明确的对外接口。 -思考组织内的限界上下文时,不应该从共享数据的角度来考虑,而应该从这些上下文能够听过的功能来考虑。 - -### 集成 -> 集成是微服务相关技术中最重要的一个。 - -理想的集成技术应该: -- 避免破坏性修改:消费方不随着生产方的修改而修改; -- 保证API的技术无关性:微服务之间的通信方式的技术无关性很重要; -- 服务易于消费方使用; -- 隐藏内部实现细节; - - -#### 服务写作的方式:同步与异步 - - -### 分解单块系统 - -### 部署 - -### 测试 - -### 监控 - -### 安全 - -### 康威定律和系统设计 - -### 规模化微服务 - -### 总结 - +# 构建微服务 diff --git a/computer_science/architecture_design/design_patterns.md b/computer_science/architecture_design/design_patterns.md index 34b7cad..da2e870 100644 --- a/computer_science/architecture_design/design_patterns.md +++ b/computer_science/architecture_design/design_patterns.md @@ -1,62 +1 @@ - - - -# 基本的设计原则 -设计模式其实有无数个,而不是23个。但是它们的源于七个基本的设计原则。 -1. Single Responsibility Principle:不要存在多于一个的导致类变更的原因 - - 变更是必然的,但是改动应该尽可能少的地方,降低对其他组件的影响(高内聚低耦合) -2. Open/Close Principle:软件实体(如模块、组件、类、方法等)应该是可扩展(对扩展开放)而不可修改(对修改关闭) - - 通过扩展软件实体来应对变化,满足新需求,而不是修改旧的代码 -3. Liskov Substitution Principle:子类对象能够替换程序中任何地方出现的父类对象 - - 就是子类对象不要覆写父类的方法,保持子类和父类的行为一致,降低维护成本 -4. Interface Segregation Principle:客户端不应该依赖它不需要的接口 - - 接口要尽可能小 -5. Dependency Inversion:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖具体实现,具体实现应该依赖抽象 - - 细节是多变的,而抽象梗稳定。当细节变化发生时,调用方应该尽可能不要动 -6. The Least Knowledge Principle(The Law of Demeter):一个对象应该对其他对象保持最小的了解 - - 调用方应该尽可能小范围的去访问被调用方的成员,以减少依赖 -7. Composite/Aggregation Reuse Principle:尽量使用组合/聚合 而不是 继承来达到复用目的 - - 继承更不好理解,也暴露了更多的细节 - -前面五个简称 SOLID,由 Martin在论文《设计原则和设计模式》中提出来的。后面两个是其他人提的。 -3和7是从可维护的角度考虑,其他则是从变更发生时尽可能减少修改的角度考虑。 - - -# 设计模式概述 -什么是设计模式:每个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样就能一次又一次的使用该方案而不必做重复劳动。 - -设计模式是面向对象软件设计的经验,使用UML描述。 - -设计模式的基本要素: -- 名字:标识; -- 问题:使用该模式的处境; -- 方案:描述模式的各个部分之间的职责和协助方式; -- 效果:模式应用的效果及使用模式应权衡的问题 - -模式的分类:有很多种分类类型。 - -按照目的分有: -- 创建型:和对象的创建有关; -- 结构型:处理类|对象间的关系; -- 行为型:描述对类|对象交互方式和和职责分配 - -按照范围有: -- 类:通过继承建立关系,静态的; -- 对象:处理对象的关系,动态的 - -我们说常用的设计模式有23个,分类如下: -|范围||目的|| -|-|-|-|-| -| |创建型|结构型|行为型| -|类|Factory Method|Adapter(类)|Interpreter
Template Method| -|对象|Abstract Factory
Builder
Prototype
Singleton|Adapter(对象)
Bridge
Composite
Decorator
Facade
Flyweight
Proxy|Chain of Responsibility
Command
Iterator
Mediator
Memento
Observers
State
Strategy
Visitor| - -## TODO pattern basic elements one by one \ No newline at end of file +# 设计模式 diff --git a/network/tls/1.overview.md b/network/tls/1.overview.md new file mode 100644 index 0000000..3ada663 --- /dev/null +++ b/network/tls/1.overview.md @@ -0,0 +1,6 @@ +tls 是用来加强 http 的安全,得到 HTTPS (HTTP over TLS)。那么: + +1. 安全问题本身到底是什么? +1. tls 的基本流程是哪些呢? + 1. tls 的版本有哪些,差别是什么? +1. tls 在工程实践上要考虑哪些问题呢? diff --git a/network/tls/2.secure_issue.md b/network/tls/2.secure_issue.md new file mode 100644 index 0000000..55d223f --- /dev/null +++ b/network/tls/2.secure_issue.md @@ -0,0 +1,394 @@ +# HTTP安全问题指的是什么? +关于这个问题,我们首先要理解HTTP是什么。HTTP 是 HypeText Transfer Protocol。 是应用层协议,HT则指明内容格式为超文本(可以简单的理解加了超链接(a标签)的文本,实现文本间的交叉引用)。 + +HTTP消息一共有两类: +- 从客户端发出来的是 请求消息; +- 从服务端发出来的是 响应消息。 + +HTTP消息的结构是面对行数据的。文本形式,简单可读,包括: + +| 消息组成 | 请求消息示例 | 响应消息示例 | 说明 | +| ---| ----- | ---| ---| +|Start line(第一行)| GET /test/a.html HTTP/1.0 |HTTP/1.0 200 OK | 表明请求或响应发生了什么 +|Header fields(头部字段)| Accept:text/*
Accept-Language: en,for |Content-type: text/plain
Content-length: 19 | 每个头字段为 key:value,最后一定加一个空行 | +|Body(消息体)| | Hi I'm a message ! | 空行后面是可选的消息体,可以包含任何类型的数据发生给服务器或者响应给客户端,可以是非文本 | + +如上的HTTP消息在进行完全的知识共享(早期发明的目的正是这个)时没有任何问题,但是随着应用范围的扩展,如何保证 "Alice 给 Bob 发了消息M" 确实是 "Alice 给 Bob 发了消息M" 这种问题就出现了。概括的说,就是如下四类问题: +- 机密性:传输内容可能被窃听 +- 完整性:传输内容可能被篡改 +- 认证:伪装者伪装为发送者 +- 不可否认性:发送者事后称自己没做过 + +HTTP安全问题本质上是信息安全问题。 + + +# 密码学应用角度的信息安全问题的概述 + +## 信息安全密码学上的技术 +说 HTTP安全问题,需要打个岔一下密码学。从使用的角度而言,信息安全说面临的威胁及其密码技术主要有: + +| 信息安全问题 | 问题总结 | 应对的密码技术 | +| --- | --- | --- | +| 窃听(秘密泄露) | 机密性 | 对称密码、非对称密码 | +| 篡改(信息被修改) | 完整性 | 单向散列函数、消息认证码、数字签名 | +| 伪装(伪装成真正的发送者) | 认证 | 消息认证码、数字签名 | +| 否认(事后称自己没有做) | 不可否认性 | 数字签名 | + +密码技术概述为: +- 对称密码:又叫共享密钥密码。使用相同的密钥进行加密和解密 + - 解决问题:消息的机密性 + - 基本原理是:$A\ XOR\ b\ XOR\ b = A$。字节序列异或两次会得到自身 + - 常见算法有: + - DES: Data Encryption Standard(不安全) + - AES: Advanced Encryption Standard。用于取代DES。15个候选算法最终 `Rijndael` 被选中 + - 存在的问题:密钥配送(Diffie-Hellman密钥协商算法 或者 非对称密码) +- 非对称密码:又叫公钥密码。发送者用公钥对明文加密得到密文,接收者用私钥对密文解密得到明文 + - 解决问题:消息的机密性 + - 常见算法: + - RSA:(名字是三个作者的名字的首字母) + - 基本原理是:`E`ncryption 和 `N`umber 组成公钥,`D`ecryption 和 `N`umber 组成私钥。 $cipertext = (plaintext ^ E)\ mod\ N, plaintext = (cipertext^D) \ mod \ N$ + - (进一步的基本原理请看数学推导,保证安全是大整数质因数分级没有快速算法) + - ECC:椭圆曲线密码(见说明1) + - 存在的问题: + - 慢 + - 没有解决公钥配送的问题(中间人攻击):发送者拿到了中间人的公钥,中间人解密后再用接收者的公钥加密发给接收者 +- 单向散列函数:one-way hash function。输入是消息,输出是 hah value。相同的输入会得到相同的输出。 + - 解决问题:消息的完整性 + - 常见算法: + - MD4/5: `M`essage `D`igest 的缩写。(不安全) + - SHA-1: `S`ecure `H`ash `A`lgorithm。不安全 + - SHA-256/384/512: 都是SHA-2 + - SHA-3: 一个叫 Keccak 的算法 + - 存在的问题: + - 没有解决认证的问题。 +- 消息认证码:`M`essage `A`uthentication `C`ode。 + - 解决问题:消息的发送者认证 —— 防伪装 + - 基本原理:将共享密钥和消息混合后得到散列值。发送方计算并发送,接收方接收后计算再对比(伪装者没有共享密钥) + - 常见算法: + - 单向散列函数。叫HMAC。 H 就是 Hash的简写。比如 HMAC-SHA-256 + - 分组密码,比如AES的CBC模式(将最后一个分组的密文当做MAC值) + - 存在的问题: + - 没有解决第三方认证:接收者无法向第三方证明消息来自某个发送方(数字签名)。进一步的,也办法解决否认的问题。 +- 数字签名:Digital signature + - 解决问题:消息的发送者认证-防否认 + - 基本原理:签名密钥只有签名者持有,验证密钥所有人验证者都可以持有(非对称密码反过来用即可) + - 常见算法:直接对消息签名(不常用),也可以对消息的散列值签名 + - 存在的问题:无法解决公钥确实是属于真正的发送者(证书) + + +基于上面的各种算法的组合使用,会得到 `混合密码系统`。TLS就是一个典型的例子。 + +说明1:ECC:`E`lliptic `C`urve `C`ryptography。 +椭圆曲线密码密钥短但是强度高,其不仅仅解决加解密的问题,包括: +- 基于椭圆曲线的公钥密码。比如 `ECC` +- 基于椭圆曲线的数字签名。比如 `EC`DSA +- 基于椭圆曲线的密钥交换。比如 `EC`DH `EC`DHE + +## 密钥协商算法 +还需要讲一下密钥协商算法。对称密码中遗留了一个问题:密钥如何配送。 + +这个问题其实是一个无解的问题:在解决数据传输安全问题上,我们要先安全的传输数据(密钥)。那换一个思路:如果密钥不需要传输呢? + +没错,就有一个用作者名称命名的、叫 `Diffie Hellman` 的算法可以完成密钥协商,从而实现双方只需要在网络中传输各自的部分信息、但双方都能计算得到一样的结果(这个就是协商的结果,可以作为共享密钥使用)。 + +基本流程如下: + +R1 = $G^A \ mod \ P$ + +R2 = $G^B \ mod \ P$ + +``` +Alice Bob + +生成质数 P、G +生成随机数 A +计算 R1 + ------- P、G、R1------> + + 生成随机数 B + 计算 R2 + <------- R2 ---------- + + +使用 P、G、A、R2 得到密钥 使用 P、G、B、R1 得到密钥 +``` + +数学表达式是: + + +AliceKey = $R2^A \ mod \ P = (G^B \ mod \ P)^A \ mod \ P = G^{B*A} \ mod \ P$ + +BobKey = $R1^B \ mod \ P = (G^A \ mod \ P)^B \ mod \ P = G^{A*B} \ mod \ P$ + +Alice 和 Bob 得到的结果是一样的,但是结果本身本没有在网络中传输。而想通过传输的数据反向破解却非常难(有限域的离散对数问题)。 + +还有一个ECDH的协商算法。总体流程不变,但是底层的数据问题不同(ECDH是 椭圆曲线上的离散对数问题)。在使用中了解ECDH能够用较短的密钥长度能实现较高的安全性即可。 + +## 公钥证书 +在讲数字签名时遗留了一个问题:用户如何确认公钥来源于发送者,而不是攻击者? + +想象中间人攻击: +``` +Alice 中间人 Bob + +请求公钥 -> + <- 返回MPK +发送 MPK加密的明文-> + 解密得到明文 + ... +``` + +这个问题也看起来是个无解的问题。这时候就需要 “第三方担保”: +- 第三方组织认证其他的公钥的真实性(担保公钥确实来自于真实的所有者) +- 客户端绝对相信某些可信的第三方组织(其公钥被内置在客户端) + +### 证书是什么? +平时我们所说的证书,全称是公钥证书(Public-Key Certificate),简单的理解就是:为公钥加上数字签名得到的内容。 + +**证书的内容** + +证书本身是一个文件,使用的是 `ASN.1` (Abstract Syntax Notation One) 标准来结构化 **描述** 证书。描述如下: + +``` +Certificate ::= SEQUENCE { + // 签名的内容 + tbsCertificate TBSCertificate, + // 证书的签名算法 + signatureAlgorithm AlgorithmIdentifier, + // 签名的值 + signatureValue BIT STRING +} + +TBSCertificate ::= SEQUENCE { + // 版本号,当前有INTEGER { v1(0), v2(1), v3(2) } + version [0] EXPLICIT Version DEFAULT v1, + // 证书编号,一个整数 + serialNumber CertificateSerialNumber, + + // 这个在Certifcate 中也出现了 + signature AlgorithmIdentifier, + + // 有效期 + validity SEQUENCE { + notBefore Time, + notAfter Time + }, + + // 证书本身的公钥信息 + subjectPublicKeyInfo SEQUENCE { + // 服务器公钥对应的算法(比如RSA, ECC) + algorithm AlgorithmIdentifier, + // 服务器公钥值 + subjectPublicKey BIT STRING + }, + + // 颁发者 + issuer Name, + // 颁发者(CA)编号,字符串 + issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + -- If present, version shall be v2 or v3 + // 申请证书的机构 + subject Name, + // 申请者(服务器实体)编号,字符串。早期是域名 + subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + -- If present, version shall be v2 or v3 + // 一系列 扩展 + // 一个证书可能包含多个域名,对应的就是SAN(Subject Directory Attributes) + extensions [3] EXPLICIT Extensions OPTIONAL + -- If present, version shall be v3 +} + +Extension ::= SEQUENCE { + extnID OBJECT IDENTIFIER, + critical BOOLEAN DEFAULT FALSE, + extnValue OCTET STRING +} + +AlgorithmIdentifier ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters ANY DEFINED BY algorithm OPTIONAL +} +``` + +注意,这里面有两个 AlgorithmIdentifier(不包括冗余的那个),分别对应的是 证书本身的签名算法和证书中公钥的加密算法。 + + +**证书的分类** +根据域名进行分类: +- 单域名证书:一个证书只包含一个域名(www.a.com) +- 泛域名证书:一个证书包含一个域名的所有的直接子域名(*.a.com) +- SAN证书:一个证书包含多个域名(a.com 和 a.org) +- SAN泛域名证书:一个证书包含多个普通域名或者泛域名(*.a.com 和 3.a.org) + +根据审核的严格程度,也可以分为: +- `D`omain `V`alidated +- `O`rganization `V`alidated +- `E`xtended `V`alidated + +### 证书如何认证公钥的真实性 +虽然一直在说用的是数字签名技术,但是并没有详细说明流程。 + +当客户端得到了服务端发送的证书后,根据X.509标准解析证书: +1. 域名校验:校验客户端访问的域名和证书的SAN包含的域名是否匹配 +1. 日期校验:确认证书是否在有效期内 +1. 扩展校验:如果扩展Critical为True,客户端必须正确处理该扩展 +1. 公钥校验:证书得到Key Usage 扩展需要包括 Digital Signature 和 Key Encipherment + +这些校验完成之后,就要认证(服务器实体)证书本身的真实性了: +1. 根据扩展中的 `Authority Key Identifier` 找到上一级证书(服务端发送完整证书链的好处在用客户端不需要再额外寻找中间证书) +1. 确认当前证书的签发者(issuer)是上一级证书的使用者(subject) +1. 使用上一级证书中的公钥(subjectPublicKeyInfo字段),校验当前证书的签名(signature)是否正确 + +至此,客户端证书校验完成了 —— 如何确认 “上一级” 证书的真实性呢?答案是:“更上一级”,直到根证书。 + +``` + |-服务器实体 证书-| + | 服务器实体名称 | + |---中间 证书--| | 服务器公钥 | + | 中间CA名称 | ---签发者---| CA签发者 | +|---根 证书--| | 中间CA公钥 | --验证签名->| 签名 | +| 根CA名称 | ---签发者---| CA签发者 | |---------------| +| 根CA公钥 | --验证签名->| 签名 | +| 签名 | |-------------| +|-----------| 中间证书可以有很多 + 自验证签名 +``` +上面的的依赖关系叫 `证书链`,里面的证书分为3类: +- 根证书:也叫自签名证书 +- 中间证书:就是在中间的证书。有上一级证书(可能是跟证书,也可能是其他中间证书)签发 +- 服务器实体证书:就是证书的使用者(比如某家公司)申请的证书。包含了域名、服务器公钥等服务器实体的信息 + +看百度的证书链: +``` +GlobalSign + GlobalSign RSA OV SSL CA 2018 + baidu.com +``` + +根证书是自验证的签名。那问题又来了,如何验证根证书的合法性呢?这是个社会学的问题了,好比海关为什么要相信某个国家颁发的护照? +现实生活中,操作系统会自带系统根证书,不同的软件(浏览器)也都有默认的根证书路径。 + +信任锚:浏览器集成了各个根证书,并充分信任这些根证书 + +### 证书如何管理 +上面其实提到了几个证书有关的行为:认证、签发。就证书而言,包括的操作有: +- 申请、签发(创建/更新)、吊销 +- 存储、获取、认证 + +围绕着证书有这么多的行为、流程,统称 `P`ublic `K`ey `I`nfrastructure 标准族(a family of standards)。而在HTTPS中使用的标准叫 `X.509`。PKI规定了证书的作用和结构(ASN.1描述),证书的申请、吊销、分发,证书的校验等问题。 + + +顺便多提几个名词: +- ASN.1 前面提过,是一种表示法,在本上下文是用来描述PKI的。目的是支持不同平台的网络通信,与机器架构和语言无关 +- BER: basic encoding rules,基本编码规则是第一个编码标准。编码的含义就是:把内存的数据编码为二进制数据(用于传输、存储等) +- DER:distinguished encoding rules,唯一编码规则。是BER的一个子集。是X.509依赖的 编码规则。将X.509 编码为二进制数据(就是证书文件,所以有的证书有 .DER后缀的) +- PEM:privacy-enhanced mail的简称。就是 base64.encode(DER) 的值(ASCII编码格式)。 + + +回到 `PKI`,其包括的实体有: + +``` + +---+ + | C | +------------+ + | e | <-------------------->| End entity | + | r | Operational +------------+ + | t | transactions ^ + | | and management | Management + | / | transactions | transactions + | | | PKI users + | C | v + | R | -------------------+--+-----------+---------------- + | L | ^ ^ + | | | | PKI management + | | v | entities + | R | +------+ | + | e | <---------------------| RA | <---+ | + | p | Publish certificate +------+ | | + | o | | | + | s | | | + | I | v v + | t | +------------+ + | o | <------------------------------| CA | + | r | Publish certificate +------------+ + | y | Publish CRL ^ + | | | + +---+ Management | + transactions | + v + +------+ + | CA | + +------+ + + Figure 1 - PKI Entities +``` +- End entity: 证书的使用者(比如浏览器)或者 证书的申请者(subject of a certificate)(比如某家公司) +- CA: certification authority。证书签发机构。证书的申请者递交`C`ertificate `S`igning `R`equst(证书签名请求) 的身份被审核后给其签发证书 +- RA: registration authority。主要负责证书申请者的身份审核。一般CA包括了RA +- repository:一个存储证书和 `C`ertificate `R`evocation `L`ist(证书吊销列表) 存储和获取的分布式系统 + +从具体流程的角度看证书的生命周期: +``` + |----------| |-------------| |--------| + | | ---1 CSR--> | |---2 CSR-->| | + | 证书订阅人 | | 登记机构 | | CA | + | | <--4 证书--- | (验证身份) |<--3 证书--| | + |----------| |-------------| |--------| + 部署|证书 颁发|证书 + | | +--------|-------------------------------------------------|---- + | | + | ----------------------| + ↓ ↓ ↓ + |----------| |-------------| |------------| + | Web服务器 | | CRL服务器 | | OCSP服务器 | + |----------| |-------------| |------------| + a ↑ |b ↑ ↑ + 请| |验 | | + 求| |证 | | + 签| |签 | | + 名| |名 | | + | ↓ | | + |---------| | | + | 信赖方 | | | + | (浏览器) |---->--A 吊销状态检测----|------->-----------| + |---------| +``` + +1、2、3、4 在上面大概讲过,跳过;a、b 在前面也讲过,跳过。 + +**证书状态管理** + +证书可能在有效期内需要吊销,比如私钥被泄露了。这时候,浏览器就需要及时的知道。一共有两个机制完成这个工作:CRL,OCSP + +CRL 是 Certificate Revocation List,证书吊销列表。是TLS/SSL协议的一部分。其是一个 **全量** 的被吊销的证书序列号和原因的列表。 +它存在一些问题: +- 全量意味着 越来越大,也意味着校验方需要关注不不关心的被吊销的证书,进一步的会让TLS握手时间变长(必须下载所有的CRL,不然就是分开检查(soft-fail),才能进行后面的步骤) +- CRLs不是实时更新 + +OCSP 是一个 CRL 替代方案, Online Certificate Status Protocol,在线证书状态协议。最主要的差别就是其可以只查询某个证书的状态。 + +需要了解的是还有一个 `OCSP Stapling` (OCSP封套)的技术。用来解决 `soft-fail校验`(浏览器为了用户体验不在握手过程中校验证书的状态,而是分开、异步校验) 的问题。流程图如下: + +``` + 浏览器 HTTPS代理 OCSP服务 + | | | + | |-------OSCP请求------->| + | | | + | |<------OSCP响应--------| + | | | + | | | + |--status_request扩展 请求-->| | + | | | + |<--CertificateStatus 子消息-| | + | | | +``` + +CertificateStatus扩展tls1.1后才支持。 + +----- +参考: +- 《图解密码技术》,结城浩 著 +- 《深入浅出HTTPS》 +- 《HTTPS权威指南》 +- RFC2459: Internet X.509 Public Key Infrastructure: Certificate and CRL Profile +- RFC6960: Internet X.509 Public Key Infrastructure: Online Certificate Status Protocol - OCSP \ No newline at end of file diff --git a/network/tls/3.basic_processing.md b/network/tls/3.basic_processing.md new file mode 100644 index 0000000..504b15f --- /dev/null +++ b/network/tls/3.basic_processing.md @@ -0,0 +1,653 @@ +# 前置基本概念 +`T`ransport `L`ayer` S`ecurity 协议实现互联网 客户端/服务端 通信的 反监听、防篡改和防伪装。它在传输层之上,保证传输层的安全。 + +理解TLS,应该提前理解 [`Connection states`](https://datatracker.ietf.org/doc/html/rfc2246#autoid-16)。TLS连接状态指明了 压缩算法、加密算法和数字签名算法及其相关的参数(比如协商得到的主密钥)。伪代码表示如下: + +``` +struct { + // 连接终端 + // enum { server, client } ConnectionEnd; + ConnectionEnd entity; + + // 数据加密相关 + // 加密算法 + // enum { null, rc4, rc2, des, 3des, des40 } BulkCipherAlgorithm; + BulkCipherAlgorithm bulk_cipher_algorithm; + // 加密类型 + // enum { stream, block } CipherType; + CipherType cipher_type; + // 密钥的长度 + uint8 key_size; + uint8 key_material_length; + // enum { true, false } IsExportable; + IsExportable is_exportable; + + // 消息认证相关 + // 认证算法 + // enum { null, md5, sha } MACAlgorithm; + MACAlgorithm mac_algorithm; + // 返回的code长度 + uint8 hash_size; + + // 压缩算法 + // enum { null(0), (255) } CompressionMethod; + CompressionMethod compression_algorithm; + + // 协商的结果,应用数据加密的密钥 + opaque master_secret[48]; + opaque client_random[32]; + opaque server_random[32]; +} SecurityParameters; + +``` + +这里提前引出加密套件(Cipher Suite)的概念。加密套件是TLS协议中用来约定客户端和服务端通信的信息,由一系列的密码基元组成,指导协商和后续的数据传输。 + +举例说明,在 1.2版本里面有如下密码套件: +``` +Cipher Suite Key Cipher Mac + Exchange +----------------------------------- -------- -------------- ------- +TLS_RSA_WITH_AES_256_CBC_SHA256 RSA AES_256_CBC SHA256 +TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA DH_RSA 3DES_EDE_CBC SHA +TLS_DH_anon_WITH_AES_128_CBC_SHA256 DH_anon AES_128_CBC SHA256 +``` + +其格式的为: +``` +TLS_{密钥协商算法[_身份验证算法_]}_WITH_{数据加密算法}_{消息认证码算法} + +身份验证算法指的是证书中包含的服务器公钥的算法。并不是证书本身的签名算法。 +``` + +各个阶段可能的算法有: +``` +Key +Exchange +Algorithm Description Key size limit +--------- -------------------------------- -------------- +DHE_DSS Ephemeral DH with DSS signatures None +DHE_RSA Ephemeral DH with RSA signatures None +DH_anon Anonymous DH, no signatures None +DH_DSS DH with DSS-based certificates None +DH_RSA DH with RSA-based certificates None + RSA = none +NULL No key exchange N/A +RSA RSA key exchange None + + Key IV Block +Cipher Type Material Size Size +------------ ------ -------- ---- ----- +NULL Stream 0 0 N/A +RC4_128 Stream 16 0 N/A +3DES_EDE_CBC Block 24 8 8 +AES_128_CBC Block 16 16 16 +AES_256_CBC Block 32 16 16 + + +MAC Algorithm mac_length mac_key_length +-------- ----------- ---------- -------------- +NULL N/A 0 0 +MD5 HMAC-MD5 16 16 +SHA HMAC-SHA1 20 20 +SHA256 HMAC-SHA256 32 32 +``` + +TLS本身分两层: +- TLS Handshake Protoco 完成 `连接状态` 的 创建/恢复。由三个子协议组成:Handshake protocol、Change cipher spec protocol、Alter procotol,完成服务端和客户端的认证、在应用层协议传输数据之前完成加密算法、密钥的协商 + - 对端的身份认证可用使用 非对称算法,虽然是可选,但是绝大多数时候至少会有一端被要求 + - 共享密钥的协商是安全的,没有第三方可以获取 + - 协商是可靠的:没有攻击者能够在不被通信双方觉察到的情况下修改协商通信 +- TLS Record Protocol 根据 `连接状态` 传输数据。在可靠传输协议(TCP)之上,提供私密(数据被对称加密)、可靠(数据会使用MAC进行一致性校验)的连接 + +总体而言,相关协议的关系为: + ``` +|-----------------------------------------------------------| +| | +| |------------------------------------------------|------| | +| | |---------------------------|-------------- | | | +| | | handshake protocol + | application | | | | +| | | change spec protocol + | data | | | | +| | | alert protocol | protocol | | | | +| | |----↑ TLS Handshke Protocl |-------------| | | | +| | | | | +| | |------------------------------------------ | | | +| | | | | | | +| | | TLS Record Protocl | | | | +| | | | | HTTP | | +| | |-----------------------------------------| | ... | | +| | | | | +| |----------------↑ TLS protocol -----------------| | | +| |--------- (TLS protocol + HTTP = HTTPS) --------|------| | +| | +|---------------------------------------↑ Application Layer | +----------------------------------------------------------------------| +| | | +| TCP | ... | +| | | +----------------------------------------------------↑ Transport Layer | + ``` + + +# TLS握手协议 +下文大部分是使用tls1.0规范。 + +握手阶段能够协商出连接中使用的 一系列的算法、一个主密钥、客户端和服务端随机参数,进而完成认证、加密和数字签名。 + +## TLS全握手流程 +全握手的消息流如下: + +``` + Client Server + + 1 ClientHello --------> + ServerHello 2 + Certificate* 3 + ServerKeyExchange* 4 + CertificateRequest* 5 + <-------- ServerHelloDone 6 + 7 Certificate* + 8 ClientKeyExchange + 9 CertificateVerify* +10 [ChangeCipherSpec] +11 Finished --------> + [ChangeCipherSpec] 12 + <-------- Finished 13 + + +14 Application Data <-------> Application Data + + Fig. 1 - Message flow for a full handshake + +* 代表可选 +``` +(copy from RFC-2246) + + +进一步说明: + +**步骤1** [客户端 ClientHello]客户端主动发起握手,发送 ClientHello。数据格式伪代码如下: + +``` +struct { + uint32 gmt_unix_time; + opaque random_bytes[28]; +} Random; + +struct { + uint8 major, minor; +} ProtocolVersion; + +uint8 CipherSuite[2]; + +struct { + // 客户端期望使用的协议版本,值为其支持的最高的版本 + ProtocolVersion client_version; + + // 客户端生成的随机数 + Random random; + + // 客户端希望当前连接的ID + // 如果没有就为空 + // 如果有,说明希望会话恢复。 + SessionID session_id; + + // 客户端支持的加密套件列表。如果希望会话恢复,需要传递上一次会话一样的值 + CipherSuite cipher_suites<2..2^16-1>; + + // 客户端支持的压缩算法列表。如果希望会话恢复,需要传递上一次会话一样的值 + CompressionMethod compression_methods<1..2^8-1>; +} ClientHello; +``` + +**步骤2** [服务端 ServerHello]服务端收到后,会回复 ServerHello。数据格式伪代码如下: + +``` +struct { + // 并客户端建议低的服务端最高支持的版本 + ProtocolVersion server_version; + + // 服务端生成的随机数 + Random random; + + // 连接的会话ID + // 如果客户端传递了,而且会话缓存也有效,服务端返回相同的值, + // 不然就是不同的值,表示新的会话 + // 服务端也可能返回空,表示不希望会话被缓存 + SessionID session_id; + + // 服务端从客户端支持的列表中挑选出来的 加密套件 + CipherSuite cipher_suite; + + // 服务端从客户端支持的列表中挑选出来的 压缩算法 + CompressionMethod compression_method; +} ServerHello; +``` + +**步骤3** [服务端 Certificate*]如果加密套件要求证书认证,服务端紧接着会发送证书。数据格伪代码如下: + +``` +opaque ASN.1Cert<1..2^24-1>; + +struct { + ASN.1Cert certificate_list<0..2^24-1>; +} Certificate; +``` + +协商结果如果是 `*_anno_*` (anonymous) 就不会执行这步。因为中间人攻击,这类套件已经被废弃了。 + +证书相关内容 见 安全问题章节。 + +**步骤4** [服务端 ServerKeyExchange*]如果证书包括的信息不足以进行预主密钥交换,服务端紧接着发送本子消息。 +这个消息和选中的加密套件中的密钥协商算法有关,主要用来给客户端传输预主密钥。包括如下两种情况: +- RSA的公钥,客户端生成预主密钥后,用公钥加密预主密钥后继续传输 +- Diffie-Hellman的参数,客户端可以完成密钥交换 + +``` +struct { + // The modulus of the server's temporary RSA key. + opaque rsa_modulus<1..2^16-1>; + // The public exponent of the server's temporary RSA key. + opaque rsa_exponent<1..2^16-1>; +} ServerRSAParams; + +struct { + // The prime modulus used for the Diffie-Hellman operation. + opaque dh_p<1..2^16-1>; + // The generator used for the Diffie-Hellman operation. + opaque dh_g<1..2^16-1>; + // The server's Diffie-Hellman public value (g^X mod p). + opaque dh_Ys<1..2^16-1>; +} ServerDHParams; /* Ephemeral DH parameters */ + +// enum { anonymous, rsa, dsa } SignatureAlgorithm; +select (SignatureAlgorithm) +{ case anonymous: struct { }; + case rsa: + digitally-signed struct { + // MD5(ClientHello.random + ServerHello.random + ServerParams); + opaque md5_hash[16]; + // SHA(ClientHello.random + ServerHello.random + ServerParams); + opaque sha_hash[20]; + }; + case dsa: + digitally-signed struct { + opaque sha_hash[20]; + }; +} Signature; + +struct { + // enum { rsa, diffie_hellman } KeyExchangeAlgorithm; + select (KeyExchangeAlgorithm) { + case diffie_hellman: + ServerDHParams params; + Signature signed_params; + case rsa: + ServerRSAParams params; + Signature signed_params; + }; +} ServerKeyExchange; +``` + +这里面有两个问题: +- 预主密钥的生成有两种方式,一个是客户端直接生成。这个会有不满足向前保密(攻击者将历史密文存储,有朝一日拿到了服务端的私钥,主密钥就被破解了,所有的历史消息也被破解了);另一种就是用DH算法,算法相关内容见 “安全问题” 相关的讨论。 +- 预主密钥和主密钥的关系。主密钥就是在后续传输过程中进行对称加解密用到的密钥。那预主密钥如何生成主密钥呢? + +``` +// PRF: pseudo-random function。伪随机函数。入参是 私钥、种子、身份标签,得到的是一个固定长度的字节数组 +// pre_master_secret +// 如果是RSA,则使用客户端内容解密后得到预主密钥 +// 如果是DH,则使用DH算法,其 协商Key(Z) 就是主密钥 +master_secret = PRF(pre_master_secret, "master secret", + ClientHello.random + ServerHello.random) +[0..47]; +``` + + +**步骤5** [服务端 CertificateRequest*]如果需要的话,服务端会马上给客户端发送证书请求的消息。消息格式如下: + +``` +struct { + // enum { rsa_sign(1), dss_sign(2), rsa_fixed_dh(3), dss_fixed_dh(4), (255) } ClientCertificateType; + // 按照服务端喜好排序的一系列的证书类型的请求 + ClientCertificateType certificate_types<1..2^8-1>; + + // opaque DistinguishedName<1..2^16-1>; + // 可接受的证书机构(CA)的名字 + DistinguishedName certificate_authorities<3..2^16-1>; +} CertificateRequest; +``` + +**步骤6** [服务端 ServerHelloDone]服务端一定会发送 `Server hello done` 消息,来表明服务端的消息发完了,等待客户端响应。其消息格式为空。 + +``` +struct { } ServerHelloDone; +``` + +**步骤7** [客户端 Certificate*] 如果服务端发送了请求证书的消息。数据结构和第5步一样。 + +**步骤8** [客户端 ClientKeyExchange]客户端总是会发送 密钥交换消息。对应服务端选中的加密套件,一般有两种情况: +- 客户端生成了预主密钥,将其使用服务端的公钥加密后得到的内容 +- 客户端生成 DH算法的客户端参数 + +其数据结构为: + +``` +struct { + // The latest (newest) version supported by the client. This is + // used to detect version roll-back attacks. Upon receiving the + // premaster secret, the server should check that this value + // matches the value transmitted by the client in the client + // hello message. + ProtocolVersion client_version; + + // 46 securely-generated random bytes. + opaque random[46]; +} PreMasterSecret; + +struct { + public-key-encrypted PreMasterSecret pre_master_secret; +} EncryptedPreMasterSecret; + +struct { + // enum { implicit, explicit } PublicValueEncoding; + select (PublicValueEncoding) { + case implicit: struct { }; + // The client's Diffie-Hellman public value (Yc). + case explicit: opaque dh_Yc<1..2^16-1>; + } dh_public; +} ClientDiffieHellmanPublic; + + +struct { + select (KeyExchangeAlgorithm) { + case rsa: EncryptedPreMasterSecret; + case diffie_hellman: ClientDiffieHellmanPublic; + } exchange_keys; +} ClientKeyExchange; +``` + +**步骤9** [客户端 CertificateVerify*]如果客户端的证书被请求了,且客户端证书具有签名能力(所有证书,除了包含静态的Diffie-Hellman参数的证书)。将前面所有的信息组装到一起,进行签名后发送。签名算法同 `Server key exchange` message。 + +**步骤10** [客户端 ChangeCipherSpec] 客户端会主动发送这个消息。这个消息不是握手协议的,而是一个单独的子协议。表明客户端已经完成了所有的协商信息同步了(这时候双方应该都能计算出预主密钥、主密钥及其参数),后续所有的消息都需要使用TLS记录层协议加密保护了。消息格式伪代码: + +``` +struct { + enum { change_cipher_spec(1), (255) } type; +} ChangeCipherSpec; +``` + +整个过程还有 `连接状态` 的概念,其实也很好理解,握手完成后,连接状态从 待读状态/待写状态 切换为 可读状态/可写状态。 + +**步骤11** [客户端 Finished]客户端总是会发送本消息。它会把迄今为止所有的消息都组装到一起,然后签名。数据格式伪代码如下: + +``` +struct { + // verify_data = + // PRF(master_secret, finished_label, MD5(handshake_messages) + + // SHA-1(handshake_messages)) [0..11]; + // finished_label + // For Finished messages sent by the client, the string "client + // finished". For Finished messages sent by the server, the + // string "server finished". + // handshake_messages + // All of the data from all handshake messages up to but not + // including this message. This is only data visible at the + // handshake layer and does not include record layer headers. + opaque verify_data[12]; +} Finished; +``` + +**步骤12** [服务端 ChangeCipherSpec] 服务端也会发送本消息,含义和数据结构同客户端 + +**步骤13** [服务端 Finished] 服务端也会发送本消息,含义和数据结构同客户端 + + +## TLS快速握手 + +快速握手要解决握手耗时和握手CPU开销的问题。 + +``` +Client Server + +ClientHello --------> + ServerHello + [ChangeCipherSpec] + <-------- Finished +[ChangeCipherSpec] +Finished --------> +Application Data <-------> Application Data + + Fig. 2 - Message flow for an abbreviated handshake +``` + +理解了全握手,就比较容易快速握手了。当ClientHello 中携带了的 SessionID 服务端任务可以使用,服务端会直接回复一个ServerHello,其中的SessionID 的内容就是客户端传输过来的值。后续所有的数据就开始用 `连接状态` 的约定开始保护传输。 + + +# 其他协议 + +**握手协议-报警协议** + +Alter protocol 是指连接的某一方给另外一方发送的报警信息,协议格式如下: + +``` +struct { + // enum { warning(1), fatal(2), (255) } AlertLevel; + AlertLevel level; + AlertDescription description; +} Alert; +``` +- 对于fatal级别的错误,连接应该被关闭,也不应该被复用 +- 报警协议被要求被缴满和压缩传输,但是又归类为握手协议的子协议感觉不太合理(`The TLS Handshake Protocol consists of a suite of three sub-protocols: change cipher spec protcol, handshake protocol, alert protocol`) + +**应用数据协议** + +应用数据消息已经被分帧、压缩和加密,由记录层传输。(这个协议看起来并没有什么用,RFC也只有一个段落描述) + +**记录协议** + +记录层接收上层的任意长度的非空的内容,经过加密等处理后交给下层协议传输。其消息格式如下: + +``` +|--------------------|--------------------| +| 类型 | 版本 | 长度 | | +|--------------------| | +| 标 头 | 数 据 | +|-----------------------------------------| +``` + +相关的格式的伪代码为: +``` +enum { + change_cipher_spec(20), + alert(21), + handshake(22), + application_data(23), (255) +} ContentType; + +struct { + uint8 major, minor; +} ProtocolVersion; + +struct { + ContentType type; + ProtocolVersion version; + uint16 length; + // The application data. This data is transparent and treated as an + // independent block to be dealt with by the higher level protocol + // specified by the type field. + opaque fragment[TLSPlaintext.length]; +} TLSPlaintext; +``` + +`TLSCompressed` 格式和明文一模一样,`TLSCiphertext` 的数据格式和明文很类似: +``` +struct { + ContentType type; + ProtocolVersion version; + uint16 length; + select (CipherSpec.cipher_type) { + case stream: GenericStreamCipher; + case block: GenericBlockCipher; + } fragment; +} TLSCiphertext; +``` + +- 数据先会使用协商的压缩算法进行**压缩**。得到的 `TLSCompressed` 数据格式还是和压缩前的一样,但是长度和数据发生了改变。 +- 数据接着会用协商好的签名算法进行**签名**,追加到 `TLSCompressed` 后面。 +- 数据接着是**加密**,转换为 `TLSCiphertext`。 + + +加密和签名的相关的参数生成逻辑为: +``` +首先生成 key_block: +key_block = PRF(SecurityParameters.master_secret, + "key expansion", + SecurityParameters.server_random + + SecurityParameters.client_random); +(master_secret 由 pre_master_secret 派生而来) + +进一步生成各种数据发生期间需要的参数: +client_write_MAC_secret = key_block[SecurityParameters.hash_size] +server_write_MAC_secret = key_block[SecurityParameters.hash_size] + +client_write_key = key_block[SecurityParameters.key_material_length] +server_write_key = key_block[SecurityParameters.key_material_length] +// 对于 可导出的加密算法,还需要进一步处理: +final_client_write_key = PRF(SecurityParameters.client_write_key, + "client write key", + SecurityParameters.client_random + + SecurityParameters.server_random); +final_server_write_key = PRF(SecurityParameters.server_write_key, + "server write key", + SecurityParameters.client_random + + SecurityParameters.server_random); + +// IV 只在非可导出块(非流)算法中需要生成 +iv_block = PRF("", "IV block", SecurityParameters.client_random + + SecurityParameters.server_random); +client_write_IV = iv_block[SecurityParameters.IV_size] +server_write_IV = iv_block[SecurityParameters.IV_size] +``` + +# tls各版本间的差别 +## tls1.0 vs. tls1.1 + +tls1.0 rfc 发布在1999年, tls1.1 rfc 发布在 2006年。tls1.1 对 tls1.0做了比较小的安全提升,大概为: +- 使用显式IV替代隐式IV,避免 CBC攻击 +- 使用 bad_record_mac 替代 decrytion_failed 报警,避免 CBC攻击 +- ... + +其差别可以查阅 [RFC tls1.1](https://datatracker.ietf.org/doc/html/rfc4346#autoid-2)。 + +**TLS扩展** + +TLS 扩展在 2003年被提出(RFC3546)并加入到后续的一系列版本中。 + +单个扩展的数据格式比较简单: +``` +struct { + ExtensionType extension_type; + opaque extension_data<0..2^16-1>; +} Extension; +``` + +扩展后的ClientHello的数据格式为: +``` +struct { + ProtocolVersion client_version; + Random random; + SessionID session_id; + CipherSuite cipher_suites<2..2^16-1>; + CompressionMethod compression_methods<1..2^8-1>; + Extension client_hello_extension_list<0..2^16-1>; +} ClientHello; +``` + +ClientHello 加上 Extension后还能正常工作的原因是 tls1.0 在设计的时候考虑到了向前兼容性的问题,做了如下规定:`In the interests of forward compatibility, it is permitted for a client hello message to include extra data after the compression methods.` + + +扩展后的ServerHello的数据格式为: +``` +struct { + ProtocolVersion server_version; + Random random; + SessionID session_id; + CipherSuite cipher_suite; + CompressionMethod compression_method; + Extension server_hello_extension_list<0..2^16-1>; +} ServerHello; +``` + +服务端就可能根据自己的情况去回复扩展或者忽略扩展内容。 + +扩展的类型有: +- server_name(0):客户端期望连接的服务端的名字。就是常用的 `S`erver `N`ame `I`ndication。 +- max_fragment_length(1):客户端期望协商得到一个更小的明文帧长度(默认是2^14) +- client_certificate_url(2): +- trusted_ca_keys(3) +- truncated_hmac(4): +- status_request(5):安全问题中提到的 "ocsp stapling" + +其他RFC陆陆续续的添加了新的扩展,常见的有: +- session_ticket:表明支持没有状态的会话恢复。取代SessionID的一种方案。SessionID要求服务端存储连接状态相关的信息,SessionTicket则是将会话状态加密后发给客户端。当会话恢复时直接解密即可。 +- next_protocol_negotiation:表明支持NPN。 +- application_layer_protocol_negotiation:表明支持的应用层协议。这个是用来询问服务端是否支持HTTP/2。如果支持服务端会在扩展中回复,不然就是只支持HTTP/1.1 +- renegotiation_info:表明可以支持安全的重协商。这个是为了解决重协商(在现有的tls连接上再建立一个更安全的连接)里面的一个安全漏洞,不展开描述。 + +## tls1.1 vs. tls1.2 + +tls1.1 rfc 发布在 2006年, tls1.2 rfc 发布在 2008年。tls1.2 对 tls1.1 做了比较大的安全提升,大概为: +- 移除和新加了一些列的加密套件 +- 支持了部分扩展 +- ... + +其差别可以查阅 [RFC tls1.2](https://datatracker.ietf.org/doc/html/rfc5246#autoid-3)。 + +**PSK** + +在tls1.2中使用了 PSK [Pre-Shared Key](https://datatracker.ietf.org/doc/html/rfc4279) 加密套件的概念,用来基于 PSKs 来支持认证(其他的认证方式有基于PKI 和 Kerberos)。 + +比如加密套件包括: +``` +CipherSuite Key Exchange Cipher Hash + +TLS_PSK_WITH_RC4_128_SHA PSK RC4_128 SHA +``` + +pre-shared key 是对称密钥,事先在通信双方完成共享。套件分为三类: +- PSK key exchange algorithm:用来对称密钥完成认证 +- DHE_PSK key exchange algorithm:使用含有pre-shared key 的 Dffie-Hellman 交换认证 +- RSA_PSK key exchange algorithm:结合服务端公钥认证 和 客户端pre-shared key认证 + +细节本文不展开,只说明PSK用来完成认证、和公钥证书类似。 + +## tls1.2 vs. tls1.3 + +tls1.2 rfc 发布在 2008年,tls1.1 rfc 发布在 2018年。tls1.3 对 tls1.2 做了较大的改动,大概为: +- 支持的对称加密算法列表移除了被认为是遗留问题的算法,留下来的都是“关联数据认证加密(`A`uthenticated `E`ncryption with `A`ssociated `D`ata)”算法。加密套件的概念发生了改变,认证机制和密钥交换交换机制 与 记录保护算法分开和Hash分离。 +- 增加 0-RTT模式,以牺牲部分安全性为代价为一些应用数在连接建立阶段节省移除往返 +- Static RSA 和 Diffie-Hellman加密套件被删除,提供向前安全 +- ServerHello 之后的所有握手信息都加密传输。新引入的 `EncryptedExtension` 消息可以保证扩展以加密的方式传输 +- 密钥导出函数被重新设计。新的设计使得密码学家能够通过改进的密钥分离特性进行更容易的分析。基于HMAC的提取-扩展密钥导出函数(HKDF)被用作一个基础的原始组件。 +- Handshake状态机进行了重大重构,以便更具一致性和删除多余的消息如ChangeCipherSpec(除了中间件兼容性需要)。 +- 椭圆曲线算法已经属于基本的规范,且包含了新的签名算法,如EdDSA。TLS 1.3删除了点格式协商以便于每个曲线使用单点格式。 +- 其它的密码学改进包括改变RSA填充以使用RSA概率签名方案(RSASSA-PSS),删除压缩,数字签名算法DSA,和定制DHE组(Ephemeral Diffie-Hellman)。 +- 废弃了TLS1.2的版本协商机制,以便在扩展中添加版本列表。这增加了不支持版本协商的server的兼容性。 +- 之前版本中会话恢复(根据或不根据server端状态)和基于`P`re- `S`hared `K`ey 的密码族已经被一个单独的新PSK交换所取代。 +- 引用已更新至最新版本的RFC(例如,RFC 5280而不是RFC 3280)。 +- ... + +其差别可以查阅 [RFC tls1.3](https://datatracker.ietf.org/doc/html/rfc8446#autoid-3)。 + +tls1.3 和签名的tls版本差别比较大,单独的讲。 + +---- +# 参考 +- [RFC TLS1.0](https://datatracker.ietf.org/doc/html/rfc2246) +- [RFC TLS1.1](https://datatracker.ietf.org/doc/html/rfc4346) +- [RFC TLS1.2](https://datatracker.ietf.org/doc/html/rfc5246) +- [RFC TLS1.3](https://datatracker.ietf.org/doc/html/rfc8446) +- [RFC TLS扩展](https://datatracker.ietf.org/doc/html/rfc3546) +- [RFC TLS Session Ticket](https://datatracker.ietf.org/doc/html/rfc5077) + diff --git a/network/tls/4.tls1.3.md b/network/tls/4.tls1.3.md new file mode 100644 index 0000000..6276bfa --- /dev/null +++ b/network/tls/4.tls1.3.md @@ -0,0 +1,649 @@ +tls1.3 酝酿了十年,和前面的tls版本有了较大的改动 —— 但是整体流程还是类似的。 + +# Handshake Protocol +握手协议的目的还是为 record layer 层协商出相应的安全参数。 + +其类型为: +``` +enum { + - hello_request(0), + client_hello(1), + server_hello(2), + + new_session_ticket(4), + + end_of_early_data(5), + + encrypted_extensions(8), + certificate(11), + - server_key_exchange (12), + certificate_request(13), + - server_hello_done(14), + certificate_verify(15), + - client_key_exchange(16), + finished(20), + + key_update(24), + + message_hash(254), + (255) +} HandshakeType; + +- 表示旧的存在,在1.3中被移除的消息类型 ++ 表示旧的不存在,在1.3中被加入的消息类型 +``` + + +全流程的握手为: + +``` + Client Server + +Key ^ 1 ClientHello +Exch | + key_share* + | + signature_algorithms* + | + psk_key_exchange_modes* + v + pre_shared_key* --------> + + ------------↓ 异常分支:不匹配参数的握手 ↓------------------- + HelloRetryRequest 2.1 + <-------- + key_share + 3 ClientHello + + key_share --------> + ------------↑ 异常分支:不匹配参数的握手 上------------------- + + + ServerHello 2.2 ^ Key + + key_share* | Exch + + pre_shared_key* v + {EncryptedExtensions} 4 ^ Server + {CertificateRequest*} 5 v Params + {Certificate*} 6 ^ + {CertificateVerify*} 7 | Auth + {Finished} 8 v + <-------- [Application Data*] 9 + ^ 10 {Certificate*} +Auth | 11 {CertificateVerify*} + v 12 {Finished} --------> + + [Application Data] <-------> [Application Data] + + ++表示之前提到消息中发送的重要扩展。 +*表示不经常发送的可选或者特定情况下的消息或扩展。 +{} 表示由[sender]_handshake_traffic_secret 导出的秘钥加密的消息。 +[] 表示由[sender]_application_traffic_secret_N 导出的秘钥加密的消息。 +``` + +说明如下: + +**步骤1 + 3** [客户端 ClientHello] 客户端主动发起握手,进行加密协商 或者 在服务端发送 `HelloRequestRequest` 消息后(3),客户端会发送 `ClientHello`。 数据格式伪代码如下: +``` +uint16 ProtocolVersion; +opaque Random[32]; + +uint8 CipherSuite[2]; /* Cryptographic suite selector */ + +struct { + ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */ + Random random; + opaque legacy_session_id<0..32>; + CipherSuite cipher_suites<2..2^16-2>; + opaque legacy_compression_methods<1..2^8-1>; + Extension extensions<8..2^16-1>; +} ClientHello; +``` + +对比 tls < 1.3 的数据格式伪代码: +``` +struct { + uint32 gmt_unix_time; + opaque random_bytes[28]; +} Random; + +struct { + uint8 major, minor; +} ProtocolVersion; + +uint8 CipherSuite[2]; + +struct { + // 客户端期望使用的协议版本,值为其支持的最高的版本 + ProtocolVersion client_version; + + // 客户端生成的随机数 + Random random; + + // 客户端希望当前连接的ID + // 如果没有就为空 + // 如果有,说明希望会话恢复。 + SessionID session_id; + + // 客户端支持的加密套件列表。如果希望会话恢复,需要传递上一次会话一样的值 + CipherSuite cipher_suites<2..2^16-1>; + + // 客户端支持的压缩算法列表。如果希望会话恢复,需要传递上一次会话一样的值 + CompressionMethod compression_methods<1..2^8-1>; +} ClientHello; +``` + +新旧版本对比可以发现: +- 版本号的格式有所不同。旧的版本号被废弃了,使用 legacy_version 字段接收,并且要求值必须是 "1.2"。新增 `supported_versions` 字段来接收。(原因是在事实表明很多服务端并没有正确的实现版本协商) +- SessionID 字段 (和SessionTicket扩展)被移除,使用了 legacy_session_id 字段接收。会话恢复的功能被合并到 `pre-sahred keys` 中。 +- CompressionMethod 被移除(1.3不再压缩),使用了 legacy_compression_methods 接收(安全漏洞) +- 新加了扩展字段(一直都有,1.3直接作为结构的一部分) + +可以发现:版本号、SessionID、CompressionMethod 都被废弃了,Extension 则是被扶正。 + +消息包括扩展部分说明: +- (密钥如何生成和使用)客户端支持的 AEAD算法/HKDF哈希对 的加密套件 +- (密钥如何协商)`supported_groups` 表明 客户端(EC)DHE组,`key_share` 包括这些组的 (EC)DHE共享 +- (密钥如何协商)`pre_shared_key` 包括客户端知道的对称密钥标识,`psk_key_exchange_modes` 说明 PSKs的密钥交换模式(PSK由服务端后续发送的 `NewSessionTicket`消息中的ticket派生出来。所以第一次握手一定不是PSK交换模式) +- (数据如何签名)`signature_algorithms` 表明 客户端支持的签名算法,`signature_algorithms_cert` 用来进一步指定证书类的签名算法 + + +注: +- `A`uthenticated `E`ncryption with `A`ssociated `D`ata 算法:它是一个规范,规定了一种同时提供认证和加密的算法模式,具体算法比如 `AES-GCM(AES的GCM模式,可以类比 AES-CBC)` +- `H`MAC-based Extract-and-Expand `K`ey `D`erivation `F`unction:基于HAMC的压缩和扩展密钥派生函数。是一种基于哈希函数的密钥派生函数,用于从一个长密钥生成一到多个加密密钥。 +- DHE:`DHE denotes ephemeral Diffie-Hellman, where the Diffie-Hellman parameters are signed by a DSS or RSA certificate, which has been signed by the CA —— tls1.0`。是一种临时的DH,使用DH计算出来的共享密钥是临时的,不会在多个会话中多次使用,也就是说DH加上了这种特性后计算出来的相关密钥是动态的,是会不断的变化。所以说DHE是具备完美向前安全性(PFS)。TLS1.3中已经将DH和ECDH这些静态的DH密钥交换算法剔除了。 + + +**`key_share`** 扩展 + +这个扩展 和旧版本的 `ServerKeyExchange` 有点类似。新版本主要用来补充 (EC)DHE的客户端参数。 + +这个扩展的数据格式为: +``` +struct { + // 名字,客户端 服务端使用这个标识是哪组的 + NamedGroup group; + // DHE 或者 ECDHE encode之后的内容 + opaque key_exchange<1..2^16-1>; +} KeyShareEntry; +``` +这个扩展客户端可能发送零到多个。零就代表着客户端请求 `HelloRetryRequest`,让服务端选择NamedGroup。 + +服务端的`ServerHello` 或者 `HelloRetryRequest` 返回一个 KeyShareEntry。 + + +**pre_shared_key**扩展 + +pre_shared_key扩展用于协商PSK密钥建立相关联握手使用的预共享密钥标识。数据结构为: +``` +struct { + // 秘钥标签 + opaque identity<1..2^16-1>; + // 密钥生存时间的混淆版本。 + uint32 obfuscated_ticket_age; +} PskIdentity; + +opaque PskBinderEntry<32..255>; + +struct { + // 客户端想要与服务器协商的标识列表。 + PskIdentity identities<7..2^16-1>; + // 一系列HMAC值,每个标识值一个,并且以相同顺序排列 + PskBinderEntry binders<33..2^16-1>; +} OfferedPsks; + +struct { + select (Handshake.msg_type) { + case client_hello: OfferedPsks; + // 服务器选择的标识,以客户端列表中的标识表示为(0-based)的索引 + case server_hello: uint16 selected_identity; + }; +} PreSharedKeyExtension; +``` + +key交换模式除了 `(EC)DHE`,就是 PSK了(客户端预生成的被剔除)。如果客户端想使用PSK,就需要发送 `pre_shared_key` + `psk_key_exchange_modes` 扩展。 + +`psk_key_exchange_modes` 的数据格式为: +``` +enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode; + +struct { + PskKeyExchangeMode ke_modes<1..255>; +} PskKeyExchangeModes; +``` +- psk_ke: PSK-only密钥建立。 在这种模式下,服务器不得提供key_share值。 +- psk_dhe_ke: PSK和(EC)DHE的秘钥建立。 在这种模式下,客户端和服务器必须提供key_share值 + + +**early_data**扩展 + +如果使用了PSK,客户端就能够在 第一个消息中发送应用数据,实现0-RTT握手。 + +``` +Client Server + +ClientHello ++ early_data ++ key_share* ++ psk_key_exchange_modes ++ pre_shared_key +(Application Data*) --------> + ServerHello + + pre_shared_key + + key_share* + {EncryptedExtensions} + + early_data* + {Finished} + <-------- [Application Data*] +(EndOfEarlyData) +{Finished} --------> + +[Application Data] <-------> [Application Data] + + 0-RTT的握手消息流 + ++ 表明是在之前提到的消息中发送的重要扩展 +* 表明可选的或者特定条件下发送的消息或扩展 +() 表示消息由client_early_traffic_secret导出的密钥保护 +{} 表示消息由 [sender]_handshake_traffic_secret导出的密钥保护 +[] 表示消息由[sender]_application_traffic_secret_N导出的密钥保护 +``` + +方法就是使用 `early_data`扩展 + `pre_shared_key`扩展。`early_data`的数据结构为: +``` +struct {} Empty; + +struct { + select (Handshake.msg_type) { + case new_session_ticket: uint32 max_early_data_size; + case client_hello: Empty; + case encrypted_extensions: Empty; + }; +} EarlyDataIndication; +``` + + +**步骤2.2** [服务端 HelloRetryRequest*] 如果服务端选择了一个(EC)DHE组但是客户端没有提供兼容的 `key_share` 扩展,服务端必须回复 `HelloRetryRequest` 消息。这时候客户端需要重新发送 `ClientHello`。 + +HelloRetryRequest 的数据格式和字段和 ServerHello 是一样的。 + +功能上则和之前版本的 `hello_request` 很类似。 + + +**步骤2.1** [服务端 ServerHello] 如果服务端选择参数成功,将回复 `ServerHello`。数据结构伪代码如下: +``` +struct { + ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */ + Random random; + opaque legacy_session_id_echo<0..32>; + CipherSuite cipher_suite; + uint8 legacy_compression_method = 0; + Extension extensions<6..2^16-1>; +} ServerHello; +``` + +对比旧的数据格式: + +``` +struct { + // 并客户端建议低的服务端最高支持的版本 + ProtocolVersion server_version; + + // 服务端生成的随机数 + Random random; + + // 连接的会话ID + // 如果客户端传递了,而且会话缓存也有效,服务端返回相同的值, + // 不然就是不同的值,表示新的会话 + // 服务端也可能返回空,表示不希望会话被缓存 + SessionID session_id; + + // 服务端从客户端支持的列表中挑选出来的 加密套件 + CipherSuite cipher_suite; + + // 服务端从客户端支持的列表中挑选出来的 压缩算法 + CompressionMethod compression_method; +} ServerHello; +``` + +变化和ClientHello是一样的。 + +扩展里面可以包括如下信息: +- `pre_shared_key` 扩展:如果使用了PSK,服务端会提供本扩展,选择一个key +- `key_share` 扩展: 如果 (EC)DHE 被使用,服务端会提供本扩展(如果没有使用PS,那么 (EC)DHE 和 基于证书的认证总是被使用) + +**步骤4** [服务端 EncryptedExtensions] 服务端会马上使用 `server_handshake_traffic_secret` 衍生出的密钥 加密发送 本消息(不需要建立加密上下文但不与各个证书相关联的扩展)。数据格式伪代码为: + +``` +struct { + Extension extensions<0..2^16-1>; +} EncryptedExtensions; +``` + + +**步骤5** [服务端 CertificateRequest*] 如果服务端需要对客户端认证,本消息将发送。消息格式如下: + +``` +struct { + // 一个字符串,会在客户端的响应中返回 + opaque certificate_request_context<0..2^8-1>; + Extension extensions<2..2^16-1>; +} CertificateRequest; +``` + +对比旧格式: +``` +struct { + // enum { rsa_sign(1), dss_sign(2), rsa_fixed_dh(3), dss_fixed_dh(4), (255) } ClientCertificateType; + // 按照服务端喜好排序的一系列的证书类型的请求 + ClientCertificateType certificate_types<1..2^8-1>; + + // opaque DistinguishedName<1..2^16-1>; + // 可接受的证书机构(CA)的名字 + DistinguishedName certificate_authorities<3..2^16-1>; +} CertificateRequest; +``` + +可以发现,一切都可以扩展化。 + +相关扩展:**post_handshake_auth** + +这个扩展只能客户端发送,表示愿意执行握手的认证: +- 如果客户端没有发送,服务端不能发送`CertificateRequest`。 +- 如果发送了,服务器可以在握手完成后的任何时候通过发送CertificateRequest消息来请求客户端认证。 客户端必须用适当的Authentication消息来响应 + +**步骤6** [服务端 Certificate*] 当使用证书作为密钥交换方法进行认证(除了PSK外都是)时,服务端必须发送证书。数据格式为: + +``` +enum { + X509(0), + RawPublicKey(2), + (255) +} CertificateType; + +struct { + select (certificate_type) { + case RawPublicKey: + /* From RFC 7250 ASN.1_subjectPublicKeyInfo */ + opaque ASN1_subjectPublicKeyInfo<1..2^24-1>; + + case X509: + opaque cert_data<1..2^24-1>; + }; + // 目前,有效的扩展为 OCSP Status 和 SignedCertificateTimestamp(确保 Certificate Transparency 证书透明度的一个机制) + Extension extensions<0..2^16-1>; +} CertificateEntry; + +struct { + // 服务端认证为空,客户端认证为CertificateRequest.certificate_request_context + opaque certificate_request_context<0..2^8-1>; + // CertificateEntry结构的序列(链),每个结构包含一个证书和一组扩展。 + CertificateEntry certificate_list<0..2^24-1>; +} Certificate; +``` + +对比旧格式: + +``` +opaque ASN.1Cert<1..2^24-1>; + +struct { + ASN.1Cert certificate_list<0..2^24-1>; +} Certificate; +``` + +**步骤7** [服务端 CertificateVerify*] 用于明确证明端点拥有与其证书相对应的私钥。CertificateVerify消息也为截至当前的握手提供完整性。 服务器在通过证书进行验证时必须发送此消息,客户端在通过证书进行验证时必须发送此消息。 + +计算方式就是将当前所有的握手消息都使用私钥进行签名,供对端校验。 + +旧版本并没有这一步。 + +**步骤8** [服务端 Finished] 数据格式如下: +``` +finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length) + +struct { + // verify_data = + // HMAC( + // finished_key, + // Transcript-Hash(Handshake Context, Certificate*, CertificateVerify*) + // ) + opaque verify_data[Hash.length]; +} Finished; +``` + +和旧版本对比: +``` +struct { + // verify_data = + // PRF(master_secret, finished_label, MD5(handshake_messages) + + // SHA-1(handshake_messages)) [0..11]; + // finished_label + // For Finished messages sent by the client, the string "client + // finished". For Finished messages sent by the server, the + // string "server finished". + // handshake_messages + // All of the data from all handshake messages up to but not + // including this message. This is only data visible at the + // handshake layer and does not include record layer headers. + opaque verify_data[12]; +} Finished; +``` + +签名算法和源数据都不一样了。 + + +**步骤9** [服务端 Application Data] 服务端可以在此刻发送数据,但是由于握手还没有完成,所以既不能保证对端的身份,也不能保证它的活性(如ClientHello可能已经被重放) + +**步骤10** [客户端 Certificate*] 同步骤6。 + +**步骤11** [客户端 CertificateVerify*] 同步骤7。 + +**步骤12** [客户端 Finished] 同步骤8。 + + +## 其他消息: Handshake前的消息 + +TLS还允许在主握手之后发送其他消息。这些消息使用握手内容类型,并由合适的应用流量密钥进行加密。主要以下三个。 + +**NewSessionTicket**消息 +在收到客户端Finished消息后,服务端任何时候都可以发送NewSessionTicket消息。 该消息在ticket值和从恢复主秘钥(resumption_master_secret)中导出的秘密PSK之间建立了对应关系。 + +服务器可以在一个连接上发送多个ticket,可以是紧接着发送,也可以是在特定事件之后。 + +多tikcet对客户端有多种用途,包括: +- 打开多个并行HTTP连接。 +- 通过(例如)Happy Eyeballs [RFC8305]或相关技术跨接口和地址族执行连接竞速。 + +其数据结构为: +``` +struct { + // 表示从发布ticket时间开始的32位无符号整数,以秒为单位,网络序。 服务器不得使用任何大于604800秒(7天)的值。 + uint32 ticket_lifetime; + + // 一个安全生成的随机32位值,用于掩盖客户端pre_shared_key 扩展中的ticket年龄。 + uint32 ticket_age_add; + + // 每个ticket对应一个值,使ticket在这个连接上唯一。 + opaque ticket_nonce<0..255>; + + // 作为PSK标识的ticket值。 + opaque ticket<1..2^16-1>; + + Extension extensions<0..2^16-2>; +} NewSessionTicket; +``` + +ticket关联的PSK的计算方法为: +``` +PSK = HKDF-Expand-Label(resumption_master_secret, + "resumption", ticket_nonce, Hash.length) +``` + +原则上可以持续发行新的ticket,它可以无限期地延长最初从initial non-PSK握手(很可能与对端证书绑定)中派生的密钥材料的寿命。 + +任何ticket必须只用建立原始连接时使用的KDF哈希算法相同的密码套件来恢复。 +``` + Client Server + +Initial Handshake: + ClientHello + + key_share --------> + ServerHello + + key_share + {EncryptedExtensions} + {CertificateRequest*} + {Certificate*} + {CertificateVerify*} + {Finished} + <-------- [Application Data*] + {Certificate*} + {CertificateVerify*} + {Finished} --------> + <-------- [NewSessionTicket] + [Application Data] <-------> [Application Data] + + +Subsequent Handshake: + ClientHello + + key_share* + + pre_shared_key --------> + ServerHello + + pre_shared_key + + key_share* + {EncryptedExtensions} + {Finished} + <-------- [Application Data*] + {Finished} --------> + [Application Data] <-------> [Application Data] + + 会话恢复和PSK的消息流 +``` + + +**CertificateRequest**消息 + +当客户端发送了post_handshake_auth扩展后,服务器可以在握手完成后的任何时候通过发送CertificateRequest消息来请求客户端认证。 客户端必须用适当的Authentication消息来响应。 + +**KeyUpdate**消息 + +KeyUpdate握手消息用于指示发送方正在更新其发送的加密密钥。这个消息可以由任何一端在Finished消息后发送。 + +在一组给定的密钥下,可以安全加密的明文数量是有密码学限制的。 [AEAD-LIMITS]提供了对这些限制的分析,其假设是底层基元(AES或ChaCha20)没有弱点。在达到这些限制之前,实现应该进行密钥更新。 +- 对于AES-GCM来说,在给定的连接上,最多可加密2^24.5大小的记录(约2400万条),同时保持约2^-57的安全系数,以保证验证加密(AE,Authenticated Encryption)的安全性。 +- 对于ChaCha20/Poly1305,记录序列号将在达到安全限值之前被wrap。 + + +# Record Protocol +数据结构和旧版本是一样的(兼容性),但是流程从 `TLSPlaintext` -压缩-> `TLSCompressed`(+签名) -加密-> `TLSCiphertext` 变为:`TLSPlaintext` -AEAD-> `TLSCiphertext`。 + +密钥生成算法则有了较大的改动。 明文和密文相关转换关系为: +``` +AEADEncrypted = + AEAD-Encrypt(write_key, nonce, additional_data, plaintext) + +plaintext of encrypted_record = + AEAD-Decrypt(peer_write_key, nonce, additional_data, AEADEncrypted) +``` + +**nonce算法** + +每条记录都有一个int64的序列化,0开始,i++。将其按网络序编码,左边用0填充到iv_length,在与静态的 client_write_iv或server_write_iv(取决于角色)进行异或 得到。 + + +**write_key算法** + +``` +[sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length) +[sender]_write_iv = HKDF-Expand-Label(Secret, "iv", "", iv_length) + +Secret 来自于: + +-------------------+---------------------------------------+ + | Record Type | Secret | + +-------------------+---------------------------------------+ + | 0-RTT Application | client_early_traffic_secret | + | | | + | Handshake | [sender]_handshake_traffic_secret | + | | | + | Application Data | [sender]_application_traffic_secret_N | + +-------------------+---------------------------------------+ +``` + +对于各种 `Secret` (traffic_secret),计算逻辑比较复杂,按照如下格式约定: +- HKDF-Extract从上取Salt参数,从左取IKM参数,其输出在底部,输出的名称在右侧。 +- Derive-Secret的Secret参数用进位箭头表示。例如,Early Secret是生成client_early_traffic_secret的Secret。 +- "0"表示一串Hash.length字节设置为0。 + +有如下推导图: +``` + 0 + | + v +PSK -> HKDF-Extract = Early Secret + | + +-----> Derive-Secret(., "ext binder" | "res binder", "") + | = binder_key + | + +-----> Derive-Secret(., "c e traffic", ClientHello) + | = client_early_traffic_secret + | + +-----> Derive-Secret(., "e exp master", ClientHello) + | = early_exporter_master_secret + v + Derive-Secret(., "derived", "") + | + v +(EC)DHE -> HKDF-Extract = Handshake Secret + | + +-----> Derive-Secret(., "c hs traffic", + | ClientHello...ServerHello) + | = client_handshake_traffic_secret + | + +-----> Derive-Secret(., "s hs traffic", + | ClientHello...ServerHello) + | = server_handshake_traffic_secret + v + Derive-Secret(., "derived", "") + | + v +0 -> HKDF-Extract = Master Secret + | + +-----> Derive-Secret(., "c ap traffic", + | ClientHello...server Finished) + | = client_application_traffic_secret_0 + | + +-----> Derive-Secret(., "s ap traffic", + | ClientHello...server Finished) + | = server_application_traffic_secret_0 + | + +-----> Derive-Secret(., "exp master", + | ClientHello...server Finished) + | = exporter_master_secret + | + +-----> Derive-Secret(., "res master", + ClientHello...client Finished) + = resumption_master_secret +``` + +其中的 `Derive-Secret` 函数定义如下: + +``` +HKDF-Expand-Label(Secret, Label, Context, Length) = + HKDF-Expand(Secret, HkdfLabel, Length) + +Where HkdfLabel is specified as: + +struct { + uint16 length = Length; + opaque label<7..255> = "tls13 " + Label; + opaque context<0..255> = Context; +} HkdfLabel; + +Derive-Secret(Secret, Label, Messages) = + HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length) +``` + +其中 `I`nput `K`eying `M`aterial有两个来源: +- PSK (外部建立的预共享密钥,或从以前连接中的resumption_master_secret值导出) +- (EC)DHE共享secret + + 如果给定的secret不可用,则使用Hash.length字节的0值,并不意味着跳过一轮。所以如果没有使用PSK,Early Secret仍将是HKDF-Extract(0,0)。 + + +# Alert Protocol +和旧的类似,但是更细化了,细节不展开描述。 + +---- +# 参考 +- [RFC TLS1.0](https://datatracker.ietf.org/doc/html/rfc2246) +- [RFC TLS1.3](https://datatracker.ietf.org/doc/html/rfc8446) + diff --git a/network/tls/5.practices.md b/network/tls/5.practices.md new file mode 100644 index 0000000..bd63c9d --- /dev/null +++ b/network/tls/5.practices.md @@ -0,0 +1,28 @@ +TLS在实践过程中需要考虑的点有很多,简单的描述。 + +# HTTP网站如何升级到HTTPS网站 +一个方式是301(客户端)重定向。但是会多一次网络开销。 + +另外一个方式是 `H`TTP `S`trict `T`ransport `S`ecurity。HSTS 需要客户端配合。方式是服务端在 HTTPS的响应中加上 `Strict-Transport-Security` 头部,客户端解析后来定义自己的逻辑。它其实是客户端重定向。 + +举例说明: + +1. 客户端 访问 http://www.example.com +2. 服务端返回了 301 重定向到 https://www.example.com +3. 客户端 访问 https://www.example.com +4. 服务端返回并携带了如下头:`Strict-Transport-Security: max-age=1; includeSubDomains; preload` + +以后的 max-age = 1秒 的时间内,客户端访问 http://www.example.com 和其子域时(includeSubDomains) 时(包括里面里面的混合内容,比如js),都应该遵循 HSTS的协议,自行重定向到 https网站 + + +**preload** 含义 + +为了解决第一次访问时因为客户端不知道网站是否支持HTTPS必须有一次http访问的问题。使用 HSTS 的预加载功能,预加载可以将指定域名添加到 Preload List 名单中,添加到 Preload List 名单后,浏览器就只能通过 HTTPS 方法来进行访问,从而杜绝 HTTP 访问时被这类攻击的可能。 + +可以通过访问https://hstspreload.org/ 提交域名申请。但需要注意的是,添加后的域名将会硬编码到浏览器内。(感觉Header里面的 preload没什么用) + + + +--- +参考: +(RFC HSTS)[https://datatracker.ietf.org/doc/html/rfc6797] \ No newline at end of file