高频前端面试题汇总之浏览器原理篇
本人近期在准备找份前端实习,便整理了一下高频的前端面试题,分享给大家一起来学习。这里的面试题答案都比较简洁扼要,如果需要详细了解的,可以自行查阅相关的资料。如有问题,欢迎指正!
一、浏览器安全
1. 什么是 XSS 攻击?
XSS 攻击指的是跨站脚本攻击,攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息。其本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
攻击者可以通过这种攻击方式进行以下操作:
- 获取页面的数据,如DOM、cookie
- 占用服务器资源,从而使用户无法访问服务器
- 破坏页面结构
- 流量劫持(将链接指向某网站)
XSS 可以分为存储型、反射型和 DOM 型:
- 存储型指的是恶意脚本会存储在服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
- 反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据处理后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后当做脚本执行。
- DOM 型指的通过修改页面的 DOM 节点形成的 XSS。
2. 如何防御 XSS 攻击?
- cookie 使用 http-only,使得脚本无法获取
- 对 v-html 和 innerHTML 加载的信息进行转义
- 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式。
3. 什么是 CSRF 攻击?
CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
常见的 CSRF 攻击有三种:
- GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
- POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
- 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。
4. 如何防御 CSRF 攻击?
- 进行同源检测:服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。
- 使用 CSRF Token 进行验证:服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。但是我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。
- 对 Cookie 进行双重验证:服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。
- 在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为被第三方使用用。
5. 什么是中间人攻击?
是指攻击者与通讯的两端分别创建独立的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对方直接对话, 但事实上整个会话都被攻击者完全控制,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。
攻击过程如下:
- 客户端发送请求到服务端,请求被中间⼈截获
- 服务器向客户端发送公钥
- 中间⼈截获公钥,保留在自己手上。然后自己⽣成⼀个伪造的公钥,发给客户端
- 客户端收到伪造的公钥后,⽣成加密hash值发给服务器
- 中间⼈获得加密hash值,⽤自己的私钥解密获得真秘钥,同时⽣成假的加密hash值,发给服务器
- 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端
6. 如何防范中间人攻击?
- 采用传输加密:SSL 和 TLS 可以阻止攻击者使用和分析网络流量
- 安装 DAM 数据库活动监控:DAM 可以监控数据库活动,检测篡改数据
7. 有哪些可能引起前端安全的问题?
- 跨站脚本攻击 (XSS)
- 跨站请求伪造攻击(CSRF)
- 恶意第三⽅库
- iframe的滥⽤
8. 网络劫持有哪几种,如何防范?
-
DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)
-
HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)
DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
二、浏览器缓存
1. 对浏览器的缓存机制的理解
- 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时对比使用;
- 下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持HTTP1.1,则使用 expires 头判断是否过期;
- 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 If-None-Match 和 If-Modified-Since 的请求;
- 服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做修改,Etag 值一致则没有修改,命中协商缓存,返回 304;如果不一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200;
- 如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;不一致则返回新的 last-modified 和文件并返回 200;
2. 协商缓存和强缓存的区别
强缓存: 使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。
服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。
Expires 是 http1.0 中的方式,因为它的一些缺点,在 HTTP 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires。Cache-Control 可设置的字段:
public: 表示可以被任何对象缓存
private: 只能被用户浏览器缓存,不允许任何代理服务器缓存
no-cache:需要先和服务端确认返回的资源是否发生了变化,如果未发生变化,则直接使用缓存好的资源
no-store:禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源
max-age=:设置缓存的最大有效期,单位为秒
s-maxage=:仅适用于共享缓存(CDN),优先级高于max-age或者Expires头
协商缓存: 如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容;如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。命中协商缓存的条件有两个: max-age=xxx 过期了、值为 no-store
使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。协商缓存也可以通过两种方式来设置,分别是 http 头信息中的 Etag 和 Last-Modified属性。
(1)服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。
(2)因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确。
当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
3. 为什么需要浏览器缓存?
浏览器缓存指的是浏览器将用户请求过的静态资源,存储到本地,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。使用浏览器缓存,有以下优点:
- 减少了服务器的负担,提高了网站的性能
- 加快了客户端网页的加载速度
- 减少了多余网络数据传输
4. 点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?
- 点击刷新按钮或者按 F5: 浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
- 用户按 Ctrl+F5(强制刷新): 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
- 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
三、浏览器渲染原理
1. 浏览器的渲染过程
- 首先解析收到的文档,根据文档定义构建一棵 DOM 树
- 然后对 CSS 进行解析,生成 CSSOM 规则树
- 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。
- 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局。
- 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。
2. 浏览器渲染优化
(1)针对JavaScript
1.尽量将JavaScript文件放在body的最后
2.body中间尽量不要写<script>标签
3.使用async属性和defer属性来异步引入<script>标签
(2)针对CSS
1.导入外部样式使用link,而不用@import
2.如果css少,尽可能采用内嵌样式,直接写在style标签中
(3)针对DOM树、CSSOM树
1.HTML文件的代码层级尽量不要太深
2.使用语义化的标签,来避免不标准语义化的特殊处理
3.减少CSSD代码的层级,因为选择器是从右向左进行解析的
(4)减少回流与重绘
1.不要使用table布局, 一个小的改动可能会使整个table进行重新布局
2.不要频繁操作元素的样式
3.将DOM的多个读操作(或者写操作)放在一起
4.操作DOM时,尽量在低层级的DOM节点进行操作
5.避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
6.使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
3. 渲染过程中遇到 JS 文件如何处理?
JavaScript 的加载、解析与执行会阻塞文档的解析,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。
4. 什么是文档的预解析?
当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
5. CSS 如何阻塞文档解析?
理论上,既然样式表不改变 DOM 树,也就没有必要停下文档的解析等待它们。然而,JavaScript 脚本执行时可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题。所以如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟 JavaScript 脚本执行和文档的解析,直至其完成 CSSOM 的下载和构建。
6. 如何优化关键渲染路径?
为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:
(1)关键资源的数量。
(2)关键路径长度。
(3)关键字节的数量。
关键资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。同样,关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。最后,浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
优化关键渲染路径的常规步骤如下:
(1)对关键路径进行分析和特性描述:资源数、字节数、长度
(2)最大限度减少关键资源的数量:删除它们,延迟它们的下载
(3)优化关键字节数以缩短下载时间
四、浏览器本地存储
1. 浏览器本地存储方式
(1)Cookie: 最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的。Cookie的大小只有4kb,它是一种纯文本文件,每次发起HTTP请求都会携带Cookie。
特性:
1.Cookie一旦创建成功,名称就无法修改
2.Cookie是无法跨域名的,也就是说a域名和b域名下的cookie是无法共享的
3.每个域名下Cookie的数量不能超过20个,每个Cookie的大小不能超过4kb
4.有安全问题,如果Cookie被拦截了,那就可获得session的所有信息
5.Cookie在请求一个新的页面的时候都会被发送过去
使用场景:
1.我们将sessionId存储到Cookie中,每次发请求都会携带这个sessionId
2.可以用来统计页面的点击次数
(2)LocalStorage: HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了。
优点:
1.LocalStorage的大小一般为5MB,可以储存更多的信息
2.LocalStorage是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在
3.仅储存在本地,不像Cookie那样每次HTTP请求都会被携带
缺点:
1.存在浏览器兼容问题,IE8以下版本的浏览器不支持
2.如果浏览器设置为隐私模式,那我们将无法读取到LocalStorage
3.LocalStorage受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问
常用API:
// 保存数据到 localStorage
localStorage.setItem('key', 'value');
// 从 localStorage 获取数据
let data = localStorage.getItem('key');
// 从 localStorage 删除保存的数据
localStorage.removeItem('key');
// 从 localStorage 删除所有保存的数据
localStorage.clear();
// 获取某个索引的Key
localStorage.key(index)
(3)SessionStorage: 是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。
使用场景:
由于SessionStorage具有时效性,所以可以用来存储一些网站的游客登录的信息,
还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了。
常用API:
// 保存数据到 sessionStorage
sessionStorage.setItem('key', 'value');
// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('key');
// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem('key');
// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();
// 获取某个索引的Key
sessionStorage.key(index)
2. Cookie有哪些字段,作用分别是什么
- Value:值
- Size: 大小
- Name:名称
- Path:可以访问此 cookie 的页面路径。 比如 domain 是 abc.com,path 是
/test
,那么只有/test
路径下的页面可以读取此cookie。 - Secure: 指定是否使用HTTPS安全协议发送Cookie。
- Domain:可以访问该 cookie 的域名
- HTTP: 该字段包含 HTTPOnly 属性 ,该属性用来设置cookie能否通过脚本来访问。
- Expires/Max-size:超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。
3. Cookie、LocalStorage、SessionStorage区别
- cookie: 其实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存时间由 expires 属性指定,并且 cookie 只能被同源的页面访问共享。
- sessionStorage: html5 提供的一种浏览器本地存储的方法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保存的数据。它一般能够存储 5M 的数据,它在当前窗口关闭后就失效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享。
- localStorage: html5 提供的一种浏览器本地存储的方法,它一般也能够存储 5M 的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会失效,并且 localStorage 也只能被同源页面所访问共享。
上面几种方式都是存储少量数据的时候的存储方式,当需要在本地存储大量数据的时候,我们可以使用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。
4. 前端储存的方式有哪些?
cookies
localStorage
sessionStorage
IndexedDB
5. IndexedDB有哪些特点?
- 键值对储存:IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
- 异步:IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
- 支持事务:IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 同源限制: IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
- 储存空间大:IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。
- 支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据
6. localstorage会遇到什么安全问题
1.采用明文存储,所以在存储的时候最好在服务器端进行加密
2.可能会被植入广告追踪标志
3.用户定期清除浏览器缓存有助于 cookie 更有效地发挥作用,但是会丢失localstorage中存储的数据
7. token为什么要设置过期时间,时限多长合适?
安全性问题,如果不设置过期时间,那么token 容易被盗,就会产生不可估量的影响。比如支付宝因为涉及强资金安全性,如果时长设置过长可能导致用户本人离开后由他人操作故意导致恶心资金流转的问题等。
那么token 时限多长合适呢?这就需要根据业务性来区分,涉及支付类,对数据账号安全特别敏感的,建议每次都重新登录;对于日常使用的,比如学习类APP,工具类APP,不涉及到金额,token 时限可以长点。
8. cookie和localStorage区别
(1)存储量的区别
cookie 单个的最大存储为 4k,如果大于 4k,则存储失败,浏览器中找不到对应的 cookie 信息
localStorage 的最大存储为 5m。如果大于这个最大限制浏览器提示出错
(2)存储量的区别
cookie默认是会话级存储,可以设置过期时间
localStorage 是持久化存储,除非主动 clear 掉
(3)可操作
cookie 不仅仅是存储数据,还有其他多个属性可供其操作设置:Domain 与 Path 一起决定了 cookie 的作用范围,Expires/Max-Age 决定了过期时间,HttpOnly 如果设为 true,那么通过js(document.cookie)无法读取 cookie 数据,Secure 如果设为 true,那么 cookie 只能用 https 协议发送给服务器。
localStorage 只是存储数据
(4)使用场景
cookie 的使用场景一般是作为客户端与服务端的一种信息传递,当添加了 cookie,默认的同源的 cookie 信息会自动作为请求头的一部分被发送到服务端
localStorage 一般仅用作客户端的数据存储,如存储一个异步请求的结果数据,然后在页面重渲染时,可以直接读取 storage 中的数据,减少一次请求的发送
五、浏览器同源策略
1. 什么是同源策略
跨域问题其实就是浏览器的同源策略造成的。同源策略是一种安全机制,同源指的是:协议、端口号、域名必须一致。
URL | 是否跨域 | 原因 |
---|---|---|
store.company.com/dir/page.ht… | 同源 | 完全相同 |
store.company.com/dir/inner/a… | 同源 | 只有路径不同 |
store.company.com/secure.html | 跨域 | 协议不同 |
store.company.com:81/dir/etc.htm… | 跨域 | 端口不同 ( http:// 默认端口是80) |
news.company.com/dir/other.h… | 跨域 | 主机不同 |
同源政策主要限制了三个方面:
- 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。
- 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。
- 当前域下 ajax 无法发送跨域请求。
同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
2. 如何解决跨越问题
(1)CORS
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器,让运行在一个 origin (domain)上的Web应用被允许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域HTTP 请求。
CORS 是在后端服务器做相应配置的,这里就以express
为例:
// 配置关键代码Access-Control-Allow-Origin与Access-Control-Allow-Methods
app.use((req,res,next)=>{
//实验验证,只需要设置这一个就可以进行get请求
res.header('Access-Control-Allow-Origin', 'http://localhost:8080')//配置8080端口跨域
//res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS') // 允许的 http 请求的方法
// res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With')
next()
})
Access-Control-Allow-Origin
:设定请求源,就告诉了浏览器。如果请求我的资源的页面是我这个响应头里记录了的"源",则不要拦截此响应,允许数据通行。如上配置,由于服务端设置了res.header('Access-Control-Allow-Origin', 'http://localhost:8080')
,如果请求数据的源是http://localhost:8080
则可以允许访问返回的数据。这样浏览器就不会抛出错误提示,而是正确的将数据交给你。Access-Control-Allow-Methods
:允许请求的方法。Access-Control-Allow-Headers
:用于preflight request
(预检请求)中,列出了将会在正式请求的Access-Control-Expose-Headers
字段中出现的首部信息。
(2)JSONP
jsonp 的原理就是利用<script>
标签没有跨域限制,通过<script>
标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。
1)原生JS实现:
<script>
let script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
服务端返回如下(返回时即执行全局函数):
handleCallback({"success": true, "user": "admin"})
2)Vue axios实现:
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
后端node.js代码:
let querystring = require('querystring');
let http = require('http');
let server = http.createServer();
server.on('request', function(req, res) {
var params = querystring.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
JSONP的缺点:
- 具有局限性, 仅支持get方法
- 不安全,可能会遭受XSS攻击
(3)postMessage 跨域
HTML5 XMLHttpRequest Level 2中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数:
- data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
- origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
1)a.html:(domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
let iframe = document.getElementById('iframe');
iframe.onload = function() {
let data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
2)b.html:(domain2.com/b.html)
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
let data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
(4)nginx代理跨域
nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段。
1)nginx配置解决iconfont跨域,浏览器跨域访问 js、css、img等 常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在 nginx 的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
2)nginx反向代理接口跨域:通过 Nginx 配置一个代理服务器域名与 domain1 相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。
#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
(5)nodejs 中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发。
1)非Vue框架的跨域
- 前端代码:
let xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
- 中间件服务器代码:
let express = require('express');
let proxy = require('http-proxy-middleware');
let app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
2)Vue 框架的跨域
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
(6)document.domain + iframe跨域
此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1)父窗口:(domain.com/a.html)
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
let user = 'admin';
</script>
2)子窗口:(child.domain.com/a.html)
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
(7)location.hash + iframe跨域
实现原理:a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
1)a.html:(domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
let iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2)b.html:(.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
let iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3)c.html:(www.domain1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
(8)window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
1)a.html:(domain1.com/a.html)
let proxy = function(url, callback) {
let state = 0;
let iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
2)proxy.html:(domain1.com/proxy.html)
中间代理页,与a.html同域,内容为空即可。
3)b.html:(domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
通过 iframe 的 src 属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
(9)WebSocket协议跨域
WebSocket protocol是HTML5一种新的协议,它实现了浏览器与服务器全双工通信,同时允许跨域通讯。 原生 WebSocket API使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口。
1)前端代码:
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
let socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
2)Nodejs socket后台:
let http = require('http');
let socket = require('socket.io');
// 启http服务
let server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
3. 正向代理和反向代理的区别
- 正向代理:
客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。
- 反向代理:
服务器为了能够将工作负载分布到多个服务器来提高网站性能 (负载均衡),当其收到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。 一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
两者区别如图示:
正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。
4. Nginx的概念及其工作原理
Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。
传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。
Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的HTTP请求,而每个 Apache process 只能处理一个。
5. Vue.config.js配置proxy为什么能解决跨域
首先浏览器是禁止跨域的,但是服务端不禁止,在本地运行 npm run dev 等命令时实际上是用 node 运行了一个服务器,因此 Vue 的转发机制 proxyTable 实际上是将请求发给自己的服务器,再由服务器转发给后台服务器,做了一层代理。Vue的proxyTable 用的是 http-proxy-middleware 中间件, 因此不会出现跨域问题。
6. 浏览器对哪些跨域是允许的
<script>
、<img>
、<iframe>
、<link>
、<a>
等标签都可以加载跨域资源。
转载自:https://juejin.cn/post/7208466455879221285