HTTP(Hypertext Transfer Protocol)超文本传输协议是一个用于 Web 应用程序通信的应用层协议。它是一种客户端-服务器协议,客户端通过发送请求到服务器来获取资源,服务器则根据请求返回响应。HTTP 协议通常使用 TCP作为传输协议,但也可以使用其它传输协议
由于图片解析问题,可以点击查看 👉🏻 博客原文
扫码关注公众号,查看更多优质文章
URI、URL、URN
URI是一个通用的术语,用于唯一地标识互联网上的任何资源。它包括两个主要的部分:标识符和解析器。标识符是用来唯一标识一个资源的字符串,而解析器则是用来解析这个字符串,以便访问该资源。URI包括两种类型:URL
和URN
URL是URI的一个子集,它是一种具体的URI形式,用来唯一地标识互联网上的资源,并指定如何访问该资源。URL包含以下信息:
- 协议:访问该资源所使用的协议(如HTTP、FTP等)
- 域名或IP地址:资源所在的服务器的主机名或IP地址
- 端口:用于访问该资源的服务器端口号
- 路径:资源所在服务器上的路径和文件名
以下是一些URL的例子:
http://www.example.com/index.html
ftp://ftp.example.com/pub/file.txt
https://www.example.com/login
URN是另一种URI类型,用来给资源一个永久的、唯一的名称,而不是标识该资源的位置。URN不包括访问该资源的方式。URN的例子包括:
- urn:isbn:978-1-491-90234-1
- urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6
在实际应用中,URL是使用最广泛的URI类型。URL既能用来表示静态资源(如网页、图片、视频等),也能用来表示动态资源(如Web服务API、数据库查询等)
常见状态码
HTTP状态码是指当客户端向服务器发送请求时,服务器返回的响应状态码。它表示了服务器对请求的处理结果和状态。HTTP状态码由3个数字组成,第一个数字表示响应类别,后两个数字没有分类的作用。HTTP状态码范围从100到599,其中100到199是信息性状态码,200到299表示成功,300到399表示重定向,400到499表示客户端错误,500到599表示服务器错误,常见的状态码如下:
状态码 | 类型 | 含义 | 描述 |
---|---|---|---|
100 | Informational | Continue | 服务器已经接收到了请求头,并且客户端应继续发送请求主体部分 |
101 | Informational | Switching Protocols | 服务器正在根据客户端的请求切换协议(比如升级Websocket协议) |
102 | Informational | Processing | 服务器正在处理请求,但尚未完成处理 |
200 | Success | OK | 请求已经成功,并返回了请求所需要的信息 |
201 | Success | Created | 请求已经成功,并在服务器上创建了新的资源 |
202 | Success | Accepted | 请求已经被接受,但尚未被处理完成 |
204 | Success | No Content | 请求已经成功,但不需要返回任何数据(配置跨域时通常使用204返回) |
206 | Success | Partial Content | 请求成功,但是只返回了部分内容(范围请求) |
301 | Redirection | Moved Permanently | 请求的资源已经被永久转移到新的URL |
302 | Redirection | Found | 请求的资源已经临时转移到新的URL |
304 | Redirection | Not Modified | 客户端缓存的资源是最新的,服务器返回的内容为空 |
307 | Redirection | Temporary Redirect | 请求的资源已经临时转移到新的URL |
400 | Client Error | Bad Request | 请求格式错误,服务器无法解析该请求 |
401 | Client Error | Unauthorized | 请求未经授权,需要进行身份验证 |
403 | Client Error | Forbidden | 请求被拒绝,拒绝访问 |
404 | Client Error | Not Found | 请求的资源不存在 |
408 | Client Error | Request Timeout | 请求超时 |
409 | Client Error | Conflict | 请求与服务器上的资源发生冲突 |
415 | Client Error | Unsupported Media Type | 服务器无法处理请求中所包含的媒体类型 |
429 | Client Error | Too Many Requests | 请求次数超过了限制 |
500 | Server Error | Internal Server Error | 服务器发生了错误,无法完成请求 |
501 | Server Error | Not Implemented | 服务器不支持请求的功能 |
502 | Server Error | Bad Gateway | 服务器作为网关或代理,从上游服务器收到的响应无效或错误 |
503 | Server Error | Service Unavailable | 服务器暂时无法处理请求,通常是由于服务器过载或维护 |
504 | Server Error | Gateway Timeout | 服务器充当网关或代理时,没有及时从上游服务器收到请求 |
HTTP报文
HTTP报文是HTTP通信中的数据传输单位,可以分为请求报文和响应报文两种类型。请求报文由请求行、请求头部和请求正文组成,其中请求行包括请求方法、请求URI和HTTP协议版本,请求头部包括若干个字段和值,用于描述请求的相关信息,请求正文可选,通常用于传输POST请求中的表单数据等内容;响应报文由状态行、响应头部和响应正文组成,其中状态行包括HTTP协议版本、状态码和状态描述,响应头部包括若干个字段和值,用于描述响应的相关信息,响应正文通常包含响应的实际内容
抓包查看HTTP报文:
首部字段
HTTP的首部字段可以分为通用首部、请求首部和响应首部三类。以下是这三类首部字段的一些常见示例:
通用首部字段:
- Cache-Control:指定缓存机制
- Connection:指定连接类型
- Date:指定消息创建时间
- Pragma:指定缓存机制
- Trailer:指定尾部(不常用)
- Transfer-Encoding:指定传输编码方式
- Upgrade:升级为其他协议
- Via:指定代理服务器的相关信息
- Warning:包含一些警告信息
请求首部字段:
- Accept:指定可接受的响应类型
- Accept-Encoding:指定可接受的内容编码方式
- Authorization:指定身份认证信息
- Cookie:指定来自客户端的Cookie信息
- Host:指定请求的主机名和端口号
- Referer:指定请求来源
- User-Agent:指定客户端的用户代理信息
响应首部字段:
- Accept-Ranges:指定可接受的字节范围
- Age:指定缓存资源生成的时间
- Content-Encoding:指定响应正文的编码方式
- Content-Length:指定响应正文的长度
- Content-Type:指定响应正文的类型(Multipurpose Internet Mail Extensions 或 MIME 类型),可以在这里查看更多的MIME类型
- Server:指定服务器的信息
- Expires:资源过期时间,基于具体时间
- Last-Modified:资源修改时间,用于缓存
- Etag:文件编码,用于判断缓存
请求方法
HTTP定义了一些请求方法(Request Method),用于定义客户端与服务器之间进行请求和响应的行为。下面是HTTP的请求方法及其用途:
GET
:用于请求指定资源。GET 请求应该只用于获取资源,而不应该对资源进行修改。GET 方法是幂等的,不会对资源造成任何更改,因此不会对安全造成威胁。然而,使用 GET 方法时应当注意避免将敏感信息暴露在 URL 中,因为 URL 可能会被缓存、浏览器历史记录、服务器日志等记录下来HEAD
:与 GET 请求类似,但是服务器不会返回请求的资源,只返回头部信息。HEAD 请求常用于获取资源的元信息,如 Content-Type、Content-Length 等POST
:用于向服务器提交数据,POST 请求可能会导致服务器的状态发生变化。POST 请求常用于提交表单数据,例如登录、注册等。由于 POST 请求通常包含用户输入的敏感数据,因此可能受到跨站脚本攻击、跨站请求伪造攻击等安全威胁。为了防止这些攻击,可以在 POST 请求中使用 CSRF token 或者验证码等技术进行验证PUT
:用于向服务器上传指定资源。PUT 请求是幂等的,即多次执行相同的 PUT 请求,对服务器的影响是相同的。PUT 请求通常用于上传文件或者更新资源DELETE
:用于请求删除指定资源。DELETE 请求用于删除服务器上的资源CONNECT
:用于建立与服务器的隧道连接,通常用于 HTTPS 的代理服务器连接OPTIONS
:用于查询服务器支持的请求方法和头部信息,通常用于跨域请求中的预检请求(Preflight Request)TRACE
:用于跟踪请求-响应的传输路径,通常用于调试和测试PATCH
:用于对资源进行局部更新。PATCH 请求可以用于更新资源的一部分,而不需要替换整个资源
HTTP演变
HTTP是一种用于在Web上传输数据的协议,经过多次演变和改进,逐渐增加了新的特性和功能,包括支持多种请求方法、请求头和响应头、复用TCP连接、持久连接、分块传输编码、服务器推送、头部压缩等,以提高Web性能和效率。HTTP的演变版本包括HTTP/0.9、HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3,每个版本都在不断地改进和完善,以满足不同的需求和应用场景
HTTP/1.1
HTTP/1.1是目前应用最广泛的HTTP协议版本之一,它引入了多个新特性,例如复用TCP连接、持久连接、分块传输编码、虚拟主机等,这些特性使得HTTP/1.1能够更加高效地传输数据,从而提高了Web性能和效率。同时,HTTP/1.1也支持多种请求方法和响应状态码,例如GET、POST、PUT、DELETE等方法,以及200、404、500等状态码。HTTP/1.1仍然是Web开发中的重要组成部分,尽管有着一些限制和缺点,但仍然是Web传输协议中的重要参考标准之一
HTTP/1.1的缺点是队头阻塞
💡什么是队头阻塞
HTTP/1.1中会复用同一个TCP连接(浏览器会限制同一个域名TCP最大同时连接数),当同一个连接同时请求多个资源时,它们会按顺序进行数据传输,如果前者丢包或者响应时间过长就会阻塞后面的请求,或者后面的TCP连接会等待前面的传输,这就是队头阻塞
HTTP/2
:::tip
关于如何使用HTTP2,这里举例用nginx配置HTTP2,详细步骤已经在『nginx使用手册』文章中说明了
:::
HTTP/2基于SPDY协议进行了扩展和改进,提供了多路复用、头部压缩、服务器推送等多个新特性,使得HTTP2比HTTP/1.1更加高效和快速
HTTP2的优点:
- 多路复用:在一个TCP连接中同时传输多个请求和响应,解决了HTTP/1.1中的队头阻塞问题
- 头部压缩:采用HPACK算法对请求头和响应头进行压缩,减少了传输数据的大小,提高了网络性能
- 服务器推送(Server Push):能够将相关资源预加载到客户端,进一步提高了Web性能。
HTTP2的缺点:
- 虽然HTTP2解决了HTTP/1.1中的队头阻塞问题,但在高延迟的网络环境下仍然存在性能瓶颈
- 实现HTTP2需要升级服务器和客户端软件,对现有的Web生态系统具有一定的影响
多路复用
多路复用(Multiplexing)是其最重要的特性之一,它允许在同一 TCP 连接上同时传输多个请求和响应。在传统的 HTTP/1.1 协议中,每个请求和响应都需要建立一次 TCP 连接,尽管可以使用KeepAlive减少TCP的握手,但浏览器限制了同域名的的TCP最大连接数,因此多个请求和响应之间存在着明显的串行化和等待时间,这会导致性能瓶颈。而在 HTTP/2 中,通过多路复用的技术,所有请求和响应都可以在同一个 TCP 连接上并发处理,大大提高了网站的性能和吞吐量
具体来说,HTTP/2 的多路复用技术通过将所有的请求和响应分解成若干个小的数据流(Stream),并且每个数据流可以被细分成若干个数据帧(Frame)来传输。这样,就可以在同一个 TCP 连接上同时传输多个数据流,而不需要等待某个请求或响应的完成。由于同一 TCP 连接上可以并发处理多个数据流,因此 HTTP/2 的多路复用可以大大提高网站的性能,减少网络延迟和响应时间
💡使用了HTTP2就不会有队头阻塞了吗?
从以上的角度可以理解不存在队头阻塞了,但在弱网环境下依然会存在。某个数据流的帧因为网络问题或其他原因无法及时传输到对端时,其后面的数据流会一直等待,直到该数据流的所有帧全部传输完成。通常可以使用帧优先级的策略,将重要的数据优先传输,不重要的放后面
由于HTTP2都会共用同一个TCP连接,弱网环境下可能传输效率比HTTP/1.1更差
ServerPush
ServerPush
顾名思义服务器推送技术,通常客户端必须首先发送请求才能获取到服务器上的资源,例如 HTML 文件、JavaScript、CSS 文件和图片等。而在 HTTP2 中,服务器可以在收到客户端请求之前主动将一些资源推送给客户端,这样客户端就可以提前获取到这些资源,从而避免了等待服务器响应的时间,减少了页面加载的延迟
使用nginx配置ServerPush
location / {root /usr/share/nginx/html;index index.html index.htm;# push某个静态资源http2_push /style.css;
}
目前在笔者电脑浏览器上查看ServerPush类型网络已经不明显了,以下是Chrome(112)和Firefox(109)的面板结果:
出了用浏览器查看ServerPush的请求,也可以使用nghttp
工具进行查看:
➜ nghttp https://general-mac.com -ans
id responseEnd requestStart process code size request path13 +494us +50us 444us 200 176 /2 +567us * +431us 136us 200 67 /style.css15 +2.38ms +584us 1.80ms 200 99K /images/k8s-fine.jpg
以上responseEnd为*
的表示ServerPush类型的资源
ServerPush问题
ServerPush主动推送资源可能会存在以下问题:
- 推送的资源不被缓存,当一些静态资源设置了缓存时,server端不知道直接推送已经被缓存的静态资源
- 一些需要身份验证的资源可能被错误的推送
- 推送对首屏的渲染并没有太大用处,现在大量使用CDN,静态资源也不一定存在同一个服务上
这里不做详细的问题分析,可以看看AlloyTeam这篇文章
头部压缩
HTTP2的头部压缩技术大体包含以下部分:
- 客户端和服务端维护一份相同的静态字典表,包含常见的头部键或键值
- 客户端和服务端维护维护一份动态字典表,根据实际情况动态添加字典表
- 上下文感知存储已经传输过的头部信息,避免重复传输相同的头部信息
- 使用哈弗编码(Huffman),这里查看静态哈弗编码表(Huffman)
本文不对头部压缩做深入解释,可以阅读详解 HTTP/2头压缩算法 —— HPACK和HTTP/2 HPACK实际应用文章了解更多
HTTP/3
HTTP/3基于QUIC协议进行开发,提供了一些新特性来进一步提高Web性能。与HTTP/2相比,HTTP/3最大的不同在于底层传输协议的改变,使用了QUIC协议代替了TCP协议,并使用TLS 1.3
作为底层安全传输协议
HTTP/3的优点:
- 采用QUIC协议,相比TCP协议,具有更低的延迟和更好的拥塞控制
- 使用TLS 1.3协议,提供更加安全的传输
- 在HTTP/2的基础上进一步优化,使得Web性能更加高效
HTTP/3的缺点:
- 与HTTP/2不兼容,需要新的协议栈的支持
- HTTP/3在早期的实现中存在一些问题,可能导致不稳定或性能问题
- 目前,HTTP/3还处于实验阶段,尚未被广泛采用,因此可能会存在一些不确定因素
HTTPS
:::tip
关于如何将HTTP升级为HTTPS,这里举例用nginx配置HTTPS,详细步骤已经在『nginx使用手册』文章中说明了,你可以参考其并结合wireshark等其他工具进行抓包分析
:::
HTTP 是明文协议,数据传输过程中不加密,容易被攻击者窃取、篡改或冒充。而 HTTPS 通过使用 SSL/TLS 协议对数据进行加密和认证,保证了数据在传输过程中的安全性,防止数据被窃取、篡改或冒充。HTTPS不是新的协议仍是基于HTTP协议在传输层TCP之上添加了一层安全协议SSL/TSL可以说HTTPS = HTTP + SSL/TLS
,大概过程如下图:
SSL/TLS
为了实现HTTP报文加密传输,引入了SSL、TLS协议。SSL(Secure Socket Layer)
协议和 TLS(Transport Layer Security)
协议是用于保护网络通信安全的协议,它们主要用于在客户端和服务器之间建立加密连接,保证通信过程中的机密性、完整性和可信性
SSL 和 TLS 协议工作在传输层之上,通过在传输层与应用层之间插入一层安全层来保护应用层数据的安全性。由于 SSL 协议存在一些安全漏洞和缺陷,因此 TLS 协议得以诞生并逐渐取代了 SSL 协议。TSL也经历多个版本的迭代:
- TLS 1.0:1999 年发布,主要基于 SSL 3.0 开发而来,对 SSL 3.0 中的一些安全漏洞做了修复,支持对称加密、非对称加密和消息摘要等算法。但是该协议存在一些缺陷,如 CBC 攻击等
- TLS 1.1:2006 年发布,修复了 TLS 1.0 中的一些缺陷和漏洞,支持更安全的加密算法和扩展性。此版本不支持 SSL 2.0 和 SSL 3.0
- TLS 1.2:2008 年发布,主要是在 TLS 1.1 的基础上进行了改进,支持更安全的加密算法和协商过程,增强了安全性和性能,目前大部分使用HTTPS的都使用了TLS1.2
- TLS 1.3:2018 年发布,主要是在 TLS 1.2 的基础上进行了改进,优化了握手协议,支持 0-RTT 模式,提升了性能和安全性
随着 TLS 协议版本的迭代,不断加强了安全性、性能和扩展性。建议使用 TLS 1.2 及以上版本的协议,以获得更好的安全性保障
对称/非对称加密
在讲握手前先了解下对称和非对称加密的概念,对称加密和非对称加密都是计算机安全领域中常用的加密算法,用于保护数据的安全性。它们的主要区别在于加密和解密时使用的密钥类型不同
对称加密
是指在加密和解密过程中使用同一个密钥的加密算法。也就是说,加密和解密都使用相同的密钥,因此这种加密算法也称为“共享密钥加密”;非对称加密
则使用不同的密钥进行加密和解密。这种加密算法需要一对密钥:一个公钥和一个私钥。公钥可以随意分发给其他人,而私钥则必须保持私密。使用公钥加密的数据只能用私钥解密,使用私钥加密的数据只能用公钥解密
两种加密算法各有优缺点:
- 对称加密算法的优点是加密和解密速度快,适用于大量数据的加密和解密。但是,由于密钥需要在通信双方之间共享,所以在密钥分发和管理方面存在安全隐患
- 非对称加密算法的优点是密钥不需要在通信双方之间共享,因此安全性更高,但加密和解密速度相对较慢,适用于少量数据的加密和解密
而TLS结合了两种加密算法的优点,握手阶段使用非对称加密进行密钥的共享,握手后的数据传输使用对称加密;为什么要这样设计呢?由于握手阶段双方还没有互相认证,如果使用对称加密共享秘话,这对外界都是透明的,黑客很容易拿到密钥,会有很大的安全隐患。而使用非对称加密,双方可以在本地根据一系列规则生成对应的规则避免密钥传输或者被中间人破解,大大提高安全性。由于非对称加密过程很耗时,在握手成功后二者便使用对称加密进行数据传输缩短解密时间
TLS握手
TLS有很多个版本目前主流的版本为TLS1.2和TLS1.3,每个协议版本都有多个加密算法如:RSA、DH,这里简单介绍下TLS1.2的RSA握手和DH握手,具体的密码学原理不做介绍,自行科普
I、RSA握手
Client Hello
:客户端发送问候消息,携带着一个随机数、加密套件、协议版本发送给服务端Server Hello
:服务端收到ClientHello根据Client的加密套件选择一个密码套件,并生成一个随机数,确定协议版本后也发送一个ServerHello给客户端,其中包含密码套件、server随机数、协议版本、证书验证证书
:客户端根据CA机构验证证书的合法性,如果不合法将会握手失败,反之继续生成预主密钥
:客户端从证书中取出公钥,生成第二个随机数(pre-master)也就是预主密钥,使用公钥加密后发送给服务端私钥解密
:服务端收到客户端发送的预主密钥后,使用自己的私钥解密得到预主密钥会话密钥
:此时客户端和服务端都有3个随机数,双方都根据3个随机数和密码套件生成会话密钥,用来后续的通信加密Client Encrypted Message
:客户端使用会话密钥加密消息发送一个会话摘要,等待服务端的验证Server Encrypted Message
:服务端收到客户端的消息并验证后,也使用会话秘钥给客户端发送一个会话摘要握手成功
:双方验证成功后便进入正式数据传输阶段
II、DH(Diffie-Hellman)握手
- 客户端发送一个 ClientHello 消息给服务器,其中包含SSL/TLS协议版本号、加密套件列表、随机数等信息
- 服务器返回一个 ServerHello 消息给客户端,其中包含 SSL/TLS 协议版本号、加密套件(如 TLS_DHE_RSA_WITH_AES_256_GCM_SHA384)、随机数等信息
- 服务器发送一个 Certificate 消息,包含服务器的证书信息,包括公钥和证书链
- 服务器发送一个 ServerKeyExchange 消息,其中包含 DH 公开参数和签名信息。DH公开参数包括质数p、底数g和服务器的公钥,签名信息是由服务器的私钥生成的,用于验证公开参数的合法性
- 客户端接收到服务器发送的 DH 公开参数后,生成一个随机数(ClientRandom),并计算出 DH 密钥交换的临时公钥。然后,客户端使用服务器的公钥和DH密钥交换的临时公钥,计算出一个共享密钥
- 客户端发送一个 ClientKeyExchange消息,其中包含使用服务器的公钥和DH密钥交换的临时公钥计算出的共享密钥的信息
- 客户端发送一个 ChangeCipherSpec 消息,表示从现在开始使用新的密钥加密通信
- 客户端发送一个 Finished 消息,其中包含使用新密钥加密的随机数和一个验证信息。服务器接收到该消息后,使用相同的方式验证信息的正确性,验证通过后,向客户端发送一个 ChangeCipherSpec 消息和一个 Finished 消息
- 客户端接收到服务器发送的消息后,使用新密钥解密 Finished 消息中的验证信息,验证通过后,握手结束,可以开始安全通信
DH 密钥交换算法的主要思想是利用质数p和底数g,实现双方在不直接交换密钥的情况下,生成相同的共享密钥 K。具体过程如下:
- 选择两个质数 p 和 g,公开 p 和 g 的值
- Alice 选择一个私钥 a,并计算出 A = g^a mod p,然后将 A 发送给 Bob
- Bob 选择一个私钥 b,并计算出 B = g^b mod p,然后将 B 发送给 Alice
- Alice 计算出共享密钥 K = B^a mod p
- Bob 计算出共享密钥 K = A^b mod p
DH算法是TLS1.2中主流的加密算法,详细过程请自行翻阅相关文档
III、TLS1.3握手
TLS 1.3相较于TLS 1.2在握手阶段做出了以下的改变:
- 废除了RSA、DH和DSS等非对称加密算法作为握手过程的加密方式,只保留了部分的加密算法
- TLS 1.3的握手协议只有两个往返,比TLS 1.2简化了一半。TLS 1.3中取消了不必要的消息、字段和握手阶段,并将密钥交换过程和认证过程合并到了一个握手消息中,从而大大减少了握手时间和握手消息数量,提高了握手效率
- TLS 1.3中增加了0-RTT模式,允许客户端在建立新连接时将上一次会话的密钥用于加密数据,从而避免了连接建立时的握手延迟,提高了连接速度
💡什么是前向安全性?在RSA算法中如果黑客得到了证书密钥,就可以对以往的会话进行破解,而DH算法在握手时都会生成临时的密钥对,即使黑客拿到了密钥也只会对当前会话造成影响,而不会对之前的历史消息造成影响,这就是前向安全性
HTTP缓存
HTTP最常见的优化资源的手段就是缓存,使用缓存可以高效的减少网络资源的请求,加快页面的渲染,减轻服务器负载提高网站的可用性
通常缓存由浏览器和服务器二者相互协商的实现的,其中常见的强缓存、协商缓存等等,由于篇幅原因这里不再详细展开,可以阅读我的「web缓存策略」一文
跨域
什么是跨域?相信每个初次遇到此情况的同学都会很迷惑。跨域问题的本质是浏览器为了保护用户的安全而采取的安全策略同源策略(Same-Origin Policy)
。同源策略是一种安全策略,它要求浏览器限制脚本在不同源之间的交互。同源是指协议、域名、端口号都相同,只有在同源的情况下,浏览器才允许两个页面之间相互访问和交互数据
同源策略在 DOM、Web 数据和网络三个层面均有体现:
- DOM同源:不同源的页面无法操作dom
- 数据同源:不同源站点无法获取cookie、session、localstorage、IndexDB等等
- 网络同源:不同源站点无法请求网络数据
💡如何解决跨域呢?
解决跨域的方式有很多如:JSONP、 CORS(跨域资源共享)等等,对于跨域请求浏览器都会先进行预检请求(OPTIONS)
简单、非简单请求
简单请求和非简单请求是浏览器在向服务器发送跨域请求时的两种不同类型
简单请求(Simple Request)是指浏览器向服务器发送的请求,满足以下所有条件:
- 使用以下方法之一:GET、HEAD、POST
- Content-Type 的值仅限于下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 请求中的任何自定义 header 都使用以下方法设置:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type (但仅限于上面三个可选值)
- 请求中没有使用 ReadableStream 对象
简单请求不需要进行额外的 CORS 预检请求(CORS Preflight Request)即可跨域访问资源,浏览器会直接发送请求,并将响应数据返回给前端
非简单请求(Non-Simple Request)是指浏览器向服务器发送的请求,不满足简单请求的所有条件。在这种情况下,浏览器必须先发送一个 CORS 预检请求(CORS Preflight Request)给服务器,以确定是否允许跨域访问资源。CORS 预检请求会先使用 OPTIONS 方法发送一个带有特定 header(例如 Access-Control-Request-Method 和 Access-Control-Request-Headers)的请求,服务器收到预检请求后,根据请求头中的信息判断是否允许跨域请求。如果服务器返回允许跨域请求的响应头,浏览器才会发送实际的请求
JSONP
使用JSONP解决跨域问题,以下在请求api后会执行jsonpCb回调;JSONP仅支持GET请求,目前大多数浏览器都开始限制对 JSONP 的支持,不允许通过 script 标签加载不安全的跨域脚本文件
document.querySelector("button").addEventListener("click", () => {createJSONP();
})
function jsonpCb(res) {// 处理返回的数据...console.log(res)
}
// 创建JSONP标签加载数据
function createJSONP() {const script = document.createElement("script");script.src = "http://localhost:10000/api?callback=jsonpCb"document.head.appendChild(script)
}
CORS
除了JSONP之外大多数都可以使用CORS(跨域资源共享)协议进行跨域请求,CORS 协议通过在 HTTP 头部添加一些字段来告知浏览器,哪些源可以被信任,从而使得跨域请求得以正常进行。在 CORS 协议中,服务器端需要设置Access-Control-Allow-Origin
等头部信息来指定允许跨域请求的源和方法,这里使用nginx代理进行cors的配置
# 设置允许的源(IP+PORT)
add_header Access-Control-Allow-Origin $http_origin;
# 设置允许的 请求方法
add_header Access-Control-Allow-Methods 'OPTIONS, GET, POST, PUT, DELETE';
# 设置允许的请求头
add_header Access-Control-Allow-Headers 'x-locale';
# 其他自定义...
# 预检请求 204 快速返回
if ($request_method = 'OPTIONS') {return 204;
}
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Host $host;
proxy_set_header X-Http-Host $http_host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://backend;
具体可以查看「nginx配置跨域」
postMessage
不做解释...
数据传输
HTTP定长传输
和不定长传输
是两种常见的 HTTP 报文传输方式,定长传输需要在 HTTP 请求头中指定消息体的长度,服务器可以一次性读取整个消息体并返回,不定长传输通过传输编码(如分块编码)来动态地读取消息体一部分一部分的返回内容
定长传输
定长传输是一种HTTP消息传输方式,其中消息体的长度在请求或响应中预先指定。在这种传输方式中,发送方将消息体的长度包含在消息头中的Content-Length
字段中,接收方使用该字段来确定消息体的长度,从而正确读取消息
使用定长传输的主要优点是,接收方可以在读取消息体之前知道其长度,从而可以更好地管理接收缓冲区的大小和减少内存分配的需求。此外,定长传输还可以简化协议的实现,因为发送方和接收方都不需要解析消息体的内容来确定其长度
➜ curl http://localhost:10000 -I
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 12
ETag: W/"c-QETVAhUYmmve97quvZif9EL6tqM"
Date: Mon, 24 Apr 2023 09:12:52 GMT
Connection: keep-alive
Keep-Alive: timeout=5
上面的例子中,Content-Length 为 12,表示消息体的长度为 12 个字节,即 Hello World。服务器在读取到请求头之后,就知道了消息体的长度,可以一次性读取整个消息体,处理速度较快
不定长传输
在 HTTP 请求头中指定传输编码,使得 HTTP 报文的长度未知,但是可以通过不定长的方式来传输消息体。最常用的编码方式是通过Transfer-Encoding设置分块编码(Chunked Encoding),将消息体分成多个大小相等的块,每个块都带有自己的大小信息和一个表示块结束的标志,服务器在读取完一个块之后才会知道下一个块的大小和内容,从而动态地读取消息体,这种方式也被称为消息长度未确定(Message Length Unknown)方式
// nodejs模拟不定长传输
app.use("/chunked", (req, res) => {res.setHeader("Transfer-Encoding", "chunked");let timer,i = 1;// 1s返回一次 总共返回9次timer = setInterval(() => {res.write(`${i}`);if (i >= 10) {clearInterval(timer);res.end();}i++;}, 1000);
});
➜ curl http://localhost:10000/chunked -I
HTTP/1.1 200 OK
X-Powered-By: Express
Transfer-Encoding: chunked
Date: Mon, 24 Apr 2023 08:49:25 GMT
Connection: keep-alive
Keep-Alive: timeout=5
不定长传输是一种灵活、高效的消息传输方式,适用于流式数据的传输和动态调整消息体长度的场景
范围请求
:::tip TIP
HTTP的范围请求在大文件的下载中发挥着重要的地位,能够有效的降低网络带宽、资源浪费的情况;使用范围请求需要服务端的支持,HTTP状态码会返回206(Partial Content)
:::
HTTP范围(Range)请求可以用于请求服务器返回资源的部分内容。范围请求可以通过指定资源的字节范围来实现,例如请求一个视频文件的某个时间段的内容,通过在请求头中添加Range字段来实现:
# 指定开始和结束
Range: bytes=0-1
# 指定开始一直到结尾
Range: bytes=100-
# 多个范围
Range: bytes=10-20, 50-60
Range请求需要服务端的支持才可以,在支持Range请求会返回响应的头部:
# 表示范围请求单位 bytes, 不支持时为none,一般也不会由此字段
Accept-Range: bytes
当服务器收到对应的范围请求后头部也会标识对应的范围字段和整体大小:
Content-Range: bytes 10-20/123456
💡使用nodejs简单的示范一下范围请求:
// 发起范围请求
fetch("/range", { headers: { "Range": "bytes=10-100" } });// nodejs服务
app.use("/range", async (req, res) => {res.download(path.resolve(__dirname, "./test.txt"), {acceptRange: true,});
});
以下是具体的传输过程中所带的信息,部分字段已经被省略:
# General
Request URL: http://localhost:10000/range
Request Method: GET
Status Code: 206 Partial Content# Response Headers
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Connection: keep-alive
Content-Length: 1171
Content-Range: bytes 100-1270/1271
X-Powered-By: Express# Request Headers
Host: localhost:10000
Range: bytes=100-
范围请求通常的使用场景有如下:
- 下载大文件:当需要下载大文件时,如果客户端在网络不稳定或带宽较小的情况下下载完整的文件会导致下载速度慢或者下载失败,因此使用范围请求可以只下载部分文件,从而提高下载速度和成功率
- 音视频流的播放:在播放音视频流时,客户端可以使用范围请求来获取特定的时间段内的数据,而不是获取整个文件。这可以提高播放的响应速度和减少缓冲时间
总之,范围请求允许获取资源的部分内容,减少网络流量和提高传输速度
大文件上传与下载
了解如何进行大文件上传与下载对于HTTP的使用难点、复杂度有更深入的认识,由于篇幅原因这里不做演示了,可以查看我的「大文件上传」与「大文件下载」相关文章
身份认证
我们知道HTTP是个无状态协议,想要限制资源的访问,就需要客户端与服务端建立一套身份认证机制,对于鉴别了身份的人才可以获取相关资源
常见的认证机制如Cookie
、Session
、Token
等技术,这里简单介绍下Cookie和Session
Cookie
Cookie是一种存储在客户端的小型文本文件(存储大小通常为4kb),它由Web服务器发送给Web浏览器,并保存在用户的本地计算机上。Cookie可以用于存储用户的会话信息、偏好设置、购物车内容等数据,以便以后使用。可以通过cookie的具体参数精确设置使用范围、过期时间等等,统一域名可以使用一个或多个cookie,浏览器发送请求时会在头部携带上相关的cookie信息,设置cookie相关参数包括:
- 名称(Name):Cookie的名称,用于标识Cookie
- 值(Value):Cookie的值,保存在客户端计算机上,由服务器设置
- 域(Domain):Cookie的作用域,指定Cookie可访问的域名,只有当前域名才可以使用,可以设置具体域名,也可以使用
.domain.com
这种所有子域名等等 - 路径(Path):Cookie的路径,除了指定域名外,还可以设置指定的路径,其他路径则不能使用当前cookie
- 过期时间(Expires/maxAge):指定Cookie的过期时间,expires设置具体的时间,maxAge设置相对时间,maxAge的优先级高于expires,如果未设置,则只在当前会话中有效,关闭浏览器会失效;当maxAge位0时表示要清楚当前cookie
- 安全(Secure):指定Cookie是否只能通过HTTPS连接传输
- HttpOnly:只能通过HTTP协议传输,JS无法访问防止XSS攻击
// 使用nodejs模拟cookie
const express = require("express");
const app = express();res.cookie("__ut", "123456", {// maxAge: 0, 表示清除cookiemaxAge: 1000 * 60 * 10, // 优先级高// expires: new Date("2023-04-24 10:17"),httpOnly: true, // 只允许HTTP请求使用,其他方式无法获取secure: true, // 规定cookie只能HTTPS传输sameSite: "strict", // 同站点使用domain: "localhost", // 生效的域名path: "/cookie.html" // 生效的路径,默认 / 所有路径生效
})
上面使用nodejs设置了key为__ut、值为123456的cookie,并设置了相关的属性
// 当设置了httpOnly后,无法在页面中获取
console.log(document.cookie); // ''
当页面有了相关cookie后,在cookie没有过期时,每次请求都会携带上相关的cookie
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: max-age=0
Connection: keep-alive
Cookie: __ut=123456
Host: localhost:10000
If-Modified-Since: Mon, 24 Apr 2023 01:10:45 GMT
If-None-Match: W/"201-187b0d02a08"
sec-ch-ua: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99"
sec-ch-ua-mobile: ?0
Session
和Cookie相比Session是将认证信息存储在服务器,由服务器进行管理。具体是用户登录后服务器将会生成一个session对象,该对象包含一个唯一的sessionId还有过期时间等其他信息,服务端将sessionId存入cookie中设置到客户端,这样用户下次访问时便会带上cookie,服务器根据cookie中的sessionId对比存储起来的session信息,这便是session的原理
由于session存放在服务端,当数据量大时会消耗很多资源,采用负载均衡时要保持数据的同步和一致
这里不再演示Session的使用方法,感兴趣的同学可以自行搜索。此外还可以使用token的方式进行认证,这里不再介绍
HTTP攻击
HTTP是互联网上最常用的协议之一,也是攻击者经常利用的目标之一。攻击者可以通过利用HTTP协议的漏洞或者弱点,实施各种攻击,如请求欺骗、SQL注入攻击、跨站脚本攻击、跨站请求伪造攻击、拒绝服务攻击等。这些攻击可能会导致用户隐私泄露、系统瘫痪、数据丢失等严重后果,给个人和企业带来极大的损失
这里将列举常见的几种攻击,并结合例子演示攻击过程以及有效的防御措施
XSS
:::tip 温馨提示
🔍 本模块的代码可以点击这里查看
:::
XSS(Cross-Site Scripting)攻击是一种常见的Web安全漏洞,攻击者利用这种漏洞向网站注入恶意脚本代码(运行了不明来历的代码),使得当其他用户访问这个网站时,这些恶意脚本代码会在他们的浏览器中执行,从而达到攻击者的目的,比如窃取用户的敏感信息、篡改网页内容、盗用用户账户等。XSS攻击通常分为三种类型:反射型XSS
、存储型XSS
和DOM-based XSS
类型 | 存储区 | 插入点 |
---|---|---|
存储型 XSS | 后端数据库 | HTML |
反射型 XSS | URL | HTML |
DOM 型 XSS | 后端数据库/前端存储/URL | 前端 JavaScript |
- 存储型:通常是后端没有对SQL进行过滤,将恶意代码存储到了数据库中,渲染时恶意代码直接渲染到页面上了,没有转义时就会收到攻击
# 假如插入一个脚本 insert into article values ('<script>alert("xss")</script>');# 页面在服务端渲染时 <article><%= getArticle('xxx')></article># 出码结果,当浏览器运行时就会弹出 xss <article><script>alert("xss")</script></article>
- 反射型:通常是将恶意代码存在链接中,服务端渲染时,取到链接参数渲染页面,没有进行转义就会收到攻击
# 假如有一个恶意的链接 # https://www.website.com?id=<script>alert('xss')</script># 服务端渲染 <user><%= getParameter('id')%></user>
- DOM型:这类通常是JS的一些漏洞,通过JS将恶意代码渲染到页面上
// JS有一段这样的逻辑 document.body = getURLQuery('id') # https://www.website.com?id=<script>alert('xss')</script>
💡那么如何防止XSS攻击呢?
通过以上的分析可以得出XSS攻击产生恶意的脚本,然后在浏览器渲染时执行,可以通过以下方式进行防御:
- 前端或后端对传入的内容进行转义(需要区分不同场景,转义不会影响页面渲染时)
- 后端存储入库时进行转义(同上)
- 页面渲染时进行字符转义(重要)
💡 除了简单的转义操作,还可以使用CSP(Content Secure Policy)web内容安全策略,CSP 通过指定可信任资源的白名单,限制网页内容只能从指定的源加载,从而有效地减少了攻击者利用恶意脚本注入攻击的可能性,简单来讲此属性就是来配置资源加载白名单,符合白名单的才会加载,其余的浏览器都会阻止资源加载
CSP语法规则:
Content-Security-Policy: <policy-directive> [value1, [value2, [...]]]; <policy-directive> ...
Content-Security-Policy-Report-Only: 同上
以上是定义Content-Security-Policy
和Content-Security-Policy-Report-Only
HTTP头部的语法规则,可以定义多个策略指令,指令之间使用;
分隔;每个指令都可以定义多个值,多个值之间使用空格分隔;以上两个字段的区别在于Content-Security-Policy-Report-Only
并不会阻止不安全的内容被加载,而是只会向服务器报告违规的内容,CSP-RO 只是提供了一种测试 CSP 策略的方式,而不会对实际的安全策略产生影响。以下是两者常见的指令:
default-src
:指定了所有其他资源类型的默认源。如果没有其他资源类型的特定源被指定,则使用此默认源style-src
:指定了可以加载 CSS 样式表的源script-src
:指定了可以加载 JavaScript 脚本的源img-src
:指定了可以加载图像资源的源connect-src
:指定了可以从文档加载的资源的来源,例如 XHR、WebSockets 或者 fetchframe-ancestors
:指定了可以嵌入到文档中的父级的源,以及使用 frame-ancestors 'none' 表示文档不能被嵌入到任何父级中frame-src
:指定了可以嵌入到文档中的子资源(如 frame 和 iframe)的源,建议使用 child-src 代替child-src
:指定了可以嵌入到文档中的子资源(如 frame 和 iframe)的源manifest-src
:指定了用于获取 web 应用程序清单文件的源report-uri
:指定了接收违规报告的 URI
这里只列出了部分的字段,更多字段的使用你可以查看MDN相关文档
这些字段的值可以设置成自己配置允许的域名外,同时也支持一些通配符如http://*.usword.cn
,还有一些内置的值:
none
:表示不允许从任何来源加载资源self
:表示只允许从同一域名加载资源,即只允许从当前网站的域名加载资源unsafe-inline
:表示允许在 HTML 元素的 style 属性和 script 元素内嵌入 JavaScript 代码unsafe-eval
:表示允许使用 eval() 和 new Function() 等方法执行动态生成的 JavaScript 代码strict-dynamic
:表示允许执行指定源的 JavaScript 代码,但是只有当这些源已经在页面中加载过了,并且只有在执行 JavaScript 代码之前进行了一些必要的检查之后才能执行
👇 下面来演示CSP的使用:
-
假设以下是我们的网站页面,其中包含了引用的css脚本、内嵌css脚本、iframe、img、引用js、内嵌js
<!DOCTYPE html> <html lang="en"> <head><link rel="stylesheet" href="style.css"> </head> <body><h2>论坛页面</h2><style nonce="argus-csp-token">button {color: blue;}</style><iframe width="300" src="https://www.youtube.com/embed/JeI_TsADXQA" allowfullscreen></iframe><img nonce="argus-csp-token" src="https://general-mac.com/images/k8s-fine.jpg?w=400" style="display: block;" alt=""><script nonce="argus-csp-token" src="http://localhost:10001/other.js"></script><script nonce="argus-csp-token">console.log("内嵌脚本执行")</script> </body> </html>
-
通过以下设置允许youtube的iframe、general-mac.com的图片、内嵌js,就是不允许其他域名的js,同时CSP-RO对于不是当前域名的样式发出报告,对于上面的网页内嵌css就会被上报,但不影响资源的加载
app.use(async (req, res, next) => {if (/\.html?[^\/]*/gi.test(req.path)) {res.header("Content-Security-Policy","img-src https://general-mac.com;script-src http://website.com 'unsafe-inline'; frame-ancestors http://website.com; frame-src https://youtu.be https://www.youtube.com; report-uri /report");res.header("Content-Security-Policy-Report-Only","style-src 'self'; report-to main-endpoint");res.header("Reporting-Endpoints",'main-endpoint="http://192.168.11.142:10000/report"');} });
在浏览器访问网页时,对于内联的css资源就会发出report报错,但不会影响样式
同时上面提供了上报的地址为
/report
,这里nodejs模拟一个上报接口:// CSP POLICY REPORT app.use("/report", (req, res) => {// 针对上报上来的数据存储分析...res.json({code: 200,report: true,}); });
上报的格式长下面这个样子,具体的字段这里就不解释了,可以查看MDN文档
:::warning 注意
report-uri已经被废弃了,请使用report-to代替,report-to需要配合Reporting-Endpoints,详情请查看文档,注意CSP Leve3还没有完全被支持
:::
除了配置CSP来限制资源的下载,还可以设置一些X-XSS-Protection、X-Frame-Options、X-Content-Type-Options、Strict-Transport-Security、X-Download-Options来加强安全防范,这些字段的具体使用可以点击查看官方文档,简单演示下:
# 启用 XSS 过滤。如果检测到攻击,阻止页面加载
X-XSS-Protection: 1; mode=block
# 该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许
X-Frame-Options: DENY
# 禁止客户端的 MIME 类型嗅探行为
X-Content-Type-Options: nosniff
# 通知浏览器应该只通过 HTTPS 访问该站点,并且以后使用 HTTP 访问该站点的所有尝试都应自动重定向到 HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains
# 告诉浏览器禁止打开下载的文件
X-Download-Options: noopen
CSRF
:::tip 温馨提示
🔍 本模块的代码可以点击这里查看
:::
CSRF(Cross-Site Request Forgery)攻击利用漏洞伪造用户的请求,使得当用户在访问一个被攻击者控制的网站时,恶意请求会被发送到一个受害者网站,从而完成攻击。比如攻击者可以伪造一个包含恶意操作的表单,当用户在攻击者控制的网站上提交这个表单时,实际上是向受害者网站发送了一个带有攻击代码的请求,从而实现攻击。CSRF攻击可以导致用户在不知情的情况下执行了恶意操作,比如修改账户信息、转账、发送垃圾邮件等
🔗 CSRF攻击演示
这里使用nodejs简单的模拟银行转账的过程,我们知道CSRF攻击是通过第三方网站进行攻击的,这里模拟两个网站,一个是正常的论坛类型网站192.168.11.143:10000
,黑客在上面发布了一条链接指向黑客网站localhost:10001
,当受害人登录后点击黑客发布的链接,就会被攻击。具体步骤如下:
-
这是正常的论坛页面,小明登录后,页面种下登录信息cookie,然后小明点击了黑客在页面上发的链接
-
黑客链接页面
localhost:10001/hack.html
很简单,隐藏了一个表单,进入页面会自动请求http://192.168.11.143:10000/transfer
并设置对应的参数amount
和to
<!-- 黑客网站 --> <form method="post" action="http://192.168.11.143:10000/transfer"> <input type="hidden" name="amount" value="10000"> <input type="hidden" name="to" value="攻击者账号"> </form><script> document.querySelector("form").submit(); </script>
-
当小明点击了黑客的链接后跳转到黑客页面后自动提交表单发起转账请求,请求也携带了小明cookie,服务器以为是小明自己发起的请求,这样就会转账成功。如下图Chrome Devtools记录了整个请求流程
以上简单的演示了攻击过程,当然黑客不可能发布这么明显的链接,通常都会用诱人的内容(如:中奖、图片等等)来勾引你点击,而就是这么简单的操作就会被攻击
💡 那么如何防止CSRF攻击呢?
防止CSRF攻击的方式还是有很多种,这里介绍常见的抵御方式
-
验证来源站点Referer、Origin:浏览器会对页面中的所有请求添加
Referer
头来标识请求来源,通过对比Referer字段来判断是不是本站请求,不是的都统统禁止掉,这里使用nginx演示不是website.com
域的都被禁止了server {location / {# 只允许 website.com相关域名可以访问,其余都会返回 403valid_referers none blocked server_names "~.*website\.com$";if ($invalid_referer) {return 403;}} }
这种方式并不是很安全,Referer头可能被篡改。而此种方式对于搜索引擎打开是硬伤,因为从搜索引擎中打开链接都会携带相关联的Referer头,如下图搜索本博客时打开,会带上google的域名,这样打开时就会被禁止掉403
除了Referer,Origin头字段也可以用来判断请求的来源,而当页面重定向时Origin头不会存在,它们都是简单的防御策略
-
使用 SameSite Cookie 属性:我们知道此类攻击是盗用了受害者的身份信息,只要限制了第三方使用身份信息也能阻止攻击。Cookie支持 SameSite 属性,其有三个值:Strict、Lax 和 None。其中,Strict 表示完全禁止第三方 Cookie,Lax 表示只允许 GET 请求时携带第三方 Cookie,None 表示允许所有请求携带第三方 Cookie。建议使用 Lax,因为在大多数情况下都足够安全
setCookie('key', 'value', {// 其他设置...sameSite: 'Lax' })
虽然samesite能够防止一定的csrf攻击,但也会对一些第三方登录等功能进行限制
-
双重验证(cookie、token、验证码):利用双重验证来提高安全性,假如每次表单提交,在页面中隐藏一个合法token,请求时携带给后端,后端拿到后验证token的合法性;再比如使用验证码,对于重要敏感的操作都进行验证码验证,提高安全性
WebSocket
WebSocket 是一种基于TCP协议的新型网络通信协议,它可以在浏览器和服务器之间建立双向实时通信的连接,可以用于实现各种实时应用,例如聊天室、多人游戏、股票行情等。在传统的 HTTP 协议中,需要使用轮询(Polling)或长轮询(Long Polling)等技术来实现实时通信,这些技术效率低下、延迟高。WebSocket 连接是长久保持的,可以支持服务器主动向客户端推送数据,而不需要客户端频繁地向服务器发送请求
websocket是一种全新的协议,和HTTP没有半点关系,为了兼容旧的http协议通常先使用HTTP请求通过Connect
和Upgrade
头进行协议升级为websocket,如果当前环境支持websocket协议就会升级成功。具体实现步骤如下:
- 客户端通过 HTTP 或 HTTPS 协议向服务器发送 WebSocket 连接请求。请求的格式与普通的 HTTP 请求相同,其中请求头包含
Connection
、Upgrade
、Sec-WebSocket-Key
、Sec-WebSocket-Version
等几个重要的头部字段:GET /chat HTTP/1.1 Host: server.example.com # 升级为 websocket Upgrade: websocket # 想要升级当前协议 Connection: Upgrade # 客户端随机生成的字符串,用于确保握手请求的唯一性 Sec-WebSocket-Key: [随机生成的字符串] # 指示客户端使用的 WebSocket 协议的版本号 Sec-WebSocket-Version: 13
- 服务器收到请求后如果允许协议升级,将会返回相应的头部字段,其中包含
Upgrade
、Connection
、Sec-WebSocket-Accept
等几个重要的头部字段:# 状态101协议转换 HTTP/1.1 101 Switching Protocols # 可以升级到websocket Upgrade: websocket # 升级协议 Connection: Upgrade # 服务器通过计算客户端的 Sec-WebSocket-Key 字段值生成的随机字符串,并进行 SHA-1 加密 # 客户端拿到此值后会对值进行验证 Sec-WebSocket-Accept: [加密后的 Sec-WebSocket-Key 字段值]
- WebSocket 连接建立成功后,客户端和服务器就可以进行实时的双向通信了。客户端和服务器可以发送文本、二进制数据等消息,消息会被分割成多个数据帧进行传输。数据帧由帧头和帧体组成,帧头包含了一些元数据,例如消息类型、消息长度等信息
📌 使用websocket简单实现多人聊天
通过简单的HTML页面和nodejs实现多人聊天,没有登录认证等相关操作,关心核心socket通信就行了,本次代码你可以在 github 中查看
简单的HTML页面(自行发挥):
<ul></ul>
<input type="text"><button>发送</button><script type="module">const ws = new WebSocket("ws://localhost:10000/socket");const input = document.querySelector("input");const btn = document.querySelector("button");const chat = document.querySelector("ul");const url = new URLSearchParams(location.search);const id = url.get("name") || "游客" + +new Date();const reader = new FileReader();ws.addEventListener("error", (err) => {console.log(err);});ws.addEventListener("open", () => {sendMessage(id + "上线了");});ws.addEventListener("message", (ev) => {if (ev.data instanceof Blob) {reader.readAsText(ev.data);reader.onload = () => {const { id: resId, msg } = JSON.parse(reader.result) || {};if (resId !== id) {const li = document.createElement("li");li.innerHTML = `-「${resId}」说:${msg}`;chat.appendChild(li);}};} else {console.log(ev.data);}});btn.addEventListener("click", () => {sendMessage(input.value);const li = document.createElement("li");li.className = "me";li.innerHTML = `「我」说:${input.value} -`;chat.appendChild(li);input.value = "";});function sendMessage(msg) {ws.send(JSON.stringify({ id, msg }));}
</script>
使用nodejs创建服务端:
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const http = require("http");
const { WebSocketServer } = require("ws");
const server = http.createServer(app);app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use((req, res, next) => {res.cookie("__ut", "emmmmm", { maxAge: 1000000000, httpOnly: true });next();
});
app.use(express.static("./static"));const wss = new WebSocketServer({server,path: "/socket",
});wss.on("connection", (ws, clientConnect) => {// console.log(clientConnect.headers.cookie);ws.on("message", (data) => {const { id, msg } = JSON.parse(data.toString("utf8"));console.log(id, msg);wss.clients.forEach((client) => {client.send(data);});});ws.send("hello");
});server.listen(10000, () => console.log("server on 10000"));
以上创建好了后,通过http://localhost:10000
访问页面,url上的name
参数表示用户的名字,没有时默认显示游客+时间戳,现在你可以打开过个tab访问此页面,进行实时通讯吧,以下是几个截图:
更完善更复杂的websocket应用还需要你自己发挥想象,这里就做这么多介绍
参考文档
- 漏洞动图演示
- MDN HTTP Docs
- MDN Web Security Policy
- HTTP安全标头
- Unleashing an Ultimate XSS Polyglot
- mozilla observatory
- xss-by-way-of-automatic
- prompt
- XSStrike
- Reporting-Endpoints HTTP Response Header Field
- TLS握手步骤
总结
本篇文章大部分偏概念总结,自己学习时要结合实际练习,这样才不会觉得那么枯燥空洞