Http学习
前言
谨以本文记录个人的http
学习之路,借鉴神三元大佬的Http灵魂只问,巩固你的Http知识体系。
1、HTTP报文结构时怎么样的?
对于TCP而言,在传输的时候分为两个部分:TCP头和数据部分
而HTTP类似,也是header + body
的结构,具体而言:
起始行 + 头部 + 空行 + 实体
HTTP
的请求报文和响应报文是有一定区别的
起始行
请求报文
请求报文起始行类似下面样式:
GET/HOME HTTP/1.1
请求报文起始行也称为请求行也就是方法+路径+http版本
响应报文
响应报文起始行类似下面样式:
HTTP/1.1 200 OK
响应报文的起始行也称为状态行。由http版本、状态码、原因三部分组成
值得注意的是,在起始行中。每两个部分之间用空格隔开,最后一个部分后面应该接一些ABNF语法规范
头部
不管是请求头还是响应头,其中的字段是相当多的,而且牵扯到http
非常多的特性,重点看看这些头部字段的格式:
- 字段名不区分大小
- 字段名允许出现空格,不可以出现下划线
_
- 字段名后面必须紧接着
:
空行
空行用来区分头部和实体
问:如果说头部中间故意加一行空行会怎么样? 那么空行后的内容全部被称为实体
实体
就是具体的数据了,也就是body
部分。请求报文对应请求体
, 响应报文对应响应体
。
2、如何理解HTTP的请求方法?
有哪些请求方法
http/1.1
规定了以下请求方法(注意,都是大写);
- GET:通常用于获取资源
- HEAD:获取资源的元信息
- POST:提交数据,即上传数据
- PUT:修改数据
- DELETE:删除资源(几乎用不到)
- CONNECT:建立连接隧道,用于代理服务器
- OPTIONS:列出可对资源实行的请求方法,用来跨域请求
- TRACE:追踪请求-响应的传输路径
GET和POST有什么区别
手写最直观的是语义上的区别
而后又有一些具体的差别
- 从缓存的角度,
GET
请求会被浏览器主动缓存下来,留下历史记录,POST
请求默认不会 - 从幂等性的角度,
GET
请求时幂等的,而POST
请求不是。(幂等表示执行相同的操作,结果也是相同的) - 从编码的角度,
GET
请求只能进行URL编码,只接受ASCII字符,而POST
请求没有限制 - 从参数的角度,
GET
请求一般放在URL中,因此不安全。PSOT
请求放在请求体中,更合适传输敏感信息 - 从TCP的角度,
GET
请求会把请求报文一次性发出去,而POST
请求会分为两个TCP数据包,首先发header
部分,如果服务器响应100(continue),然后发body
部分,(火狐
浏览器除外,他的POST之发出一个TCP包)
3、如何理解URI?
URI,全程为(Uniform Resource Identifier),也就是统一资源标识符,他的作用很简单,就是区分互联网上不同的资源
但是,它并不是我们常说的网址
,网址指的就是URI
,实际上URI
包含了URN
和URL
两个部分,由于URL过于普及,就默认将 URI 视为 URL 了。(下文将URI 视为 URL)
URL的结构
url真正最完整的结构为:
- scheme:表示协议名,比如常见的
http
,https
,file
等等。后面必须和://
连在一起。 - user:passwd@:表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用
- host:port:表示主机号和端口
- path:表示请求路径,标记资源所在位置
- query:表示查询参数,为
key=value
这种形式,多个键值对之间用&
连接 - fragment表示 URI 所定位的资源内的一个锚点,浏览器可以根据这个锚点跳转到对应的位置
举个例子
https://www.baidu.com/s?wd=HTTP&rsv_spt=1
这个URL中,https
即scheme
部分,www.baidu.com
为host:port
部分(注意,http 和 https 的默认端口分别为80、443),/s
为path
部分,wd=HTTP&rsv_spt=1
为query
部分
URL编码
URL只能使用ASCII
,ASCII
之外的字符是不支持显示的,而还有一部分符号是界定符,如果不加以处理就会导致解析出错
因此,URL引入了编码
机制,将所有非ASCII码字符和界定符转为十六进制字节值,然后在前面加个%
,如空格被转义成了%20
三元被转义成了%E4%B8%89%E5%85%83
4、如何理解HTTP状态码?
RFC规定了HTTP的状态码为三位数,本分为五类:
- 1xx:表示目前是协议处理的中间状态,还需要后续操作
- 2xx:表示成功状态
- 3xx:表示重定向状态,资源位置发生改变,需要重新请求
- 4xx:请求报文有误
- 5xx:服务器端发生错误
常见的状态码
1xx
101 Switching Protocols。在HTTP
升级为WebSocket
的时候,如果服务器同意变更,就会发送状态码 101。
2xx
200 OK 成功状态码,通常在响应体中放有数据
204 No Content 含义与200相同,打响应头后没有body数据
206 Partial Content 顾名思义,表示部分内容,它的使用场景为 HTTP 分块下载和断点续传,当然也会带上相应的响应头字段Content-Range
。
3xx
301 Moved Permanently 永久重定向,对应302 Found 临时重定向
比如你的网站从http升级为https,以前的站点不再用了,应当返回301
,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址
而如果只是暂时不可用。那么直接返回302即可,与301不同的是,浏览器不会做缓存优化
304 Not Modified 当协商缓存命中时会返回这个状态码详见浏览器缓存
4xx
400 Bad Request: 开发者经常看到一头雾水,只是笼统地提示了一下错误,并不知道哪里出错了
403 Forbidden: 这实际上并不是请求报文出错,而是服务器禁止访问,原因有很多,比如法律禁止、信息敏感。
404 Not Found: 资源未找到,表示没在服务器上找到相应的资源
405 Method Not Allowed: 请求方法不被服务器端允许
406 Not Acceptable: 资源无法满足客户端的条件
408 Request Timeout: 服务器等待了太长时间。
409 Conflict: 多个请求发生了冲突。
413 Request Entity Too Large: 请求体的数据过大。
414 Request-URI Too Long: 请求行里的 URI 太大。
429 Too Many Request: 客户端发送的请求过多。
431 Request Header Fields Too Large请求头的字段内容太大。
5xx
500 Internal Server Error: 仅仅告诉你服务器出错了,出了啥错咱也不知道。
501 Not Implemented: 表示客户端请求的功能还不支持。
502 Bad Gateway: 服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。
503 Service Unavailable: 表示服务器当前很忙,暂时无法响应服务。
5、简要概括HTTP的特点?HTTP有哪些缺点
HTTP特点
HTTP的特点概括如下:
- 灵活可扩展,主要表现在两个方面。一个是语义上的自由,之规定基本格式,比如空格分隔单词,换行分隔字段,其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅可以传输文本,还能传输图片、视频等任意数据,非常方便
- 可靠传输。HTTP基于TCP/IP,因此把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了
- 请求-应答。也就是
一发一达
、有来有回
,当然这个请求方和应答方不单单值客户端和服务器之间,如果某台服务器作为代理来连接后端服务器,那么这台服务器也会边沿请求端的角色 - 无状态。这里的状态是指通信过程的上下文信息,而每次http请求都是独立的、无关的、默认不保留状态信息
HTTP缺点
无状态
所谓的优点和缺点是要分场景来看的,对于HTTP而言,最具有争议的地方在于它的无状态
在需要长连接的场景中,需要保存大量的上下信息,避免传输大量重复的信息,那么这时候无状态就是http的缺点
但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为http的有i按
明文传输
即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式
这里当然对于调试提供了遍历,但同时让HTTP的保温信息暴漏给了外界,给攻击者提供了方便。WIFI陷阱
就是利用了HTTP明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息
队头阻塞问题
当http开启长连接时,共用一个TCP连接。同一时刻只能处理一个请求,那么当请求耗时过长的情况下,其他的请求只能处理阻塞阶段,也是就著名的队头阻塞问题。接下来一小节讨论这个问题
6、对Accept系列字段了解多少
对于Accept
系列字段的介绍分为数据格式、压缩方式、支持语言和字符集
数据格式
上一节谈到HTTP灵活的特性,它支持非常多的数据格式,那么这么多的数据一起达到客户端,客户端怎么知道他的格式?
当然,最低效的方式就是直接猜,有没有更好的方式呢?直接指定给可以吗?
答案是肯定的。不过首先需要介绍一个标准——MIME(Multipurpose Internet Mail Extensions,多用途联网邮件扩展)。他首先用在电子邮件系统中,让邮件可以发任意数据类型的数据,这对于HTTP来说也是通用的
因此,HTTP从MIME type取出了一本分来标记报文body部分的数据类型,这些数据类型体现在Content-type
这个字段,当然这是针对于发送端而言,接收端想要收到特定类型的数据,也可以使用Accept
字段
具体而言,这两个字段的取值可以分为下面几类:
- text: text/html,text/plain,text/css等
- image:image/gif,image/jepg,iamge/png等
- audio/video: audio/mepg,video/mp4等
- application(应用): application.json,application/javascript,application/pdf,application/octet-stream
压缩方式
当然一般这些数据是会进行编码压缩的,采取什么样的压缩方式体现在发送方的Content-Encoding
字段上,同样分,接受什么样的压缩方式体现在接收方的Accept-Encoding
字段。这个字段的取值有下面几种
- gizp :当今最流行的压缩格式
- deflate :另外一种著名的压缩格式
- br :一种专门为Http发明的压缩算法
//发送端
Content-Encoding :gzip
//接收端
Accept-Encoding :gzip
支持语言
对于发送方而言,还有一个Content-language
字段,在需要实现国际化的方案中,可以用来指定支持的语言,在接受方对应的字段为Accept-language
。
// 发送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en
字符集
最后一个时比较特殊的字段,在接收端对应Accept-Charset
,指定可以接受的字符集,而发送端并没有对应的Content-Charset
,而是直接放在了Content-Type
中,以charset属性指定
// 发送端
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Charset: charset=utf-8
最后以一张图总结
7、对于定长和不定长的数据,HTTP时怎么传输的?
定长包体
对于定长包体而言,发送端在纯属的时候一般会带上Content-length
,来指明包体的长度
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', 10); //长度设置
res.write("helloworld");
}
})
server.listen(3000, () => {
console.log("成功启动");
})
启动后访问:loaclhost:3000 浏览器中显示如下:
helloworld
这时候长度正确的情况下,那不正常的情况下如何处理呢? 我们试着把这个长度设置小一些
res.setHeader('Content-Length', 8);
重启服务器,再次访问,现在浏览器中内容如下:
hellowor
那么后面的ld
哪里去了?实际上在http的响应体中直接被截去了。
然后我们试着将这个长度设置得大一些:
res.setHeader('Content-Length', 12);
此时浏览器直接无法显示。可以看到Content-length
对于http传输过程起到了十分关键的作用,如果设置不当可以直接导致传输失败
不定长包体
上述针对于定长包体
,那么对于不定长包体
而言是如何传输的呢?
这里就必须介绍另一个http头部字段
Transfer-Encoding:chunked
表示分块传输数据,设置这个字段后会自动产生两个效果:
- Content-Length字段会被忽略
- 基于长连接持续推送动态内容
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.setHeader('Content-Length', 10);
res.setHeader('Transfer-Encoding', 'chunked');
res.write("<p>来啦</p>");
setTimeout(() => {
res.write("第一次传输<br/>");
}, 1000);
setTimeout(() => {
res.write("第二次传输");
res.end()
}, 2000);
}
})
server.listen(8009, () => {
console.log("成功启动");
})
第一次传输和第二次传输会分别在1s和2s后显示在网页页面
用telent抓到的相应如下
注意:Content:keep-alive
以及之前的为i相应行和响应头,后面的内容为响应体,这两部分用换行隔开
响应体的结构如下所示
chunk长度(16进制的数)
第一个chunk的内容
chunk长度(16进制的数)
第二个chunk的内容
......
0
最后留有一个空行
的。
以上便是http对于定长数据和不定长数据的传输方式
8、HTTP如何处理大文件传输
对于几百M甚至上G的大文件来说,如果要一口气传输过来显然是不显示的,会有大量的等待时间,严重影响用户体验。因此,HTTP针对这一场景,采取了范围请求
的解决方案允许客户端仅仅请求一个资源的一部分
如何支持
当然,前途是服务器支持范围请求,要支持这个功能,就比西加上一个响应头:
Accept-Ranges:none
用来告知客户端这边是支持范围请求的
Range字段拆解
对于客户端而言,它需要指定请求的哪一部分,通过Range
这个请求头字段确定,格式为bytes=x-y
,接下来就来讨论下这个Range的书写格式:
- 0-499表示开始到499个字节
- 500- 表示从第500字节到文件终点
- -100 表示文件最后100个字节
服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回
416
错误码,否则读取相应片段,返回206
状态码。
同时,服务器需要添加Content-Range
字段,这个字段的格式根据请求头中Range
字段的不同有所差异
举个例子
// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39
单段数据
对于单段数据
的请求,返回的响应如下:
HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100
i am xxxxx
值得注意的是Content-Range
字段,0-9
表示请求的返回,100
表示资源的总大小,很好理解
多段数据
得到的响应如下:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96
i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96
eex jspy e
--00000010101--
这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101
,它代表了信息量是这样的:
- 请求一定是多段数据请求
- 响应体中的分隔符是 00000010101
因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上
--
表示结束。
以上就是 http 针对大文件传输所采用的手段。
HTTP中如何处理表单数据
在 http 中,有两种主要的表单提交的方式,体现在两种不同的Content-Type
取值:
- application/x-www-form-urlencoded
- multipart/form-data
由于表单提交一般是
POST
请求,很少考虑GET
,因此这里我们将默认提交的数据放在请求体中。
application/x-www-form-urlencoded
对于application/x-www-form-urlencoded
格式的表单内容,有一下特点
- 其中的数据会被编码成以
&
分隔的键值对 - 字符以URL编码方式编码
如:
// 转换过程: {a: 1, b: 2} -> a=1&b=2 -> 如下(最终形式)
"a%3D1%26b%3D2"
multipart/form-data
对于multipart/form-data
而言
- 请求头中的
Content-Type
字段会包含boundary
,且boundary
的值有浏览器默认指定。例如:Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe
。 -数据会被分为多个部分,每两个部分之间通过分隔符来分隔,每个部分表述均有HTTP 头部描述子包体,如Content-Type
,在最后的分隔符会加上--
表示结束。 相应的请求体
是下面这样:
Content-Disposition: form-data;name="data1";
Content-Type: text/plain
data1
----WebkitFormBoundaryRRJKeWfHPGrS4LKe
Content-Disposition: form-data;name="data2";
Content-Type: text/plain
data2
----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
小结
值得一提的是,multipart/form-data
格式最大的特点在于:每一个表单元素都是独立的资源表述。另外,你可能在写业务的过程中,并没有注意到其中还有boundary
的存在,如果你打开抓包工具,确实可以看到不同的表单元素被拆分开了,之所以在平时感觉不到,是以为浏览器和 HTTP 给你封装了这一系列操作。
而且,在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data
而不用application/x-www-form-urlencoded
,因为没有必要做 URL 编码,带来巨大耗时的同时也占用了更多的空间。
10、HTTP1.1 如何解决 HTTP 的队头阻塞问题?
什么是 HTTP队头阻塞
从前面的小节可以看出,HTTP传输是基于请求-应答模式进行了的,报文必须是一发一收,但值得注意的是,里面的任务被放在一个队列任务串中执行,一旦队首的请求处理太慢,就会阻塞后面的请求处理。这就是著名的HTTP队头阻塞问题
并发连接
对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其他所有的任务。在RFC2616规定客户端最多并发2个连接,不过事实上现在的浏览器标准中,这个上线要多的,Chrome中是6个
但其实,是提高了并发连接,还是不能满足人们对性能的需求。
域名分片
一个域名不是可以并发 6 个长连接吗?那我就多分几个域名。 比如 content1.baidu.com 、content2.baidu.com。
这样一个baidu.com
域名下可以分出非常多的二级域名,而他们都指向同样的一个服务器,并能够并发的长连接数更多了,事实上更好的解决的对头阻塞问题
11、队Cookie了解多少?
Cookie简介
前面说到了HTTP是一个无状态的协议,每次http都是独立的、无关的,默认不需要保留状态信息,但有时候需要保存一些状态,怎么办呢?
HTPP为此引入了Cookie。Cookie本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在chrome开发者面板的Application这一栏可以看到)。向同一域名下发送请求,都会携带相同的Cookie,服务器拿到Cookie进行解析,便能拿到客户端状态。而服务端可以通过响应头中的Set-Cookie
字段来对客户端写入Cookie
。举例如下:
// 请求头
Cookie: a=xxx;b=xxx
// 响应头
Set-Cookie: a=xxx
set-Cookie: b=xxx
Cookie属性
生命周期
Cookie的有效期可以通过Expires和Max-Age两个属性来设置
- Expires即
过期时间
- Max-Age用的是一段时间间隔,单位是秒,从浏览器收到报文开始计算。
若Cookie过期,则这个Cookie会被删除,并不会发送给服务端
作用域
关于作用域也有两个属性:Domain和path,给Cookie绑定了域名和路径,在发送请求之前,发先域名或者路径和这两个属性不匹配。那么就不会带上Cookie。值得注意的是,对于路径来说。/
表示域名下的任意路径都允许使用Cookie
安全相关
如果带上Secure
,说明只能通过Https来传输Cookie
如果Cookie字段带上Httponly
,那么说明只能通过HTTP协议传输,不能通过JS访问,这也是预防XSS攻击的重要手段。
相应的,对于CSRF攻击的预防,也有SameSite
属性。
SameSite
可以设置三个值,Strict
、Lax
、和None
.
-
在在
Strict
模式下,浏览器完全禁止第三方请求携带Cookie。比如请求baidu.com
网站只能在baidu.com
域名当中请求才能携带Cookie。在其他网站请求都不能。 -
在
Lax
模式,就宽松一点,但只能在get方法提交表单或者a标签发送get请求的情况下可以携带Cookie,其他情况均不能 -
在
None
模式下,也就是默认模式,请求会自动带上Cookie
Cookie的缺点
- 容量缺陷。Cookie的提及上线只有
4KB
,只能用来存储少量信息 - 性能缺陷。 Cookie紧跟域名,不关玉明下面的某个地址需不需要Cookie,请求都会携带完整的Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了血多不必要的内容。但可以通过
Domain
和Path
指定作用域来解决。 - 安全缺陷。由于Cookie以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一些列的篡改,在Cookie的有效期内重新发送给服务器,这是非常危险的。另外,在
HttpOnly
为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
12、如何理解Http代理?
我们知道在HTTP是基于请求-响应
模型的响应协议,一般由客户端发送请求,服务器来进行响应。
当然,也有特殊情况,就是代理服务器的情况,引入代理后,作为代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份
那代理服务器到底是用来做什么的?
功能
- 负载均衡。哭护短的请求只会先到达代理服务器。,后面到底有多少源服务器,IP都是多少,客户端是不知道的。因此,这个代理服务器可以拿到这个请求周,可以通过特定的算法分发给不同的服务器,让各源服务器的负载尽量平均。当然,这样的算法很多,包括随机算法、轮询、一致性hash、LRU(最近很少使用) 等等
- 保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就会将其踢出集群。并且对于上下行的数据进行过滤,对于非法IP限流,这些都是代理服务器作用 3.缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获取到而不用到源服务器哪里。
相关头部字段
Via
代理服务器需要表明自己的身份,在HTTP传输中留下自己的痕迹,怎么办呢?
通过Via
字段来记录。举个例子,现在中间有两台代理服务器,在客户端发送请求后会经理这样的一个过程
客户端 -> 代理1 -> 代理2 -> 源服务器
在源服务器收到请求后,会在请求头
拿到这个字段
Via: proxy_server1, proxy_server2
而源服务器响应式,最终在客户端拿到这样的响应头
:
Via: proxy_server2, proxy_server1
X-Forwarded-For
字面意思就是为谁转发
,它记录的是请求方的IP
地址(注意,和Via
区分开,X-Forwarded-For
记录的是请求方这一个IP)。
X-Real-IP
是一种获取用户真实IP的字段,不管中间经过多少代理,这个字段始终记录最初的客户端的IP
相应的,还有X-Forwarded-Host
和X-Forwarded-Proto
,分别记录客户端(注意哦,不包括代理)的域名
和协议名
。
X-Forwarded-For产生的问题
前面可以看到,X-Forwarded-For
这个字段记录的是请求方的IP,这意味着没经过一个不同的代理,这个字段的名字都要变,从客户端
到代理1
,这个字段是客户端的 IP,从代理1
到代理2
,这个字段就变为了代理1的 IP。
但是这会产生两个问题:
- 意味着代理必须解析HTTP请求头然后修改,比直接转发数据性能下降
- 在HTTPS通信加密过程中,原始报文时不允许修改的
由此产生了代理协议,一般使用明文版本,只需要在Http请求行上面加上这样的文本即可
// PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
...
这样就可以解决X-Forwarded-For
带来的问题了。
13、如何理解HTTP缓存
首先通过 Cache-Control
验证强缓存是否可用
-
如果强缓存可用,直接使用
-
否则进入协商缓存,即发送 HTTP 请求,服务器通过请求头中的
If-Modified-Since
或者If-None-Match
这些条件请求字段检查资源是否更新- 若资源更新,返回资源和200状态码
- 否则,返回304,告诉浏览器直接从缓存获取资源
14、什么是跨域?浏览器如何拦截响应?如何解决?
在前后端分离的开发模式中,经常会遇到跨域问题,即Ajax请求发出去了,服务器也响应成功了,前端就是拿不到响应。接下来,我们就来好好讨论下这个问题。
什么是跨域?
回归URL的组成
浏览器遵循同源政策(scheme(协议)
、host(主机)
和port(端口)
都相同则为同源
。非同源站点有这样一些限制:
- 不能读取和修改对方的DOM
- 不能访问对方的Cooke、IndexDB和LocalStroage
- 限制XMLHttpRequest请求。(后面的话题着重围绕这个)
当浏览器向目标URL发Ajax请求时,只要当前的URL和目标URL不同源,则产生跨域,被称为跨域请求
。
跨域请求的响应一般会被浏览器拦截,注意,是被浏览器拦截,相应其实是成功到达客户端。那这个拦截是如何发生的?
首先要知道,浏览器是多进程的,以Chrome为例
,进程组成如下
WebKit 渲染引擎和V8 引擎都在渲染进程当中
当
xhr.send
被调用,即Ajax请求准备发送的时候,其实还只是在渲染进程的处理。为了繁殖黑客通过脚本触碰到系统资源,浏览器将媒体分渲染进程装进了沙箱,并且为了防止CPU芯片一直存在的Spectre和Meltdown漏洞,采取了站点隔离
的手段,给每一个不同的站点(一级域名不同)分配了沙箱,互不干扰。具体见YouTube上Chromium安全团队的演讲视频。
在沙箱中的渲染进程是没办法发送网络请求的,那怎么办?只能通过网络进程来发送。这就涉及到进程间通信(IPC,Inter Process Communication)了。接下来我们看看chromium当中进程通信是如何完成的在 chromium 源码中调用顺序如下:
可能看了你会比较懵,如果想深入了解可以去看看 chromium 最新的源代码,IPC源码地址及Chromium IPC源码解析文章。
总的来说就是利用
Unix Domain Socket
套接字,配合事件驱动的高性能网络并发库libevent
完成进程的 IPC 过程。
好,现在数据传递给了浏览器主进程,主进程接收到后,才真正地发出相应的网络请求。
在服务端处理完数据后,将响应返回,主线程检查到跨域,且没有cors响应头,将相应全部丢掉,并且不会发送给渲染进程,这就达到了拦截数据目的
接下来我们来说一说解决跨域问题的几种方案
CORS
CORS其实是W3C的一个标准,全程是跨域资源共享
。他需要浏览器和服务器他需要服务器和浏览器的共同支持,具体来收,非IE和IE10以上支持CORS,服务器需要附加特定的响应头,后面具体拆解。不过在弄清楚CORS的原理之前,我们需要清除两个概念:简单请求和非简单请求。
浏览器根据请求方法和请求头的特定字段,将请求做了一下分类,具体来说规则是这样,范式满足下面条件的属于简单请求:
- 请求方法为GET、POST或者HEAD
- 请求头的取值范围:: Accept、Accept-Language、Content-Language、Content-Type(只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)
浏览器画了这样一个圈,在这个圈里面就是简单请求,圈外就是非简单请求,然后针对这两个不同的请求进行不同的处理
简单请求
请求发出去之后,浏览器做了什么?
他会自动在请求头中,添加一个Origin
字段,用来说明请求来自哪个源
。服务器拿到请求之后,在回应时对应的添加Access-Control-Allow-Origin
字段,如果Origin
不在这个字段的范围中,那么浏览器就会将响应拦截。
因此,Access-Control-Allow-Origin
字段是服务器用来决定浏览器是否拦截这个响应,这是必需的字段。与此同时,其它一些可选的功能性的字段,用来描述如果不会拦截,这些字段将会发挥各自的作用。
Access-Control-Allow-Credentials。这个字段是一个布尔值,不熬是是允许发送Cookie,对于跨域请求,浏览器对这个字段默认值为false。而如果需要拿到浏览器的Cookie,需要添加这个响应头并设置true
,并且在前端也需要设置withCredentials
属性:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
Access-Control-Expose-Headers。这个字段是给XMLHttpRequest对象赋能,让她不仅可以拿到基本的6个响应头字段(包括Cache-Control
、Content-Type
、Expires
、Last-Modified
、Pragma
),还能拿到这个字段声明的响应头字段。比如这样设置:
Access-Control-Expose-Headers: aaa
那么在前端可以通过 XMLHttpRequest.getResponseHeader('aaa')
拿到 aaa
这个字段的值
非简单请求
非简单请求相对而言会有些不同,体现在两个方面:预检请求和响应字段
我们已PUT方法为例
var url = "http://xxx.com"
var xhr = new XMLHttpRequest()
xhr.open("PUT",URL,True)
xhr.setRequestHeader('X-Custom-Header', 'xxx');
xhr.send();
当这段代码执行后,首先会发送预检请求。这个预检请求的请求行和请求体是下面这个格式
OPTIONS / HTTP/1.1
Origin:当前地址
Host:xxx.com
Access-Control-Request-Method : PUT
Access-Control-Request-Headers:X-Custom-Header
预检请求的方法是options
,同时会加上Origin
源地址和Host
目标地址,这很简单。同时也会加上两个关键字段:
- Access-Control-Request-Method,列出CORS请求用到那个HTTP方法
- Access-Control-Request-Headers,指出CORS请求将要加上什么请求头
这是预检请求。接下来是响应字段,响应字段也分为两部分,一部分是对于预检请求的响应,一部分是对于CORS请求的响应
预检请求的响应,如下面的格式:
HTTP/1.1 200 OK
Access-Control-Allow-Origin : *
Access-Control-Allow-Methods: GET,PUT,POST
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Content-Type: text/html;
charset=utf-8 Content-Encoding: gzip
Content-Length: 0
其中有这样几个关键的响应头字段
- Access-Control-Allow-Origin :表示可以允许请求的源,可以填具体的源名,也可以填
*
表示允许任意源的请求 - Access-Control-Allow-Methods:表示允许的请求方法列表
- Access-Control-Allow-Credentials: 简单请求中已经介绍。
- Access-Control-Max-Age :预检请求的有效期,在此期间,不用发出另外一条预检请求
在预检请求的响应返回后,如果请求不满足响应头的条件,则触发XMLHttpRequset
的onerror
方法,当然后面真正的CORS请求也不会再发出去了
饶了这么一大转,到了真正的CORS请求就容易多了,现在它和简单请求的情况是一样的,浏览器自动加上
Origin
字段,服务端响应头返回Access-Control-Allow-Origin。参考上面简单请求部分的内容
JSONP
虽然XMLHttpRequest
对象遵循同源政策,但是script
标签不一样,它可以通过src填上目标地址从而发出GET请求,实现跨域请求并拿到响应。这就是JSONP的原理
const jsonp = ({url,params,callbackName})=>{
cosnt generateURL = ()=>{
let dataSrc = "",
for(let key in params){
dataSrc += `${key}=${params[key]}&`
}
dataStr += `callback=${callbackName}`
return `${url}?${dataStr}`
}
retrun new Promise((resolve,reject)=>{
//初始化回调函数名称
callbackName = callbackName || Math.random().toString.replace("," , "")
// 创建 script 元素并加入到当前文档中
let scriptEl = document.createElement("script")
scriptEl.src = generateUrl()
document.body.appendChild(scriptEl)
// 绑定到 window 上,为了后面调用
window[callbackName] = (data)=>{
resolve(data)
// script 执行完了,成为无用元素,需要清除
document.body.removeChild(scriptEle);
}
})
}
服务器端响应操作,以express为例:
let express = require('express')
let app = express()
app.get('/', function(req, res) {
let { a, b, callback } = req.query
console.log(a); // 1
console.log(b); // 2
// 注意哦,返回给script标签,浏览器直接把这部分字符串执行
res.end(`${callback}('数据包')`);
})
app.listen(3000)
前端简单调用一下就好
jsonp({
url: 'http://localhost:3000',
params: {
a: 1,
b: 2
}
}).then(data => {
// 拿到数据进行处理
console.log(data); // 数据包
})
和CORS
相比,JSONP 最大的优势在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺点也很明显,请求方法单一,只支持 GET 请求
Nginx
Nginx是一种高性能的反向代理
服务器,可以用来轻松解决跨域问题
what? 反向代理?
正向代理帮助客户端访问
客户端访问不到的服务器,然后将结果返回给客户端
反向代理是拿到客户端的请求,将请求转发给其他服务器,主要的场景是维持服务器集群的负载均衡,换句话说:反向代理帮其他的服务器拿到请求,然后选择一个合适的服务器,将请求转发给它
因此,两者的区别就很明显了,正向代理是帮客户端做事情,而反向代理服务器是帮其他的服务器做事情
好了,那 Nginx 是如何来解决跨域的呢?
比如说现在客户端的域名为client.com,服务器的域名为erver.com。客户端想服务器发送Ajax请求,当然会跨域,在这个时候Nginx登场了,通过下面的配置:
server {
listen 80 ;
server_name client.com
location/api {
porxy_pass server.com
}
}
Nginx相当于一个跳板机,这个跳板机的域名也是client.com
,让客户端首先访问client.com/api
,这当然没有跨域,然后Nginx服务器作为反向代理,将请求转发给server.com
,当响应返回时又将响应给到可魂断,这就完成整个跨域请求的过程。
15、TLS1.2握手的过程是怎么样的?
之前谈到了HTTP是明文输出的协议,纯属报文对外完全透明,非常不安全,那么如何进一步保证安全性?
由此产生了HTTPS
,其实它并不是一个新的协议,而是在HTTP下面增加了一层SSL/TLS协议,简单讲:HTTPS = HTTP + SSL/TLS