HTTP
从输入URL到页面加载发生了什么
- DNS解析:将域名解析成IP地址
- TCP连接:TCP三次握手
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束:TCP四次挥手
1、DNS解析
在浏览器输入URL后,首先要经过域名解析。浏览器通过向 DNS 服务器发送域名,DNS 服务器查询到与域名相对应的 IP 地址,然后返回给浏览器,浏览器再将 IP 地址打在协议上,同时请求参数也会在协议搭载,然后一并发送给对应的服务器。
1.什么是URL URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。比如 http://www.w3school.com.cn/ht..., 遵守以下的语法规则: `scheme://host.domain:port/path/filename` 各部分解释如下: scheme:定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http,而 https 则是进行加密的网络传输。 host:定义域主机(http 的默认主机是 www) domain:定义因特网域名,比如 w3school.com.cn port:定义主机上的端口号(http 的默认端口号是 80) path: 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。 filename: 定义文档/资源的名称 2. 什么是DNS DNS(domain name system,域名系统):因特网上域名和IP地址相互映射的分布式数据库;简单理解就是域名与IP地址的对照表,因为域名(如:www.google.com)对于我们而言,更便于记忆,但是机器却不擅长这种表达方式,因此需要将域名转换为IP地址,以便于机器识别, 这便有了DNS。 3. 根域名服务器 根服务器是架设互联网的必须设施,管理互联网的主目录,全球共有13套根域名服务器 4. 递归查询 客户端主机向本地域名服务器的查询是递归查询;所谓递归查询:客户端主机查询的域名地址无法在本地域名服务器中找到,因此本地域名服务器就以DNS客户端的身份向其他根域名服务器发起请求,进行查询,而不是让客户端主机去一直查询; 递归查询的结果要么是返回的IP地址,要么是报错,表示无法查询到地址; 5. 迭代查询 本地域名服务器向根服务器、顶级域名服务器和主机域名服务器发起的查询请求就是迭代的过程,如:本地域名服务器向根服务器发起查询请求,根服务器中会告诉本地域名服务器:”我这里没有你要找的内容,你去顶级域名服务器上找吧“,
并将顶级域名服务器的地址返回给本地域名服务器,本地域名服务器接收到后,继续向顶级域名服务器发送请求;顶级域名服务器要么返回ip地址,要么告诉本地域名服务器下一步要向哪个权限域名服务器发送请求,直到找到ip地址或找不到ip返回报错信息,然后信息返回给客户端主机; 下图给出了这两种查询的差别 递归过程:主机→本地DNS服务器→其他DNS服务器(如:我要找一个苹果吃,找到了A,问A有没有,A说我帮你去找B,B可能有,果真B有,然后B将苹果给了A,A再将苹果给我,这就是递归) 迭代过程:本地DNS服务器→根服务器,本地DNS服务器→顶级域名服务器,本地DNS服务器→权限域名服务器;(如:我要找一个苹果,找到了A,A说我也没有,B可能有,你去找B吧;我又找B,B说我也没有,你去找C吧,我又去找C,终于找到了苹果,这就是迭代的过程)
2、TCP连接:TCP三次握手
在客户端发送数据之前会发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息。
说明:
Ack:应答
Fin:结束; 结束会话
Seq: 一个数据段的第一个序列号
SYN: 同步; 表示开始会话请求
第一次握手:客户端A将标志位SYN置为1,随机产生一个值为seq=X(X的取值范围为=1234567)的数据包到服务器,客户端A进入SYN_SENT状态,等待服务端B确认(第一次握手,由浏览器发起,告诉服务器我要发送请求了);
第二次握手:服务端B收到数据包后由标志位SYN=1知道客户端A请求建立连接,服务端B将标志位SYN和ACK都置为1,ack=X+1,随机产生一个值seq=Y,并将该数据包发送给客户端A以确认连接请求,服务端B进入SYN_RCVD状态(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)。
第三次握手:客户端A收到确认后,检查ack是否为X+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=Y+1,并将该数据包发送给服务端B,服务端B检查ack是否为Y+1,ACK是否为1,如果正确则连接建立成功,客户端A和服务端B进入ESTABLISHED状态,完成三次握手,随后客户端A与服务端B之间可以开始传输数据了(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)。
为什需要三次握手? 计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。 书中的例子是这样的,“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。 假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”。主要目的防止server端一直等待,浪费资源。
3、浏览器向web服务器发送HTTP请求
TCP三次握手之后,开始发送HTTP请求报文至服务器
HTTP请求报文格式:请求行+请求头+空行+消息体,请求行包括请求方式(GET/POST/DELETE/PUT)、请求资源路径(URL)、HTTP版本号;
4、服务器处理请求并返回HTTP报文
服务器收到请求后会发出应答,即响应数据。HTTP响应与HTTP请求相似, HTTP响应报文格式:状态行+响应头+空行+消息体,状态行包括HTTP版本号、状态码、状态说明。
5、浏览器解析渲染页面
浏览器拿到响应文本后,解析HTML代码,请求js,css等资源,最后进行页面渲染,呈现给用户。页面渲染一般分为以下几个步骤:
(1)根据HTML文件解析出DOM Tree
(2)根据CSS解析出 CSSOM Tree(CSS规则树)
(3)将 DOM Tree 和 CSSOM Tree合并,构建Render tree(渲染树)
(4)reflow(重排):根据Render tree进行节点信息计算(Layout)
(5)repaint(重绘):根据计算好的信息绘制整个页面(Painting)
6、TCP四次挥手。
当数据传输完毕,需要断开TCP连接,此时发起tcp四次挥手
1、客户端向服务端发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。(由浏览器告诉服务器,我请求报文发送完了,你准备关闭吧)
2、服务端向客户端发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。(由服务器告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)
3、服务端向客户端发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。(由服务器告诉浏览器,我响应报文发送完了,你准备关闭吧)
4、客户端向服务端发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。(由浏览器告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)
简单说就是:
1、A——>B :A告诉B:“我发完了”;
2、B——>A:B告诉A:“好的,我知道你发完了”
3、B——>A:B告诉A:“我收完了”;
4、A——>B:A告诉B:“好的,我知道你发收完了”
async和defer的作用和区别
defer和async是script标签的两个属性,用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。
我们先来了解页面文档的渲染机制
- 浏览器通过HTTP协议请求服务器,获取HMTL文档并开始从上到下解析,构建DOM。
- 在构建DOM过程中,如果遇到外联的样式声明和脚本声明,则暂停文档解析,创建新的网络连接,并开始下载样式文件和脚本文件。
- 样式文件下载完成后,构建CSSDOM;脚本文件下载完成后,解释并执行,然后继续解析文档构建DOM 。
- 完成文档解析后,将DOM和CSSDOM进行关联和映射,最后将视图渲染到浏览器窗口。
在这个过程中,JS脚本文件的下载和执行是与文档解析同步进行的,如果JS出现堵塞,将会影响页面渲染,出现白屏、FOUC等现象,影响用户体验。因此我们需要使用defer和async来控制脚本异步加载。
defer和async的作用如下:
- defer:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
- async:用于异步下载脚本文件,下载完毕立即解释执行代码。
下图可以更清楚地阐述defer和async的执行以及和DOMContentLoaded、load事件的关系:
绿色线是页面解析
蓝色线是JS下载
红色线是JS执行
我们可以明显看到defer和async的脚本下载都是异步进行的,而两者区别是defer要在页面解析完成后执行脚本,async是下载完脚本立刻执行脚本,同时async会影响脚本的解析。
下面是defer和async基本语法:
<script async src="script.js"></script>
<script defer src="script.js"></script>
关于defer我们需要注意下面几点:
- defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer;
- 如果有多个声明了defer的脚本,则会按顺序下载和执行 ;
- defer脚本会在DOMContentLoaded和load事件之前执行。
关于async,也需要注意以下几点:
- 只适用于外联脚本,这一点和defer一致;
- 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序;
- async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序 。
HTTP status code
1、消息
这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束。由于 HTTP/1.0 协议中没有定义任何 1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。
201 Created
204 No Content
服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。
如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。
由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。
3、重定向
301 Move Permanently 永久重定向
302 Found 临时重定向
304 Not Modify 缓存
4、请求错误
400 Bad Request 客户端发送的语义有误,当前请求无法被服务器理解
401 Unauthorized当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。
403 Forbidden服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个 HEAD 请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息
404 Not Found
405 Method Not Allowed 一般跨域
414 URI Too Long
5、服务器错误
500 Internal Server Error 服务端错误
502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
504 Gateway Timeout 请求超时了
GET 和 POST 的区别
- 根据技术文档规格,GET 和 POST 最大区别就是语义,一个读一个写
- 实践上会有很多区别,如:
- 由于 GET 是读,POST 是写。所以 GET 是幂等的,POST 是不幂等的
- 由于 GET 是读,POST 是写。所以 GET 结果会被缓存,POST 结果不会被缓存
- 由于 GET 是读,POST 是写。所以 GET 打开的页面刷新是无害的,POST 打开的页面刷新需要确认
- 通常情况下,GET 请求参数放置在 URL 里,POST 请求参数放在 body 里
- GET 比 POST 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息
- GET 请求参数放在 URL 里是有长度限制的(浏览器限制的,414 URI to long),而 POST 放在 body 里没有长度限制(长度其实可是配置)
- GET 产生一个 TCP 数据包,POST 产生两个或以上 TCP 数据包
简单请求 vs 复杂请求
- 简单请求不会触发 CORS 预检请求
- 以下条件是简单请求:复杂请求会触发 CORS 预检请求
- method => GET | POST
- header => 需要关注 Content-Type => text/plain | multipart/form-data | application/x-www-form-urlencoded
- 预检请求 => 首先使用 OPTION 方法发起一个预检请求到服务器,已获知服务器是否允许该实际请求
Cookie
- Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上
- 通过 Set-Cookie 设置,是一个 key=value 结构
- Expires | Max-Age => 指明过期时间,只与客户端有关
- HttpOnly => 保证 Cookie 不会被脚本访问 => JS document.cookie API 无法访问带有 HttpOnly 属性的 Cookie
- Domain | Path => 允许 Cookie 应该发送给哪些 URL
HTTP 缓存有哪些方案?
- HTTP 缓存分为强缓存(缓存)和弱缓存(内容协商)
- Exprise 是一个过期时间,浏览器发送请求,服务器返回的时候在响应头里加exprise字段,值就是过期时间,但是当服务端与客户端时间不一致,缓存就不准确了,于是就有了Cache-Control(过期时长)
- HTTP 1.0 时代
- 强缓存(缓存)
- Expires => 以电脑本地时间为准
- Last-Modified => 浏览器请求的时候 服务器返回 Last-modified 以后浏览器每次请求都带上if-modified-Since ,值就是Last-Modified的值,服务器与资源最后修改时间做对比,如果没过时 就代表可以用返回304, 如果过时了,就正常给浏览器返回最新资源 200状态。但是它是以秒级别记录的,一个文件1s内更改多次,无法区分是否是最新的
- 弱缓存(内容协商)
- if-Modified-Since
- 状态码:304 或 200
- 强缓存(缓存)
- HTTP 1.1 时代
- 强缓存(缓存)
- 在 response header 中添加 Cache-Control: max-age = 3600,浏览器会自动缓存一个小时,如果在此时间内,再次访问相同的 url(path + query),直接不发送这个请求
- 在 response header 中添加 Etag:ABC,代表该文件的特征值,与if-None-Match对比。
- 弱缓存(内容协商)
- 强缓存过期之后,该缓存是否可以继续使用 => request header 中添加 if-None-Match: ABC => 浏览器向服务器发送的一个询问
- 服务器返回相应的状态码:304(Not Modified,继续使用缓存的内容) 或 200(使用新的文件,其中 response header 中包含了 Cache-Control 和 Etag)
- 强缓存(缓存)
总结:先是Exprise=》Cache-Control=》Last-modified(if-modified-Since)=》Etag(if-None-Match)
DNS域名解析:
1、本地解析:先从浏览器DNS缓存=》操作系统的DNS缓存=》本地HOST文件=》互联网域名服务器解析(本地DNS服务器=》根服务器=》com顶级域名服务器=》baidu.com权威服务器=》找到了)
HTTP 和 HTTPS 的区别
- HTTPS = HTTP + SSL/TLS(安全层)
- HTTP 是明文传输的,不安全。HTTPS 是加密传输的,非常安全
- HTTP 使用 80 端口。HTTPS 使用 443 端口
- HTTP 较快。HTTPS 较慢
- HTTP 不需要证书。HTTPS 需要证书
HTTP/1.1 和 HTTP/2 的区别有哪些?
- HTTP/2 使用二进制传输,并且将 head 和 body 分成帧来传输。HTTP/1.1 是字符串传输
- HTTP/2 支持多路复用,一个 TCP 连接可以发送多个请求。HTTP/1.1 不支持,一个请求建立一个 TCP 连接。多路复用就是一个 TCP 连接从单车道变成了几百个双向通行的车道
- HTTP/2 可以压缩 head。HTTP/1.1 不可以
- HTTP/2 支持服务器推送。HTTP/1.1 不支持
TCP 三次握手和四次挥手
- ACK => acknowledge => 接受
- RCVD => received => 收到
- SYN => synchronize => 同步
- seq => sequence => 顺序
- EATABLISHED => established => 已建立
- 三次握手
- 浏览器向服务器发送 TCP 数据 => SYN(seq = x)
- 服务器向浏览器发送 TCP 数据 => SYN(seq = y), ACK = x + 1
- 浏览器向服务器发送 TCP 数据 => ACK = y + 1
- 四次挥手
- 浏览器向服务器发送 TCP 数据 => FIN(seq = x + 2), ACK = y + 1
- 服务器向浏览器发送 TCP 数据 => ACK = x + 3
- 服务器向浏览器发送 TCP 数据 => FIN(seq = y + 1)
- 浏览器向服务器发送 TCP 数据 => ACK = y + 2
同源策略和跨域
同源指的是 protocol + host + port 相同便是同源的
同源策略用于控制不同源之间的交互。跨源写和跨源资源嵌入一般是允许的,但是跨源读操作一般是不允许的。
只要在浏览器里打开页面,默认遵守同源策略
保证了用户的隐私安全和数据安全
很多时候前端需要访问另一个域名的后端接口,此时浏览器会将响应屏蔽,并报错 CORS
解决跨域 :
一、JSONP
-
JSONP 是一种利用
<script>
标签进行跨域请求的方式。它的原理是在请求地址中添加一个回调函数名,服务端在返回数据时会将数据作为参数传入该回调函数中,并返回给客户端。客户端则可以通过定义这个回调函数来处理返回的数据。function jsonp(url, callback) {
使用场景:
const script = document.createElement('script');
script.src = `${url}&callback=${callback}`;
document.body.appendChild(script);
window[callback] = function(data) {
document.body.removeChild(script);
delete window[callback];
callback(data);
}
}
jsonp('http://example.com/data', function(data) {
console.log(data);
});- 只支持 GET 请求。
- 能够支持跨域请求,但只能处理 JSON 数据。
- 适用于数据量较小、不需要特别保密的场景。
- 优点:实现简单、支持跨域请求。
- 缺点:只能处理 JSON 数据、无法处理错误信息、存在安全风险。
CORS 是一种官方推荐的跨域解决方案。它的原理是在发送请求时,服务端在响应头中添加一个
Access-Control-Allow-Origin
字段,标识允许哪个源进行访问。客户端则可以通过设置withCredentials
属性来携带认证信息,以及在响应头中获取对应的字段。示例代码:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/data');
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
console.log(xhr.responseText);
}
};
xhr.send();使用场景:
-
支持 GET、POST 等类型的请求。
-
能够支持跨域请求,同时支持多种数据类型。
-
适用于大部分的场景。
优缺点:
-
优点:能够支持跨域请求、支持多种数据类型。
-
缺点:需要服务端配合设置响应头、性能稍低。
三、WebSocket
WebSocket 是一种双向通信协议,它可以在浏览器和服务端之间建立一个持久化的连接,以实现实时通信。由于它是基于 HTTP 协议的,因此支持跨域请求。
示例代码:
const ws = new WebSocket('ws://example.com/socket');
ws.onmessage = function(event) {
console.log(event.data);
};使用场景:
-
可以实现双向通信、实时更新。
-
适用于聊天室、游戏等实时性较高的场景。
优缺点:
-
优点:能够实现双向通信、实时更新。
-
缺点:需要服务端配合实现、只能处理文本数据、不稳定。
四、postMessage
postMessage 是 HTML5 中新增的一种跨窗口通信机制,它可以在不同窗口、甚至不同域名之间进行数据传递。它的原理是通过调用
window.postMessage()
方法,在目标窗口中触发一个message 事件,从而实现数据的传递。示例代码:
// 发送消息
window.parent.postMessage('hello', 'http://example.com');
// 接收消息
window.addEventListener('message', function(event) {
if (event.origin === 'http://example.com') {
console.log(event.data);
}
});使用场景:
-
可以进行不同窗口、不同域名之间的数据传递。
-
适用于嵌套页面、多窗口通信等场景。
优缺点:
-
优点:可以进行不同窗口、不同域名之间的数据传递。
-
缺点:需要进行安全性检查、可能存在 XSS 攻击风险。
五、nginx 反向代理
通过配置 nginx 的反向代理,可以实现对目标地址的访问,并将响应返回给客户端。由于是在服务端进行请求,因此不会受到浏览器的同源策略限制。
示例代码:
location /api/ {
proxy_pass http://example.com/data/;
}使用场景:
-
用于解决前端直接访问跨域 API 的问题。
-
适用于请求量较大、需要保密的场景。
优缺点:
-
优点:能够支持跨域请求、保护了 API 的安全性。
-
缺点:需要配置服务器,增加了服务器端的压力。
六、HTML5 XHR2
HTML5 中新增的 XHR2 对象,支持跨域请求,并且可以处理二进制数据和进度信息。
示例代码:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/data');
xhr.responseType = 'blob';
xhr.onload = function() {
const reader = new FileReader();
reader.onloadend = function() {
console.log(reader.result);
};
reader.readAsText(xhr.response);
};
xhr.send();使用场景:
-
支持跨域请求,能够处理二进制数据和进度信息。
-
适用于需要上传、下载文件等场景。
优缺点:
-
优点:支持跨域请求、能够处理二进制数据和进度信息。
-
缺点:浏览器兼容性不一、需要服务端配合设置响应头。
七、document.domain
通过设置 document.domain 的值,可以实现不同子域名之间的跨域通信。但是该方式只适用于主域名相同、子域名不同的情况下。
示例代码:
// 在 A 页面中设置
document.domain = 'example.com';
// 在 B 页面中设置
document.domain = 'example.com';使用场景:
-
只适用于主域名相同、子域名不同的情况下。
-
适用于不同子域名之间进行数据传递的场景。
优缺点:
-
优点:实现简单、不需要浏览器支持。
-
缺点:只适用于主域名相同、子域名不同的情况下。
八、iframe
通过在页面中插入一个 iframe,可以在其中加载目标页面,从而实现跨域通信。但是该方式存在一些安全问题,容易遭受 CSRF 攻击。
示例代码:
<iframe src="http://example.com"></iframe>
使用场景:
-
适用于嵌套页面、跨域数据传输等场景。
优缺点:
-
优点:能够实现跨域通信。
-
缺点:存在安全风险、容易遭受 CSRF 攻击。
CSRF 攻击:
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
一个典型的CSRF攻击有着如下的流程:
- 受害者登录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大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。
在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:
- Origin Header 只是域名 不是路径
- Referer Header
这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。 服务器可以通过解析这两个Header中的域名,确定请求的来源域。
但是Origin在以下两种情况下并不存在:
-
IE11同源策略: IE 11 不会在跨站CORS请求上添加Origin标头,Referer头将仍然是唯一的标识。最根本原因是因为IE 11对同源的定义和其他浏览器有不同,有两个主要的区别,可以参考MDN Same-origin_policy#IE_Exceptions
-
302重定向: 在302重定向之后Origin不包含在重定向的请求中,因为Origin可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的URL,因此浏览器不想将Origin泄漏到新的服务器上。
CSRF Token
1.将CSRF Token输出到页面中
2.页面提交的请求携带这个Token
3.服务器验证Token是否正确
双重Cookie验证
在会话中存储CSRF Token比较繁琐,而且不能在通用的拦截上统一处理所有的接口。
那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。
双重Cookie采用以下流程:
- 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如
csrfcookie=v8g9e4ksfhw
)。 - 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例
POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw
)。 - 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储Token。
用双重Cookie防御CSRF的优点:
- 无需使用Session,适用面更广,易于实施。
- Token储存于客户端中,不会给服务器带来压力。
- 相对于Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。
缺点:
- Cookie中增加了额外的字段。
- 如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效。
- 难以做到子域名的隔离。
- 为了确保Cookie传输安全,采用这种防御方式的最好确保用整站HTTPS的方式,如果还没切HTTPS的使用这种方式也会有风险。
Session、Cookie、LocalStorage、SessionStorage 的区别
- Session => 会话,用户信息 => 存储在服务器的文件中,如 MySQL 或者 Redis
- Cookie => 保存了用户凭证 => 存储在浏览器文件中,在请求的时候会发送到服务端,大小 4k 左右
- LocalStorage vs SessionStorage => 存储
- LocalStorage 如果不手动清除,会一直存在。SessionStorage 会话关闭就清除