一、了解 HTTP 协议
1 访问网页的流程
2 HTTP概念
HTTP是一种能够获取如 HTML 这样的网络资源的 protocol(通讯协议)。它是在 Web 上进行数据交换的基础。
- 是一种 client-server 协议
- 基于 TCP/IP 通信协议来传递数据
- 属于应用层的面向对象的协议
3 HTTP 历史
- HTTP/0.9 - 单行协议 此时的HTTP非常简单:只支持GET方法;没有首部;只能获取纯文本。
- HTTP/1.0 - 搭建协议的框架
1996年,HTTP正式被作为标准公布,版本为HTTP/1.0。1.0版本增加了首部、状态码、权限、缓存、长连接(默认短连接)等规范,可以说搭建了协议的基本框架。 - HTTP/1.1 - 进一步完善
1997年,1.1版本接踵而至。1.1版本的重大改进在于默认长连接;强制客户端提供 Host 首部;管线化;Cache-Control、ETag等缓存的相关扩展。
4 TCP/IP协议
4.1 TCP/IP协议概念
利用 IP 进行通信时所必须用到的协议群的统称。具体来说,IP 或 ICMP、TCP 或 UDP、TELNET 或 FTP、以及 HTTP 等都属于 TCP/IP 协议。
4.2 TCP/IP模型
4.3 数据传输
每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。 数据包从应用层往下走时, 要不断经历封装的过程, 而从链路层往上走时, 则不断经历解封的过程;
4.3.1 应用程序处理
首先应用程序会进行编码处理(相当于 OSI 表示层); 编码转化后, 邮件不会立即被发送, 会等待建立通信连接,(相当于 OSI 会话层);
4.3.2 TCP 模块的处理
TCP 根据应用的指示, 负责建立连接、发送数据以及断开连接; TCP 提供将应用层发来的数据顺利发送至对端的可靠传输; 为了实现这一功能, 需要在应用层数据的前端附加一个 TCP 首部;
4.3.3 IP 模块的处理
IP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据, 并在 TCP 首部的前端加上自己的 IP 首部; IP 包生成后, 选择合适的网间路由和交换节点传送 IP 数据包;
4.3.4 网络接口(以太网驱动)的处理 (数据链路层 + 物理层)
接收 IP 传过来的 IP 包并附加上以太网首部, 从而生成以太网数据包, 通过物理层传输给接收端;
4.3.5 网络接口(以太网驱动)的处理 (数据链路层 + 物理层)
主机收到以太网数据包后, 首先从以太网包首部找到 MAC 地址判断是否为发送给自己的包, 若不是则丢弃数据; 如果是发送给自己的包, 则从以太网包首部中的类型确定数据类型, 再传给相应的模块, 如 IP、ARP 等;
4.3.6 IP 模块的处理
IP 模块接收到数据后, 从包首部中判断此 IP 地址是否与自己的 IP 地址匹配, 如果匹配则根据首部的协议类型将数据(去除协议首部后的数据包 / 解封的过程)发送给对应的模块, 如 TCP、UDP; 对于有路由器的情况, 接收端地址往往不是自己的地址, 此时要借助路由控制表获取应该送往的主机或路由器之后再进行转发数据;
4.3.7 TCP 模块的处理
在 TCP 模块中, 首先会计算一下校验和, 判断数据是否被破坏, 然后检查是否在按照序号接收数据, 根据序号顺序重组数据, 最后检查端口号, 确定具体的应用程序; 数据被完整地接收以后, 会传给由端口号识别的应用程序;
4.3.8 应用程序的处理
接收端应用程序会直接接收发送端发送的数据, 通过解析数据, 展示相应的内容;
4.4 TCP 报文首部
- 源端口和目的端口,各占2个字节,分别写入源端口和目的端口;
- 序号(Sequence Number),占4个字节,TCP连接中传送的字节流中的每个字节都按顺序编号。例如,一段报文的序号字段值是 301 ,而携带的数据共有100字段,显然下一个报文段(如果还有的话)的数据序号应该从401开始;
- 确认号(Acknowledgment Number),占4个字节,是期望收到对方下一个报文的第一个数据字节的序号。例如,B收到了A发送过来的报文,其序列号字段是501,而数据长度是200字节,这表明B正确的收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701;
- 数据偏移,占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远;
- 保留,占6位,保留今后使用,但目前应都位0;
- 紧急URG,当URG=1,表明紧急指针字段有效。告诉系统此报文段中有紧急数据;
- 确认ACK(Acknowledgment),仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;
- 推送PSH(Push),当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就将PSH=1;
- 复位RST(Reset),当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接;
- 同步SYN,在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;
- 终止FIN,用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放; 窗口,占2字节,指的是通知接收方,发送本报文你需要有多大的空间来接受;
- 检验和,占2字节,校验首部和数据这两部分;
- 紧急指针,占2字节,指出本报文段中的紧急数据的字节数;
- 选项,长度可变,定义一些其他的可选的参数。
4.5 三次握手
4.5.1 三次握手流程
- TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
- TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,ACK=0,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
- TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
- TCP客户进程收到确认后,还要向服务器给出确认。确认报文的SYN=0, ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
- 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
4.5.2 为何采用三次握手
- 确认双方的收发能力
TCP 建立连接之前,需要确认客户端与服务器双方的收包和发包的能力。
-
第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
-
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
-
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
所以,只有三次握手才能确认双方的接收与发送能力是否正常。
- 序列号可靠同步
如果是两次握手,服务端无法确定客户端是否已经接收到了自己发送的初始序列号,如果第二次握手报文丢失,那么客户端就无法知道服务端的初始序列号,那 TCP 的可靠性就无从谈起。
- 阻止重复历史连接的初始化
客户端由于某种原因发送了两个不同序号的 SYN 包,我们知道网络环境是复杂的,旧的数据包有可能先到达服务器。如果是两次握手,服务器收到旧的 SYN 就会立刻建立连接,那么会造成网络异常。
如果是三次握手,服务器需要回复 SYN+ACK 包,客户端会对比应答的序号,如果发现是旧的报文,就会给服务器发 RST 报文,直到正常的 SYN 到达服务器后才正常建立连接。
所以三次握手才有足够的上下文信息来判断当前连接是否是历史连接。
- 安全问题
我们知道 TCP 新建连接时,内核会为连接分配一系列的内存资源,如果采用两次握手,就建立连接,那会放大 DDOS 攻击的。 TCP 作为一种可靠传输控制协议,其核心思想:既要保证数据可靠传输,又要提高传输的效率,而三次握手恰好可以满足以上两方面的需求!
4.6 四次挥手
4.6.1 四次挥手流程
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗ *∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
4.6.2 为何关闭连接需要四次
其实在 TCP 握手的时候,接收端发送 SYN+ACK 的包是将一个 ACK 和一个 SYN 合并到一个包中,所以减少了一次包的发送,三次完成握手。
对于四次挥手,因为 TCP 是全双工通信,在主动关闭方发送 FIN 包后,接收端可能还要发送数据,不能立即关闭服务器端到客户端的数据通道,所以也就不能将服务器端的 FIN 包与对客户端的 ACK 包合并发送,只能先确认 ACK,然后服务器待无需发送数据时再发送 FIN 包,所以四次挥手时必须是四次数据包的交互。
4.6.3 为什么TIME_WAIT 状态需要经过 2MSL 才能返回到 CLOSE 状态
- MSL 指的是报文在网络中最大生存时间。在客户端发送对服务器端的 FIN 的确认包 ACK 后,这个 ACK 包是有可能不可达的,服务器端如果收不到 ACK 的话需要重新发送 FIN 包。所以客户端发送 ACK 后需要留出 2MSL 时间(ACK 到达服务器 + 服务器发送 FIN 重传包,一来一回)等待确认服务器端确实收到了 ACK 包。也就是说客户端如果等待 2MSL 时间也没有收到服务器端的重传包 FIN,说明可以确认服务器已经收到客户端发送的 ACK。
- 避免新旧连接混淆。在客户端发送完最后一个 ACK 报文段后,在经过 2MSL 时间,就可以使本连接持续的时间内所产生的所有报文都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文。
5 DNS
5.1 概念
DNS(Domain Name System,域名系统),作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析。
5.2 解析过程
1、请求一旦发起,浏览器首先要做的事情就是解析这个域名,一般来说,浏览器会首先查看本地硬盘的 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接使用 hosts 文件里面的 ip 地址。
2、如果在本地的 hosts 文件没有能够找到对应的 ip 地址,浏览器会发出一个 DNS请求到本地DNS服务器 。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。
3、查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询。
4、根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。
5、本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
6、最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。
5.3 解析方式
5.3.1 递归查询
概念:客户端只发一次请求,要求对方给出最终结果
对应上图:DNS 客户向本地域名服务器的查询一般都是采用递归查询。
5.3.2 迭代查询
概念:客户端发出一次请求,对方如果没有授权回答,它就会返回一个能解答这个查询的其它名称服务器列表,客户端会再向返回的列表中发出请求,直到找到最终负责所查域名的名称服务器,从它得到最终结果。
对应上图:本地域名服务器向各域名服务器的查询是迭代查询。
二、熟悉 HTTP 结构与通讯原理
1 HTTP 主要特点
1. 简单快速
- 客户向服务器请求服务时,只需传送请求方法和路径。
- 每种方法规定了客户与服务器联系的类型不同。
- 由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
2. 灵活
- HTTP允许传输任意类型的数据对象。
- 正在传输的类型由Content-Type加以标记。
3. 无连接
- 无连接的含义是限制每次连接只处理一个请求。
- 服务器处理完客户的请求,并收到客户的应答后,即断开连接。
- 采用这种方式可以节省传输时间。
4. 无状态
- HTTP协议是无状态协议。
- 无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。
- 在服务器不需要先前信息时它的应答就较快。
5. 支持 B/S 和 C/S 模式
2 HTTP 报文
2.1 报文结构
2.1.1 请求报文
HTTP 请求报文由 请求行(request line)、请求头(header)、空行和请求体四个部分组成
2.1.2 响应报文
HTTP 响应报文由 响应行、响应头、空行和响应体四个部分组成
2.2 报文头分类
2.2.1 通用报文头
请求报文和响应报文两方都会使用到的报文头。
2.2.2 请求报文头
从客户端向服务器发送请求报文时使用的报文头,补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。
2.2.3 响应报文头
从服务器端向客户端返回响应报文时使用的报文头,补充了响应时的附加内容,也会要求客户端附加额外的内容信息。
2.2.4 实体报文头
针对请求报文和响应报文的实体部分使用到的报文头,补充了资源内容更新时间等与实体有关的信息。
3 URI
- URI(Uniform Resource Identifier 统一资源标识符):可以唯一标识一个互联网资源。
- URL(Uniform Resource Locator 统一资源定位符)
- URN(Uniform Resource Name 统一资源名称)
URI是抽象的定义,不管用什么方法表示,只要能定位一个资源,就叫URI,包括:URL(用地址定位)和 URN(用名称定位)
举个例子:去村子找个具体的人(URI),如果用地址:某村多少号房子第几间房的主人 就是URL, 如果用身份证号+名字去找就是URN了。
4 HTTP 请求方法
4.1 幂等
对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。
4.2 请求方法
1. GET
GET请求会显示请求指定的资源,只用于数据的读取。幂等方法。
2. POST
POST请求会向指定资源提交数据,请求服务器进行处理,请求数据会被包含在请求体中。非幂等方法,请求可能会创建新的资源或修改现有资源。
3. PUT
PUT请求会身向指定资源位置上传其最新的完整内容。幂等方法。用得很少。
4. DELETE
DELETE请求用于请求服务器删除所请求URI所标识的资源。幂等方法。用得很少。
5. PATCH
PATCH请求与PUT请求类似,同样用于资源的更新。PATCH一般用于资源的部分更新,而PUT一般用于资源的整体更新。
6. HEAD
HEAD方法与GET方法一样,都是向服务器发出指定资源的请求。但是,服务器在响应HEAD请求时不会回传响应主体。这样,我们可以不传输全部内容的情况下,就可以获取服务器的响应头信息。HEAD方法常被用于客户端查看服务器的性能。
7. OPTIONS
OPTIONS请求与HEAD类似,一般也是用于客户端查看服务器的性能。 这个方法会请求服务器返回该资源所支持的所有HTTP请求方法,该方法会用'*'来代替资源名称,向服务器发送OPTIONS请求,可以测试服务器功能是否正常。JavaScript的XMLHttpRequest对象进行CORS跨域资源共享时,就是使用OPTIONS方法发送嗅探请求,以判断是否有对指定资源的访问权限。
8. CONNECT
CONNECT方法是HTTP/1.1协议预留的,能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。用得很少。
9. TARCE
TRACE请求服务器回显其收到的请求信息,该方法主要用于HTTP请求的测试或诊断。用得很少。
4.3 GET 与 POST 区别
4.3.1 网上答案
- GET在浏览器回退时是无害的,而POST会再次提交请求
- GET产生的URL地址可以被收藏,而POST不可以
- GET请求会被浏览器主动缓存,而POST不会,除非手动设置
- GET请求只能进行url编码,而POST支持多种编码方式
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
- GET请求在URL中传送的参数是有长度限制的,而POST没有
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
- GET参数通过URL传递,POST放在Request body中
4.3.2 自己答案
- 用途不同:GET 是请求资源,POST 是传输资源到服务器
- 幂等性不同:GET 是幂等的,而 POST 是非幂等的
- 参数传递不同:GET 的参数放在 URL 上,有长度限制;而 POST 参数放在 body 中,无长度限制
- 安全性不同:GET 参数直接暴露在 URL 上,而 POST 相对安全
5 HTTP 状态码
5.1 Status: 1XX
指示信息,表示请求已接收,正在处理
- 100 (继续) 请求者应当继续提出请求。 服务器返回此代码表示已收到请求的第一部分,正在等待其余部分
- 101 (切换协议) 请求者已要求服务器切换协议,服务器已确认并准备切换
5.2 Status: 2XX
成功,表示请求已被成功接收
- 200 (成功) 服务器已成功处理了请求
- 204 (无内容) 服务器成功处理了请求,但没有返回任何内容(如Options请求)
- 206 (部分内容) 服务器成功处理了部分 GET 请求(如请求头带 Range、断点续传)
5.3 Status: 3XX
重定向,要完成请求必须要进行更进一步的操作
- 301 永久重定向:在请求的URL已被移除时使用,响应的location首部中应包含资源现在所处的URL
- 302 临时重定向:和永久重定向类似,客户端应用location给出URL临时定位资源,将来的请求仍为原来的URL
- 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容
5.4 Status: 4XX
客户端错误
- 400 (错误请求) 请求存在语法错误
- 401 (未授权) 请求要求身份验证
- 403 (禁止) 服务器拒绝请求
- 404 (未找到) 服务器找不到请求的网页
5.5 Status: 5XX
服务端错误
- 500 (服务器内部错误) 服务器遇到错误,无法完成请求
- 502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应
- 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态
- 504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求
6 HTTP 状态管理
6.1 Cookie + Session 进行状态管理
- Session 是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中
- Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式
由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session。典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。
思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
6.2 Token 进行状态管理
6.2.1 JWT(Json Web Tokens)概念
- Json Web Tokens 是一个开放标准
- 定义了一种紧凑且独立的方式,可以将信息作为 JSON 对象进行安全传输
- 该信息可以验证和信任,因为是经过数字签名的 JWT 通常可以称为 Json 令牌。JWT 中存储的信息是经过数字签名的,因此可以被信任和理解。
6.2.2 JWT组成
由 Header、Payload、signature 三部分组成
6.2.3 验证流程
token 验证是无状态的,服务器不记录哪些用户登录了或者哪些 JWT 被发布了,而是每个请求都带上了服务器需要验证的 token,token 放在了 Authorization header 中,形式是 Bearer { JWT },但是也可以在 post body 里发送,甚至作为 query parameter。
验证流程:
- 用户输入登录信息
- 服务器判断登录信息正确,返回一个 token
- token 存储在客户端,大多数通常在 local storage,但是也可以存储在 session storage 或者 cookie 中。
- 接着发起请求的时候将 token 放进 Authorization header,或者同样可以通过上面的方式。
- 服务器端解码 JWT 然后验证 token,如果 token 有效,则处理该请求。
- 一旦用户登出,token 在客户端被销毁,不需要经过服务器端。
6.3 Token 方式的优势
1.无状态
使用 token 而不是 cookie 的最大优点应该就是无状态,后端不需要保持对 token 的记录,每个 token 都是独立的,包含了检查其有效性的所有数据,并通过申明传达了用户信息。 服务器端的工作只有签署和校验 token。
2.跨域和 CORS
cookie 能很好的处理单域和子域,但是遇到跨域的问题就会变得难以处理。而使用 token 的 CORS 可以很好的处理跨域的问题。由于每次发送请求到后端,都需要检查 JWT,只要它们被验证通过就可以处理请求。
3.在 JWT 中存储数据
当使用 cookie 进行验证时,你是将 session id 存储到 cookie 里,JWT 允许你存储任何类型的元数据,只要是合法的 JSON。你可以在里面添加任何数据,可以只有用户 ID 和到期日,也可以添加其它的比如邮件地址,域名等等。 比如:加入你有一个 API 是 /api/orders ,用于取回最新的订单,但是只有 admin 角色的用户可以获取到这些数据。在基于 cookie 的验证中,一旦请求被创建,就需要先去访问数据库去验证 session 是否正确(现在应该都是存储到 redis 里了,不会存数据库里了),另外还要去获取数据库里的用户权限去校验用户是否拥有 admin 的权限(这个应该是根据用户 role_id 查看权限是否是 admin),最后才是调用订单信息。而使用 JWT 的话,可以将用户角色放进 JWT 内,所以只要验证通过了,就可以直接调用订单信息。
4.移动平台
现代的 API 不仅仅和浏览器交互,正确编写一个 API 可以同时支持浏览器,还有原生移动平台,比如 IOS 或者 Android。原生移动平台并不一定和 cookie 能良好的兼容,在使用中会存在一些限制和需要注意的地方。另一方面,token 更容易在 IOS 和 Android 上实现,Token 也更容易实现物联网应用程序和服务,没有 Cookie 存储的概念。
三、深入认识 HTTP 特性与使用方法
1. 编码与解码
1.1 编码规范
程序员们希望在计算机中显示字符,但计算机只能识别0和1的二进制数,于是国际组织就制定了编码规范,希望使用不同的二进制数来表示不同的字符,这样计算机就可以根据二进制数来显示其对应的字符。
例如:GBK 编码规范,计算机可以在中文字符和二进制数之间相互转换,而使用GBK编码也就可以使计算机显示中文字符。
1.2 编码规范组成
- 字库表
字库表存储了编码规范中能显示的所有字符,计算机根据二进制数从字库表中找到字符然后显示给用户,相当于一个存储字符的数据库。 - 字符集
在一个字库表中,每一个字符都有一个对应的二进制地址,而字符集就是这些地址的集合。
例如:在ASCII编码字符集中,字母A的序号(地址)是65,65的二进制就是01000001。我们可以说编码字符集就是用来存储这些二进制数的,而这个二进制数就是编码字符集中的一个元素,同时它也是字库表中字母A的地址,我们根据这个地址就可以显示出字母A。 - 编码方式
直接使用字符对应的二进制地址来显示文字是十分浪费的,为了区分每个字符,哪怕是00001111这种其实只占了1个字节的字符,也要为他分配4个字节的空间。于是程序员制定了一套算法来节省空间,而每种不同的算法都被称作一种编码方式。
整体流程:一个较短的二进制数,通过一种编码方式,转换成编码字符集中正常的地址,然后在字库表中找到一个对应的字符,最终显示给用户。
1.3 常见编码规范
1.3.1 ASCII
ASCII码,是最早产生的编码规范,一共包含00000000~01111111共128个字符,可以表示阿拉伯数字和大小写英文字母,以及一些简单的符号。ASCII码只需要1个字节的存储空间,最高位为0。它没有特定的编码方式,直接使用地址对应的二进制数来表示。
1.3.2 GBK
GBK支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字。GBK字符集中所有字符占2个字节,不论中文英文都是2个字节。 没有特殊的编码方式,一般在国内,汉字较多时使用。
1.3.3 Unicode
从以上几种编码规范可以看出,各种编码规范互不兼容,且只能表示自己需要的字符,于是,国际标准化组织(ISO)决定制定一套全世界通用的编码规范Unicode。
Unicode包含了全世界所有的字符。Unicode最多可以保存4个字节容量的字符。也就是说,要区分每个字符,每个字符的地址需要4个字节。这是十分浪费存储空间的,于是,程序员就设计了几种字符编码方式,比如:UTF-8,UTF-16,UTF-32。
最广为程序员使用的就是UTF-8,UTF-8是一种变长字符编码,注意:UTF-8不是编码规范,而是编码方式。英文在UTF-8字符编码后只占1个字节,中文占了3个字节。
1.4 编码与解码
一串二进制数,使用一种编码方式,转换成字符,这个过程我们称之为解码。使用错误的编码方式,产生其他不合理的字符,这就是我们通常说的乱码。
一串已经解码后的字符,我们也可以选用任意类型的编码方式重新转换成一串二进制数,这个过程就是编码。无论使用哪一种编码方式进行编码,最终都是产生计算机可识别的二进制数,但如果编码规范的字库表不包含目标字符,则无法在字符集中找到对应的二进制数。这将导致不可逆的乱码!例如:像ISO-8859-1的字库表中不包含中文,因此哪怕将中文字符使用ISO-8859-1进行编码,再使用ISO-8859-1进行解码,也无法显示出正确的中文字符。
1.5 URL 的编解码
URL 采用 ASCII 字符集进行编码
编码规则:
- 对URL中的非保留字符和非不安全字符不进行编码。
- 对URL中的保留字符和不安全字符,需要取其ASCII内码,然后加上%前缀,将该字符进行编码。
- 对URL中的非ASCII字符,需要取其Unicode内码,然后加上%前缀,将该字符进行编码。
2. 身份认证
- HTTP 身份验证:developer.mozilla.org/zh-CN/docs/…
- HTTPS双向认证指南:www.jianshu.com/p/2b2d1f511…
- Basic 认证与 Digest认证:www.cnblogs.com/lsdb/p/1062…
待补充...
3. 长连接与短连接
3.1 概念
- 在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
- 从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行:
Connection:keep-alive。客户端进行一次 HTTP 操作并建立连接后, 再次访问这个服务器上的网页会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在服务器端设定这个时间。实现长连接要客户端和服务端都支持长连接。
3.2 本质
HTTP的长连接和短连接本质上是TCP长连接和短连接。
3.3 优缺点
- 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。但server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。
- 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。
长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。
3.4 管道化
待补充...
4. HTTP 代理
4.1 普通代理
4.1.1 概念
允许一个网络终端(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接
HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端。
4.1.2 补充
- 假如我通过代理访问 A 网站,对于 A 来说,它会把代理当做客户端,完全察觉不到真正客户端的存在,这实现了隐藏客户端 IP 的目的。当然代理也可以修改 HTTP 请求头部,通过 X-Forwarded-IP 这样的自定义头部告诉服务端真正的客户端 IP。但服务器无法验证这个自定义头部真的是由代理添加,还是客户端修改了请求头,所以从 HTTP 头部字段获取 IP 时,需要格外小心。
- 显式指定浏览器代理这种方式一般称之为正向代理,浏览器启用正向代理后,会对 HTTP 请求报文做一些修改,来规避老旧代理服务器的一些问题(如Connection等)
- 还有一种情况是访问 A 网站时,实际上访问的是代理,代理收到请求报文后,再向真正提供服务的服务器发起请求,并将响应转发给浏览器。这种情况一般被称之为反向代理,它可以用来隐藏服务器 IP 及端口。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在了。反向代理是 Web 系统最为常见的一种部署方式。
4.2 隧道代理
4.2.1 概念
HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。
4.2.2 补充
- 假如我通过代理访问 A 网站,浏览器首先通过 CONNECT 请求,让代理创建一条到 A 网站的 TCP 连接;一旦 TCP 连接建好,代理无脑转发后续流量即可。所以这种代理,理论上适用于任意基于 TCP 的应用层协议,HTTPS 网站使用的 TLS 协议当然也可以。这也是这种代理为什么被称为隧道的原因。对于 HTTPS 来说,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,所以依然是安全的
- https相关说明待补充...
5. 网关(gateway)
5.1 概念
网关可以作为某种翻译器使用,它抽象出了一种能够到达资源的方法。网关是资源和应用程序之间的粘合剂,扮演的是协议转换器的角色。
5.2 表示方式
表示方式:<客户端协议>/<服务器端协议>
- HTTP/*: 服务器端网关通过HTTP与客户端对话,通过其他协议与服务器通信;
- */HTTP: 客户端网关通过其他协议与客户端对话,通过HTTP与服务器通信
5.3 常见网关
5.3.1 HTTP/*:服务器端Web网关
请求流入原始服务器时,服务器端Web网关会将客户端HTTP请求转换为其他协议
5.3.2 HTTP/HTTPS:服务器端安全网关
通过网关对所有的输入Web请求加密,以提供额外的隐私和安全性保护。客户端可以用普通的HTTP浏览Web内容,但网关会自动加密用户的对话
5.3.3 HTTPS/HTTP:客户端安全加速器网关
可以将HTTPS/HTTP网关作为安全加速器使用,这些HTTPS/HTTP网关位于Web服务器之前,通常作为不可见的拦截网关或反向代理使用。它们接收安全的HTTPS流量,对安全流量进行解密,并向Web服务器发送普通的HTTP请求。这些网关中通常都包含专用的解密硬件,以比原始服务器有效得多的方式来解密安全流量,以减轻原始服务器的负荷。这些网关在网关和原始服务器之间发送的是未加密的流量。所以,要谨慎使用,确保网关和原始服务器之间的网络是安全的
5.3.4 资源网关
最常见的网关——应用程序服务器,会将目标服务器与网关结合在一个服务器中实现。应用程序服务器是服务器端网关,与客户端通过HTTP进行通信,并与服务器端的应用程序相连
第一个流行的应用程序网关API就是通用网关接口(Common Gateway Interface, CGI)。CGI是一个标准接口集,Web服务器可以用它来装载程序以响应对特定URL的HTTP请求,并收集程序的输出数据,将其放在HTTP响应中回送
6. HTTP 缓存
6.1 HTTP缓存策略
HTTP缓存策略只是为了解决客户端和服务端信息不对称的问题而存在的,客户端为了加快速度会缓存部分资源,但是下次请求时,客户端不知道这个资源有没有更新,服务端也不知道客户端缓存的是哪个版本,不知道该不该再返回资源,其实就是一个信息同步问题,HTTP缓存策略就是来解决这个问题的
6.2 HTTP缓存流程图
6.3 强制缓存
6.3.1 概念
强制缓存就是直接从浏览器缓存查找该结果,并根据结果的缓存规则来决定是否使用该缓存的过程。
6.3.2 Expires
Expires 是 http 1.0 的规范,值是一个 GMT 格式的时间点字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT 。这个时间点代表资源失效的时间,如果当前的时间戳在这个时间之前,则判定命中缓存。有一个缺点是,失效时间是一个绝对时间,如果服务器时间与客户端时间偏差较大时,就会导致缓存混乱。而服务器的时间跟用户的实际时间是不一样是很正常的,所以 Expires 在实际使用中会带来一些麻烦。
6.3.3 Cache-Control
Cache-Control 这个字段是 http 1.1 的规范,一般常用该字段的 max-age 值来进行判断,它是一个相对时间,比如 Cache-Control:max-age=3600 代表资源的有效期是 3600 秒。并且返回头中的 Date 表示消息发送的时间,表示当前资源在 Date ~ Date +3600s 这段时间里都是有效的。
no-cache不使用强制缓存, 需要使用协商缓存no-store所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存public所有内容都将被缓存(客户端/代理服务器/CDN等)private只有客户端可以缓存,Cache-Control 默认值max-age=xxx缓存将在 xxx 秒后失效
6.4 协商缓存
6.4.1 概念
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,服务器根据缓存标识决定是否使用缓存的过程。
主要涉及到两对属性字段,都是成对出现的,即第一次请求的响应头带上 Last-Modified 或 Etag,则后续请求则会带上对应的请求字段 If-Modified-Since 或 If-None-Match
6.4.2 Last-Modifed / If-Modified-Since
Last-Modified: 响应头,资源最新修改时间,由服务器告诉浏览器If-Modified-Since:客户端再次发起该请求时,携带上次请求返回的Last-Modified值,告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,会根据If-Modified-Since字段值与该资源在服务器的最后被修改时间做对比,若文件修改时间在If-Modified-Since后,则重新返回资源,状态码为200;否则返回304,代表资源无更新,可以继续使用缓存文件。
6.4.3 Etag / If-None-Match
Etag: 响应头,资源的唯一标识串,由服务器告诉浏览器If-None-Match: 客户端再次发起该请求时,携带上次请求返回的Etag值。服务端接收到If-None-Match字段以后,通过比较资源的唯一标识串是否一致来判定文件内容是否被改变。若改变则重新返回资源,状态码为200;否则返回304,代表资源无更新,可以继续使用缓存文件。
6.4.4 Etag 的优势
- 一些文件也许会周期性的更改,但是内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
- 某些文件修改非常频繁(比方说1s内修改了N次) ,
If-Modified-Since因为 能检查到的粒度是秒级的而产生问题。而Etag就能够保证这种需求下客户端在1秒内能刷新 N 次 cache。 - 某些服务器不能精确的得到文件的最后修改时间。
6.5 缓存改进方案
6.5.1 md5/hash缓存
通过不缓存html,为静态文件添加 MD5 或 hash 标识,解决浏览器无法跳过缓存过期时间内主动感知文件变化的问题
6.5.2 CDN 缓存
CDN 是内容分发网络,依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥堵。
浏览器检查到强制缓存过期,则向CDN边缘节点发起请求,CDN边缘节点会检测用户请求数据的缓存是否过期,如果没有过期,则直接响应用户请求,此时一个完成http请求结束;如果数据已经过期,那么CDN还需要向源站发出回源请求(back to the source request),来拉取最新的数据。
7 内容协商机制
7.1 概念
一份特定的文件称为一项资源。当客户端获取资源的时候,会使用其对应的 URL 发送请求。服务器通过这个 URL 来选择它指向的资源的某一变体——每一个变体称为一种展现形式——然后将这个选定的展现形式返回给客户端。整个资源,连同它的各种展现形式,共享一个特定的 URL 。当一项资源被访问的时候,特定展现形式的选取是通过内容协商机制来决定的,并且客户端和服务器端之间存在多种协商方式。
7.2 分类
7.2.1 服务端驱动(主流)
服务器检查客户端的请求头并决定提供哪个版本的资源
7.2.2 客户端驱动(用的不多)
客户端发起请求,服务端返回资源可选列表,客户端做出选择后发出第二个选择请求想要的资源
7.2.3 透明代理(基本不使用)
某个中间设备代表服务端进行协商
7.3 涉及到的请求头与响应头
7.3.1 请求头
Accept:告知服务器发送何种媒体类型Accept-Language:告知服务器发送何种语言Accept-Charset:告知服务器发送何种字符集Accept-Encoding:告知服务器采用何种编码
7.3.2 响应头
Content-Type:返回的媒体类型Content-Language:返回的语言Content-Charset:返回的字符集Content-Encoding:返回的编码
7.4 近似匹配
HTTP 协议中定义了质量值,允许客户端为每种偏好类别列出多种选项,并为每种偏好选项关联一个优先次序。例如,客户端可以发送下列形式的Accept-Language首部,其中 q 值的范围从0.0-1.0
Accept-Language: zh; q=1.0, en; q=0.5, nl; q=0.2, tr; q=0.0
8 断点续传
- 请求头:
Range,If-Range - 响应头:
Accept-Ranges,Content-Range
8.1 是否支持范围请求
而在 HTTP/1.1 中,很明确的声明了一个响应头部 Access-Ranges 来标记是否支持范围请求,它只有一个可选参数 bytes。例如这里给了一个 MP4 的响应头,可以看到它是有 Accept-Ranges:bytes 来标记的,有此标记标识当前资源支持范围请求。
8.2 使用范围请求
如果已经确定双端都支持范围请求,我们就可以在请求资源的时候使用它。
所有的文件最终都是存储在磁盘或者内存中的字节,对于待操作的文件可以将其以字节为单位分割。这样只需要 HTTP 支持请求该文件从 n 到 n+x 这个范围内的资源,就可以实现范围请求了。
HTTP/1.1 中定义了一个 Ranges 的请求头,来指定请求实体的范围。它的范围取值是在 0 - Content-Length 之间,使用 - 分割。例如已经下载了 1000 bytes 的资源内容,想接着继续下载之后的资源内容,只要在 HTTP 请求头部,增加 Ranges:bytes=1000- 就可以了。
Range 还有几种不同的方式来限定范围,可以根据需要灵活定制:
- 500-1000:指定开始和结束的范围,一般用于多线程下载。
- 500- :指定开始区间,一直传递到结束。这个就比较适用于断点续传、或者在线播放等等。
- -500:无开始区间,只意思是需要最后 500 bytes 的内容实体。
- 100-300,1000-3000:指定多个范围,这种方式使用的场景很少,了解一下就好了。
HTTP 协议是一种双边协商的协议,既然请求头部已经确定是使用 Ranges 了,还有响应头部中,也需要使用 Content-Ragne 这个响应头来标记响应的实体内容范围。
Content-Range 的格式也很清晰,首先标记它的单位是 bytes 然后标记当前传递的内容实体范围和总长度。
Content-Range: bytes 100-999/1000
8.3 资源变化
当我们在一些下载工具中,下载大尺寸资源的时候,偶尔中间暂停过再重新下载,可能会遇见它又重头开始下载的情况。
这看似是 HTTP 的范围请求失效了,但是实际上并不一定如此,很可能是因为请求的资源,在请求的这个过程中,发生了改变。
假如你下载的过程中,下载的源资源文件发生了变化,但是 URL 没有改变,此时文件长度可能已经变化了(这是非常容易发现的),极端情况下就算没有长度没有变化,你再继续下载,很可能最终下载完成之后,无法将下载的内容拼接成我们需要的文件。
在 HTTP 的范围请求中,可以使用ETag 或 Last-Modified 来区分分段请求的资源是否有修改过,只需要在请求头中,将它放在 If-Range 这个请求报文头中即可。此时,如果两次操作的都是同一个资源文件,就会继续返回 206 状态码,开始后续的操作,反之则会返回 200 状态码,表示文件发生改变,要从头下载。
8.4 总结与流程
- HTTP 范围请求,需要 HTTP/1.1 及之上支持,如果双端某一段低于此版本,则认为不支持。
- 通过响应头中的
Accept-Ranges来确定是否支持范围请求。 - 通过在请求头中添加
Range这个请求头,来指定请求的内容实体的字节范围。 - 在响应头中,通过
Content-Range来标识当前返回的内容实体范围,并使用Content-Length来标识当前返回的内容实体范围长度。 - 在请求过程中,可以通过
If-Range来区分资源文件是否变动,它的值来自ETag或Last-Modifled。如果资源文件有改动,会重新走下载流程。
三、HTTPS
3.1 HTTP 的最大弊端——不安全
HTTP 在传输数据的过程中,所有的数据都是明文传输,自然没有安全性可言,特别是一些敏感数据,比如用户密码和信用卡信息等。要想让 HTTP 更安全,只能使用真正的加密算法,因为加密算法可以用密钥加密或还原数据,只要确保密钥不被第三方获取,那就能确保数据传输的安全了,而这正是 HTTPS 的解决方案。
3.2 加密算法
HTTPS 解决数据传输安全问题的方案就是使用加密算法,具体来说是混合加密算法,也就是对称加密和非对称加密的混合使用,这里有必要先了解一下这两种加密算法的区别和优缺点。
3.2.1 对称加密
对称加密,顾名思义就是加密和解密都是使用同一个密钥,常见的对称加密算法有 DES、3DES 和 AES 等
- 优点:算法公开、计算量小、加密速度快、加密效率高,适合加密比较大的数据。
- 缺点:交易双方需要使用相同的密钥,也就无法避免密钥的传输,而密钥在传输过程中无法保证不被截获,因此对称加密的安全性得不到保证
被加密的数据在传输过程中是无规则的乱码,即便被第三方截获,在没有密钥的情况下也无法解密数据,也就保证了数据的安全。但是有一个致命的问题,那就是既然双方要使用相同的密钥,那就必然要在传输数据之前先由一方把密钥传给另一方,那么在此过程中密钥就很有可能被截获,这样一来加密的数据也会被轻松解密。
3.2.2 非对称加密
非对称加密,顾名思义,就是加密和解密需要使用两个不同的密钥:公钥(public key)和私钥(private key)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公钥对外公开;得到该公钥的乙方使用公钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的私钥对加密后的信息进行解密。常用的非对称加密算法是 RSA 算法,其优缺点如下:
- 优点:算法公开,加密和解密使用不同的钥匙,私钥不需要通过网络进行传输,安全性很高。
- 缺点:计算量比较大,加密和解密速度相比对称加密慢很多。
在上述过程中,客户端在拿到服务器的公钥后,会生成一个随机码 (用 KEY 表示,这个 KEY 就是后续双方用于对称加密的密钥),然后客户端使用公钥把 KEY 加密后再发送给服务器,服务器使用私钥将其解密,这样双方就有了同一个密钥 KEY,然后双方再使用 KEY 进行对称加密交互数据。在非对称加密传输 KEY 的过程中,即便第三方获取了公钥和加密后的 KEY,在没有私钥的情况下也无法破解 KEY (私钥存在服务器,泄露风险极小),也就保证了接下来对称加密的数据安全。而上面这个流程图正是 HTTPS 的雏形,HTTPS 正好综合了这两种加密算法的优点,不仅保证了通信安全,还保证了数据传输效率。
3.3 HTTPS 定义
Hypertext Transfer Protocol Secure (HTTPS) is an extension of the Hypertext Transfer Protocol (HTTP). It is used for secure communication over a computer network, and is widely used on the Internet. In HTTPS, the communication protocol is encrypted using Transport Layer Security (TLS) or, formerly, its predecessor, Secure Sockets Layer (SSL). The protocol is therefore also often referred to as HTTP over TLS, or HTTP over SSL.
HTTPS (Hypertext Transfer Protocol Secure) 是基于 HTTP 的扩展,用于计算机网络的安全通信,已经在互联网得到广泛应用。在 HTTPS 中,原有的 HTTP 协议会得到 TLS (安全传输层协议) 或其前辈 SSL (安全套接层) 的加密。因此 HTTPS 也常指 HTTP over TLS 或 HTTP over SSL。
可见HTTPS 并非独立的通信协议,而是对 HTTP 的扩展,保证了通信安全,二者关系如下,也就是说 HTTPS = HTTP + SSL / TLS
3.4 HTTPS 加解密过程
- 客户端请求 HTTPS 网址,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的80端口)。
- 采用 HTTPS 协议的服务器必须要有一套数字 CA (Certification Authority)证书,证书是需要申请的,并由专门的数字证书认证机构(CA)通过非常严格的审核之后颁发的电子证书。颁发证书的同时会产生一个私钥和公钥。私钥由服务端自己保存,不可泄漏。公钥则是附带在证书的信息中,可以公开的。证书本身也附带一个证书电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。
- 服务器响应客户端请求,将证书传递给客户端,证书包含公钥和大量其他信息,比如证书颁发机构信息,公司信息和证书有效期等。Chrome 浏览器点击地址栏的锁标志再点击证书就可以看到证书详细信息。
- 客户端解析证书并对其进行验证。如果证书没有问题,客户端就会从服务器证书中取出服务器的公钥A。然后客户端还会生成一个随机码 KEY,并使用公钥A将其加密。但如果证书不是可信机构颁布,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。就像下面这样:
- 客户端把加密后的随机码 KEY 发送给服务器,作为后面对称加密的密钥。
- 服务器在收到随机码 KEY 之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,接下来就可以用对称加密愉快地进行通信了。
- 服务器使用密钥 (随机码 KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥 (随机码 KEY)解密数据。
- 双方使用对称加密愉快地传输所有数据。
3.5 HTTPS 与 HTTP 区别
- 最最重要的区别就是安全性,HTTP 明文传输,不对数据进行加密安全性较差。HTTPS (HTTP + SSL / TLS)的数据传输过程是加密的,安全性较好。
- 使用 HTTPS 协议需要申请 CA 证书,一般免费证书较少,因而需要一定费用。证书颁发机构如:Symantec、Comodo、DigiCert 和 GlobalSign 等。
- HTTP 页面响应速度比 HTTPS 快。
- 由于 HTTPS 是建构在 SSL / TLS 之上的 HTTP 协议,所以,要比 HTTP 更耗费服务器资源。
- HTTPS 和 HTTP 使用的是完全不同的连接方式,用的端口也不一样,前者是 443,后者是 80。
3.6 HTTPS 使用成本
- 证书费用以及更新维护
- 降低了用户的访问速度
- 需要更多的服务器资源,导致成本升高
3.7 HTTPS 详细握手过程
1. TCP 三次握手
2. 客户端发送client_hello
- TLS 版本信息
- 随机数(用于后续的密钥协商)random_C
- 加密套件候选列表
- 压缩算法候选列表
- 扩展字段
- 其他
3. 服务端发送server_hello
服务端收到客户端的client_hello之后,发送server_hello,并返回协商的信息结果:
- 选择使用的 TLS 协议版本 version
- 随机数 random_S
- 选择的加密套件 cipher suite
- 选择的压缩算法 compression method
- 其他
4. 服务端发送证书
服务端发送完server_hello后,紧接着开始发送自己的证书。例如由图可知:因包含证书的报文长度是3761,所以此报文在tcp这块做了分段,分了3个报文把证书发送完了[tcp分片]
5. 服务端发送Server Key Exchange(可选)
视加密算法而定,不一定会发送
6. 服务端发送 Server Hello Done
通知客户端 server_hello 信息发送结束
7. 客户端发送 client_key_exchange+change_cipher_spec+encrypted_handshake_message
- client_key_exchange,合法性验证通过之后,向服务器发送自己的公钥参数,这里客户端实际上已经计算出了密钥
- change_cipher_spec,客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信
- encrypted_handshake_message,主要是用来测试密钥的有效性和一致性
8. 服务端发送New Session Ticket
服务器给客户端一个会话,用处就是在一段时间之内(超时时间到来之前),双方都以协商的密钥进行通信。
9. 服务端发送 change_cipher_spec
服务端解密客户端发送的参数,然后按照同样的算法计算出协商密钥,并通过客户端发送的encrypted_handshake_message验证有效性,验证通过,发送该报文,告知客户端,以后可以拿协商的密钥来通信了
10. 服务端发送encrypted_handshake_message
目的同样是测试密钥的有效性,客户端发送该报文是为了验证服务端能正常解密,客户端能正常加密,相反:服务端发送该报文是为了验证客户端能正常解密,服务端能正常加密
11. 完成密钥协商,开始发送数据
数据同样是分段发送的
12. 完成数据发送,4次tcp挥手
3.8 补充
3.8.1 TCP分段与IP分片参考资料
TCP报文段如果很长的话,会在发送时发生分段(Segmentation),在接收时进行重组;同样IP数据报在长度超过一定值时也会发生分片(Fragmentation)。分段与分片都是为了能够传输上层交付的、数据量超过本层传输能力上限的数据,不得已才做的数据切分。
- 分段特指发生在使用TCP协议的传输层中的数据切分行为
- 分片特指发生在使用IPv4协议的网络IP层中的数据切分行为
3.8.2 最大传输单元 MTU(Maximum Transmission Unit)
数据链路层的最大载荷上限(即 IP 数据报最大长度),每段链路的 MTU 可能都不相同,一条端到端路径的 MTU 由这条路径上 MTU 最小的那段链路的 MTU 决定。
MTU 是链路层中的网络对数据帧的一个限制,以以太网为例,MTU 通常为 1500 字节。所谓的 MTU,是二层协议的一个限制,对不同的二层协议可能有不同的值,只有二层协议为以太网(Ethernet)时,MTU一般才取1500字节,注意它不是物理链路介质的限制,只有工作在二层的设备才需要指定MTU的值,如网卡、转发设备端口(统称为网络接口)等,通过同一段线缆直连的通信端口或网卡,其MTU值一定相同。
一个IP数据报在以太网中传输,如果它的长度大于当前链路 MTU 值,就要进行分片传输(这里指 IP 层分片),使得每片数据报的长度都不超过 MTU。分片传输的 IP 数据报不一定按序到达,但 IP 首部中的信息能让这些数据报片按序组装。IP 数据报的分片与重组是在网络 IP 层完成的。
3.8.3 最大报文段长度 MSS (Maximum Segment Size)
TCP传输层的最大载荷上限(即应用层数据最大长度),TCP 三次握手期间通过 TCP 首部选项中的 MSS 字段通知对端,通常一条 TCP 连接的 MSS 取通信双方较小的那一个 MSS 值,与 MTU 的换算关系为:
MTU = MSS + TCP首部长度 + IP首部长度
故在以太网中(网络层以IPv4为例):
MSS = 以太网MTU - TCP首部长度 - IPv4首部长度 = 1500 - 20 - 20 = 1460字节
未指定 MSS 时默认值为 536 字节,这是因为在 Internet 中标准的 MTU 值为576字节,576字节 MTU = TCP首部长度20字节 + IPv4首部长度20字节 + 536字节MSS。
一个应用程序如果要发送超过MSS大小的数据,就要进行分段传输(这里指TCP分段),使得每个报文段长度都不超过MSS。分片传输的TCP报文段不一定按序到达,但实现可靠传输的TCP协议中有处理乱序的机制,即利用报文段序列号在接收缓冲区进行数据重排以实现重组。TCP分段的重组是在TCP传输层完成的。
3.8.4 分段与分片
发送端进行TCP分段后就一定不会在IP层进行分片,因为MSS本身就是基于MTU推导而来,TCP层分段满足了MSS限制,也就满足了MTU的物理限制。但在TCP分段发生后仍然可能发生IP分片,这是因为TCP分段仅满足了通信两端的MTU要求,传输路径上如经过MTU值比该MTU值更小的链路,那么在转发分片到该条链路的设备中仍会以更小的MTU值作为依据再次分片。当然如果两个通信主机直连,那么TCP连接协商得到的MTU值(两者网卡MTU较小值)就是端到端的路径MTU值,故发送端只要做了TCP分段,则在整个通信过程中一定不会发生IP分片。
四、基于 HTTP 的功能追加协议
4.1 HTTP 的瓶颈
- 线头阻塞:TCP连接上只能发送一个请求,前面的请求未完成前,后续的请求都在排队等待
- 多个TCP连接:虽然HTTP/1.1管线化可以支持请求并发,但是浏览器很难实现,chrome、firefox等都禁用了管线化。所以1.1版本请求并发依赖于多个TCP连接,建立TCP连接成本很高,还会存在慢启动的问题
- 请求只从客户端开始
- 请求和响应头部不经压缩就发送
- 每次互相发送相同的头部造成的浪费较多
4.2 WebSocket
4.2.1 概念
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。
在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。
4.2.2 特性
- 支持双向通信,实时性更强
- 灵活高效
- 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小
4.2.3 如何建立 WebSocket 连接
1、客户端:申请协议升级
首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法。
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
重点请求首部意义如下:
Connection: Upgrade:表示要升级协议Upgrade: websocket:表示要升级到websocket协议。Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
2、服务端:响应协议升级
服务端返回内容如下,状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
3、Sec-WebSocket-Accept 的计算
Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。 计算公式为:
- 将
Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。 - 通过SHA1计算出摘要,并转成base64字符串。
伪代码如下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
验证下前面的返回结果:
const crypto = require('crypto');
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const secWebSocketKey = 'w4v7O6xFTi36lq3RNcgctw==';
let secWebSocketAccept = crypto.createHash('sha1')
.update(secWebSocketKey + magic)
.digest('base64');
console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
4.2.4 Sec-WebSocket-Key/Accept的作用
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用大致归纳如下:
- 避免服务端收到非法的websocket连接
- 确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。
- 用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade)
- 可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)。
- Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。
强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,其实并没有实际性的保证。
4.2.5 应用场景
- 即时聊天通信
- 多玩家游戏
- 在线协同编辑/编辑
- 实时数据流的拉取与推送
- 体育/游戏实况
- 实时地图位置
4.3 SPDY(已被 http2 取代)
其实 SPDY 并不是新的一种协议,而是在 HTTP 之前做了一层会话层。
SPDY的一些改进:
- 多路复用,请求优化
- 支持服务器推送技术
- 压缩了 HTTP 头
- 强制使用 SSL 传输协议
- 可以设置资源的优先级
4.4 HTTP2.0
HTTP2.0移植了很多 SPDY 中的功能
- 流(Stream):已建立的TCP连接上的双向字节流,可以承载一个或多个消息。
- 消息(Message):一个完整的HTTP请求或响应,由一个或多个帧组成。特定消息的帧在同一个流上发送,这意味着一个HTTP请求或响应只能在一个流上发送。
- 帧(Frame):通信的基本单位。 一个TCP连接上可以有任意数量的流。
4.4.1 二进制分帧层
HTTP2性能提升的核心就在于二进制分帧层。HTTP2是二进制协议,他采用二进制格式传输数据而不是1.x的文本格式。1.1响应是文本格式,而2.0把响应划分成了两个帧,图中的HEADERS(首部)和DATA(消息负载)是帧的类型。也就是说一条HTTP响应,划分成了两个帧来传输,并且采用二进制来编码。
4.4.2 多路复用
上面提到HTTP/1.1的线头阻塞和多个TCP连接的问题,而HTTP/2的多路复用则允许同时通过单一的TCP 连接发起并发的多个的请求-响应消息。HTTP2建立一个TCP连接,一个连接上面可以有任意多个并发的流(stream),消息分割成一个或多个帧在流里面传输。我们可以向对方不断发送帧,每帧的 stream identifier 标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。所以 http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接。
多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞,所以需要优先级机制。http2.0里的每个流都可以设置优先级(Priority),优先级高的流会被服务端优先处理和返回给客户端。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
4.4.3 首部压缩(Header Compression)
在1.X版本中,首部用文本格式传输,通常会给每个传输增加500-800字节的开销。现在打开一个网页上百个请求已是常态,而每个请求带的一些首部字段都是相同的,例如cookie、user-agent等。HTTP2为此采用 HPACK 压缩格式来压缩首部。头部压缩需要在浏览器和服务器端之间:
- 维护一份相同的静态字典,包含常见的头部名称,以及常见的头部名称和值的组合
- 维护一份相同的动态字典,可以动态的添加内容
- 通过静态 Huffman 编码对传输的首部字段进行编码
HTTP2 的部分静态字典:
所以我们在传输首部字段的时候,例如要传输method:GET,那我们只需要传输静态字典里面method:GET对应的索引值就可以了,一个字节搞定。像user-agent、cookie这种静态字典里面只有首部名称而没有值的首部,第一次传输需要user-agent在静态字典中的索引以及他的值,值会采用静态 Huffman 编码来减小体积。
第一次传输过user-agent 之后,浏览器和服务器端就会把它添加到自己的动态字典中。后续传输就可以传输索引了,一个字节搞定。
4.4.4 服务端推送(Server Push)
服务器端推送使得服务器可以预测客户端需要的资源,主动推送到客户端。 例如:客户端请求index.html,服务器端能够额外推送script.js和style.css。 实现原理就是客户端发出页面请求时,服务器端能够分析这个页面所依赖的其他资源,主动推送到客户端的缓存,当客户端收到原始网页的请求时,它需要的资源已经位于缓存。
针对每一个希望发送的资源,服务器会发送一个PUSH_PROMISE帧,客户端可以通过发送RST_STREAM帧来拒绝推送(当资源已经位于缓存)。这一步的操作先于父响应(index.html),客户端了解到服务器端打算推送哪些资源,就不会再为这些资源创建重复请求。当客户端收到index.html的响应时,script.js和style.css已经位于缓存。
4.5 QUIC&HTTP3.0
4.5.1 概念与背景
1. 什么是 QUIC
QUIC ,即快速UDP网络连接 (Quick UDP Internet Connections ), 是由谷歌提出的实验性网络传输协议 ,位于OSI模型的传输层。 QUIC旨在解决TCP协议的缺陷,并最终替代TCP协议, 以减少数据传输,降低连接建立延迟时间,加快网页传输速度。
2. 为什么选择 UDP
- UDP无连接(没有建立连接和结束连接的成本)
- UDP数据无序,且数据报之间没有关联(无队头阻塞问题)
- UDP协议简单(修改成本低)
UDP的几个特点都完全满足我们的需求,而唯一的缺陷就是UDP无法像TCP一样具有可靠性。因此谷歌决定中UDP的基础上改造出一个具备TCP优点的新协议——QUIC协议。
3. 了解 HTTP3
运行在 QUIC 之上的 HTTP 协议被称为 HTTP/3 (HTTP-over-QUIC)。QUIC 协议基于 UDP,同时 QUIC 也整合了 TCP、TLS 和 HTTP/2 的优点,并加以优化。
- 特点:
- 减少了握手的延迟(1-RTT 或 0-RTT)
- 多路复用,并且没有 TCP 的阻塞问题
- 连接迁移,(主要是在客户端)当由 Wifi 转移到 4G 时,连接不会被断开
- 集成了 TLS 1.3 加密
- HTTP/3 与 HTTP/1.1 和 HTTP/2 没有直接的关系,也不是 HTTP/2 的扩展
- HTTP/3 将会是一个全新的 WEB 协议
- HTTP/3 目前处于制订和测试阶段
4.5.2 多路复用
队头阻塞其实是计算机网络中一个很常见的问题,它的主要表现就是一个数据包影响了一堆数据包,如果它没有到来就会一直阻塞影,从而影响其他的数据包。这个问题在HTTP1.x和TCP中都会出现。
在HTTP1.1引入管线化机制时,由于其通过队列来管理请求,如果响应的顺序与请求不一致,则此时就会阻塞在队首。为了解决这个问题,在HTTP2.0中引入了多路复用机制,通过在头部中标识了对应的请求信息,解决了队首阻塞问题,提高了信道的利用率。
虽然HTTP解决了队头阻塞问题,但是TCP层仍然存在队头阻塞问题。TCP协议为了确保将数据有序交付给上层,其需要等待所有数据到来并排序整合,一旦某个包丢失,就必须等待其重传,从而阻塞整个连接的数据使用。
为了解决这个问题,QUIC采用了多路复用的思想,一个连接上可以同时承载多个流,可以同时发起多个请求,同时这些请求之间完全独立,某个请求的阻塞不会影响到其他的请求。
4.5.3 低等待延迟(0RTT)
RTT(Round-Trip Time)即数据包一来一回的时间消耗,是衡量网络建立连接的常用指标,其包含三部分(往返传播时延、网络设备内排队时延、应用程序数据处理时延)。
对于传统的 HTTPS 协议 3RTT 不同,QUIC 可以达成 0RTT 建立连接,第一个数据包就可以传输数据。
4.5.4 连接方式
QUIC 的 0RTT 并不是无条件的,因为对于第一次交互的双方来说,还需要使用 1RTT 进行密钥协商(DH算法)。因此 QUIC 的建连主要分为首次连接和非首次连接两种情况。
4.5.4.1 首次连接
其主要内容就是客户端和服务端的密钥协商和数据传输。基本流程如下:
- 客户端对于首次连接的服务端先发送client hello请求。 2, 服务端生成一个素数p和一个整数g,同时生成一个随机数为私钥,然后计算出公钥。服务端将公钥,p,g三个元素打包成为config,后续发送给客户端。
- 客户端随机生成一个自己的私钥,再从config中读取g和p,计算出自己的公钥。
- 客户端使用自己的私钥和服务端发来的config中读取的服务端公钥,生成后续数据加密用的密钥K。
- 客户端使用密钥K加密业务数据,并追加自己的公钥,都传递给服务端。
- 服务端根据自己的私钥和客户端公钥生成客户端加密用的密钥K。
- 为了保证数据安全,上述生成的密钥K只会生成使用1次,后续服务端会按照相同的规则生成一套全新的公钥和私钥,并使用这组公私钥生成新的密钥M。
- 服务端将新公钥和新密钥M加密的数据发给客户端,客户端根据新的服务端公钥和自己原来的私钥计算出本次的密钥M,进行解密。
- 之后的客户端和服务端数据交互都使用密钥M来完成,密钥K只使用1次。
4.5.4.2 非首次连接
在首次连接中客户端存储了服务端的config(服务端公钥、随机素数p、随机整数g),因此在后续的连接中可以直接使用config计算出通信的密钥,从而跳过了密钥协商的1RTT,实现了0RTT的数据交互。
为了确保安全,客户端保存config是有时间期限的,因此在config失效之后仍然需要进行首次连接的密钥交换。
4.5.5 前向安全
前向安全(Forward Secrecy),是密码学中通讯协议的安全属性,指的是长期使用的主密钥泄漏不会导致过去的会话密钥泄漏。 前向安全能够保护过去进行的通讯不受密码或密钥在未来暴露的威胁。如果系统具有前向安全性,就可以保证在主密钥泄露时历史通讯的安全,即使系统遭到主动攻击也是如此。
简单来说,前向安全指的是密钥泄漏也不会让之前加密的数据被泄漏,影响的只有当前,对之前的数据无影响。
从上面DH加密的流程中我们可以看到,为了保证数据安全,每次通信时使用的密钥只会使用一次,每次交互完都会将其销毁,后续再按照相同的规则重新生成。即使密钥泄漏出去,对方也只能获取当对应密钥的那条信息,而对其他的数据不会有任何影响。
4.5.6 前向纠错
前向纠错是一种差错控制方式,它是指信号在被送入传输信道之前预先按一定的算法进行编码处理,加入带有信号本身特征的冗码,在接收端按照相应算法对接收到的信号进行解码,从而找出在传输过程中产生的错误码并将其纠正的技术
当出现丢包时,TCP采用的是重传机制,而QUIC采用的是前向纠错机制。
- 对于TCP来说,如果发生丢包,则需要一个等待延时判断是否发生了丢包,然后再启动重传机制,这个过程会造成一定的阻塞,影响传输的时间。
- QUIC每发送一组数据就对这组数据进行异或运算,并将结果作为一个校验和包发送出去。如果前面这组数据中出现了丢包的情况,就可以通过校验和与其他包来还原出丢包的数据,避免了重传带来的损耗。
4.5.7 连接迁移
在当前的移动网络环境下,用户的网络随时都有可能发生切换,比如从移动网络切换到WIFI环境,此时我们的IP地址就会发生变化。由于TCP协议使用五元组(源IP,源端口,目的IP,目的端口,传输层协议) 来唯一表示一条连接,因此之前的连接就不可能继续保持,就需要重新建立TCP连接。
为了解决这个问题,基于UDP实现的QUIC完全摒弃了五元组的概念,其通过生成一个64位的随机数作为连接的标识,即使当我们发生了IP地址的切换,这个标识也不会发生任何变化,我们就可以快速的恢复连接,大大的改善了移动端应用的体验。
五、WEB 安全解析
5.1 SQL 注入
因为参数化查询可以重用执行计划,并且如果重用执行计划的话,SQL所要表达的语义就不会变化,所以就可以防止SQL注入,如果不能重用执行计划,就有可能出现SQL注入。
5.2 XSS(跨站脚本攻击)
概述
XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。
XSS 的本质:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
分类
反射型 XSS
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
存储型 XSS
存储型XSS漏洞的成因与反射型的根源类似,不同的是恶意代码会被保存在服务器中,导致其它用户(前端)和管理员(前后端)在访问资源时执行了恶意代码。
攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM型 XSS
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
XSS 的预防
XSS 攻击有两大要素:
- 用户提交恶意代码
- 浏览器执行恶意代码
输入过滤
后端在写入数据库前,对输入进行过滤,然后把“安全的”内容,返回给前端。但是存在问题:在提交阶段,我们并不确定内容要输出到哪里,转义后格式混乱。所以输入过滤只在校验手机号、邮箱等情况下适用。
HTML 转义
如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。
纯前端渲染
纯前端渲染的过程:
- 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
- 然后浏览器执行 HTML 中的 JavaScript。 JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。 但纯前端渲染还需注意避免 DOM 型 XSS 漏洞。
在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有 SEO 需求的页面,我们仍然要面对拼接 HTML 的问题。
预防 DOM 型 XSS 攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML、.outerHTML、document.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent、.setAttribute() 等。
如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。
DOM 中的内联事件监听器,如 location、onclick、onerror、onload、onmouseover 等,a 标签的 href 属性,JavaScript 的 eval()、setTimeout()、setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免
Content Security Policy
严格的 CSP 在 XSS 的防范中可以起到以下的作用:
- 禁止加载外域代码,防止复杂的攻击逻辑。
- 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
- 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
- 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
- 合理使用上报可以及时发现 XSS,利于尽快修复问题。
5.3 CSRF(跨站请求伪造)
概念
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的用户凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
流程
- 受害者登录 a.com,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了 b.com。
- b.com 向 a.com 发送了一个请求:a.com/act=xx。
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com以受害者的名义执行了act=xx。
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
几种常见的攻击类型
GET 类型的 CSRF
GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:
<img src="http://bank.example/withdraw?amount=10000&for=hacker" >
复制代码在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example 就会收到包含受害者登录信息的一次跨域请求。
POST 类型的 CSRF
这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:
<form action="http://bank.example/withdraw" method=POST>
<input type="hidden" name="account" value="xiaoming" />
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script>
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。
POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。
链接类型的 CSRF
链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
重磅消息!!
<a/>
由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。
CSRF的特点
- 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
- 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
- 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。
CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。
防护策略
CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。 上文中讲了CSRF的两个特点:
- CSRF(通常)发生在第三方域名。
- CSRF 攻击者不能获取到 Cookie 等信息,只是使用。
针对这两点,我们可以专门制定防护策略,如下:
- 阻止不明外域的访问
- 同源检测
- Samesite Cookie
- 提交时要求附加本域才能获取的信息
- CSRF Token
- 双重 Cookie 验证
同源检测
既然CSRF大多来自第三方网站,那么我们就直接禁止不受信任的域(使用Referer Header确定来源域名)对我们发起请求。
根据HTTP协议,在HTTP头中有一个字段叫Referer,记录了该HTTP请求的来源地址。 对于Ajax请求,图片和script等资源请求,Referer为发起请求的页面地址。对于页面跳转,Referer为打开页面历史记录的前一个页面地址。因此我们使用Referer中链接的Origin部分可以得知请求的来源域名。
这种方法并非万无一失,Referer的值是由浏览器提供的,虽然HTTP协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的Referer。
2014年,W3C的Web应用安全工作组发布了Referrer Policy草案,对浏览器该如何发送Referer做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,我们终于可以灵活地控制自己网站的Referer策略了。新版的Referrer Policy规定了五种Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。
根据上面的表格因此需要把Referrer Policy的策略设置成same-origin,对于同源的链接和引用,会发送Referer,referer值为Host不带Path;跨域访问则不携带Referer。例如:aaa.com引用bbb.com的资源,不会发送Referer。
设置Referrer Policy的方法有三种:
- 在CSP设置
- 页面头部增加meta标签
- a标签增加referrerpolicy属性
上面说的这些比较多,但我们可以知道一个问题:攻击者可以在自己的请求中隐藏Referer。如果攻击者将自己的请求这样填写,这个请求发起的攻击将不携带Referer。
<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer">
CSRF Token
前面讲到CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。
而CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
Token 验证有两种形式(多采用第二种):
- 在 HTTP 请求中以參数的形式添加一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,假设请求中没有token 或者 token 内容不对,则觉得可能是 CSRF 攻击而拒绝该请求。
- 在 HTTP 头中自己定义属性并验证:这里并非把 token 以參数的形式置于 HTTP 请求之中,而是把它放到HTTP 头中自己定义的属性里。通过 XMLHttpRequest 这个类,能够一次性给全部该类请求加上 csrftoken 这 HTTP 头属性。并把 token 值放入当中。这样攻克了上种方法在请求中添加 token 的不便。同一时候,通过XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用操心 token 会透过 Referer 泄露到其它站点中去。