前端面试之吊打面试官 浏览器篇

168 阅读22分钟

一、XSS(Cross-Site Scriping)(跨站脚本攻击)

1. XSS 的原理

XSS 是指攻击者通过在目标网站注入恶意的脚本代码,诱使用户浏览并执行这些恶意脚本。当用户访问被攻击的页面时,恶意脚本会在用户的浏览器中执行,进而窃取用户信息、伪造用户身份、或执行用户的敏感操作。XSS 通常利用网站未对用户输入的内容进行充分的验证和过滤,使得攻击者可以插入恶意的 JavaScript 代码。

2. XSS 的类型

XSS 主要分为三种类型:

1) 反射型 XSS(Reflected XSS)

反射型 XSS 发生在用户通过 URL 参数提交数据时,攻击者在 URL 中嵌入恶意代码,服务器直接将该代码反射回网页并在用户浏览器中执行。这种攻击通常需要用户主动点击攻击者构造的恶意链接。

示例

https://example.com/search?q=<script>alert('XSS');</script>

服务器未对 q 参数进行过滤,导致用户访问时执行 alert('XSS')

2) 存储型 XSS(Stored XSS)

存储型 XSS 发生在攻击者将恶意代码存储在服务器端的数据库或文件中,其他用户访问时,恶意脚本会直接从服务器端加载并在浏览器中执行。这种攻击通常更危险,因为它可以影响多个用户。

示例: 攻击者在评论区插入 <script>alert('XSS');</script>,所有访问评论区的用户都会执行这段恶意脚本。

3) DOM 型 XSS(DOM-based XSS)

DOM 型 XSS 发生在浏览器端,攻击者通过修改页面的 DOM 结构来执行恶意脚本。这种攻击并不依赖服务器返回的数据,而是通过客户端的 JavaScript 代码直接操作。

示例

document.write(location.href);

如果页面直接将 location.href 写入页面内容,攻击者可以通过操控 URL 注入恶意代码。

3. XSS 的危害

  • 窃取用户的 Cookie 信息,进而冒充用户身份进行操作。
  • 进行键盘记录或屏幕录制,窃取用户的敏感信息。
  • 执行恶意操作,如更改用户设置、发起交易等。
  • 在用户设备上加载恶意软件或病毒。

4. XSS 防御方法

  • 输入验证和输出编码:严格验证和过滤用户输入,并对输出的内容进行适当的编码,防止特殊字符(如 <, >, &)被执行。
  • 使用安全库:使用如 OWASP 提供的安全库(如 js-xss)来自动处理和过滤用户输入。
  • CSP(内容安全策略):通过设置 HTTP 响应头中的 Content-Security-Policy 限制浏览器加载的资源来源,防止加载不可信的脚本。
  • HttpOnly Cookie:将敏感信息(如 Session ID)存储在 HttpOnly 标记的 Cookie 中,防止 JavaScript 直接访问 Cookie。

二、CSRF(Cross-Site Requeset Forgery)(跨站请求伪造)

1. CSRF 的原理

CSRF 是指攻击者诱导已登录的用户在不知情的情况下执行恶意操作。攻击者通常通过构造恶意请求,并在用户不知情的情况下诱导用户发起请求。由于用户已经登录,浏览器会自动携带用户的身份凭证(如 Cookie),从而使得服务器认为这是用户的合法操作。

2. CSRF 的攻击流程

  1. 用户在浏览器中登录了受信任的网站,服务器为用户生成了身份凭证(如 sessionId 存储在 Cookie 中)。
  2. 用户在登录状态下访问另一个恶意网站或点击钓鱼链接。
  3. 恶意网站诱导用户浏览器向受信任的网站发送请求,由于用户登录状态下,浏览器会附带用户的 Cookie,因此服务器认为请求是合法的。
  4. 攻击者通过这个伪造的请求执行敏感操作,如转账、修改密码等。

3. CSRF 的危害

  • 伪造请求:在用户不知情的情况下,向受信任的网站发送伪造请求。
  • 账户盗用:修改用户的账户信息、密码或发起交易。
  • 数据泄露:未经授权的情况下获取用户的敏感数据。

4. CSRF 攻击示例

恶意网站上的攻击代码

<img src="https://bank.com/transfer?amount=1000&toAccount=attacker" />

当用户访问恶意网站时,浏览器会自动向银行网站发送该请求,而银行会认为该请求是由已登录的用户发出的,进而执行转账操作。

5. CSRF 防御方法

  • CSRF Token(防伪造令牌):在敏感操作的请求中添加一个唯一的、难以猜测的 Token,服务器在验证时要求该 Token 必须与用户的会话关联,确保请求是合法发起的。
    • 如:在表单提交时嵌入一个隐藏字段,包含 Token。
  • SameSite Cookie:通过设置 SameSite 属性,防止浏览器在跨站请求时发送 Cookie。
    • SameSite=Lax:允许从外部站点的链接点击发起请求,但禁止其他跨站请求。
    • SameSite=Strict:完全禁止跨站请求发送 Cookie。
  • 双重提交 Cookie:将 CSRF Token 存储在 Cookie 中,并且在请求中将它同时通过 Cookie 和请求参数传递,服务器验证两者一致性。
  • 验证码(CAPTCHA):在敏感操作中加入验证码,以确保用户是主动发起的请求,而不是自动化脚本或跨站请求伪造。

三、XSS 与 CSRF 的区别

特性XSS(跨站脚本攻击)CSRF(跨站请求伪造)
攻击目标用户(攻击用户的浏览器执行恶意脚本)服务器(利用用户身份发起伪造请求)
主要风险窃取用户数据、控制用户操作、劫持会话执行用户未授权的操作,如转账、修改密码等
利用方式注入恶意脚本到网页,用户访问时执行利用用户的身份凭证(如 Cookie),发起伪造请求
依赖条件需要用户访问包含恶意代码的页面用户已登录,并拥有有效的身份凭证
防御措施输入验证、输出编码、CSP、HttpOnly CookieCSRF Token、SameSite Cookie、验证码

总结

  • XSS 攻击的目标是用户,通过注入恶意脚本窃取数据或控制用户的浏览器。防御的重点是对用户输入进行严格的验证和编码,防止未处理的恶意代码被执行。
  • CSRF 攻击的目标是服务器,通过伪造用户的请求来执行敏感操作。防御的重点是确保请求来自合法的用户,通过 CSRF Token 和 SameSite Cookie 属性来验证请求的来源。

什么是进程和线程?有什么区别

进程(Process)

进程是计算机中正在运行的程序的实例,一个进程就是一个程序运行实例。它拥有独立的内存空间、代码和数据,并且由操作系统负责调度和管理。每个进程在执行时都会分配独立的内存空间,不同进程之间的内存是隔离的,一个进程的错误不会直接影响其他进程。 进程之间通过进程间通信(IPC)机制来交换数据和进行通信,常见的IPC方式包括管道、消息队列、共享内存等。进程的切换开销较大,因为需要保存和恢复进程的完整状态,涉及到内存保护和虚拟内存的切换。

线程(Thread)

线程是进程的子任务,一个进程可以包含多个线程。它们共享相同的代码和数据,但拥有独立的执行栈和寄存器集合。多个线程可以在同一进程内并发执行,共享进程的资源,如内存空间、打开的文件等。线程间的通信和数据交换比进程间的通信更加方便,因为它们共享相同的地址空间。线程的切换开销较小,因为线程共享进程的地址空间,切换时不需要切换内存页表,速度较快。

区别

  • 进程和线程都可以实现并发执行,但进程是独立的执行实体,而线程是依赖于进程的
  • 进程之间资源相互隔离,线程共享所属进程的资源
  • 创建和销毁线程的开销较小,而创建和销毁进程的开销较大。
  • 多线程程序的编程复杂度通常比单线程程序高,但多线程可以更好地利用多核处理器来提高程序的执行效率。

浏览器有哪些进程

  • 主进程:负责处理用户输入、渲染页面等主要任务。
  • 渲染进程:渲染进程负责解析HTML、CSS和JavaScript,并将网页渲染成可视化内容。
  • GPU进程:负责处理浏览器中的GPU加速任务。
  • 网络线程:网络进程负责处理浏览器中的网络请求和响应,包括下载网页和资源等。
  • 插件进程:负责浏览器插件运行。 在这里插入图片描述

协商缓存和强缓存的区别

强缓存

使用强缓存策略时,如果缓存资源有效,浏览器会从本地读取缓存资源并返回200,不必再向服务器发起请求。强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。

Expires指定资源的过期时间。在过期时间以内,改资源可以被缓存使用,不需要向浏览器发送请求。这个时间依赖于服务器时间,会存在服务器时间和客户端时间不一致。

Cache-Control属性:

  • private: 仅浏览器可以缓存
  • public:浏览器和代理服务器都可以缓存
  • max-age=xxx 过期时间,单位为秒
  • no-cache 不进行强缓存,但会有协商缓存
  • no-store 不强缓存,也不协商缓存

当两种方式一起使用时,Cache-Control 的优先级要高于 Expires

协商缓存

如果设置强缓存,无需发起请求,直接使用缓存内容。如果没有命中强缓存,设置了协商缓存,也不需要发起请求,使用缓存。

命中协商缓存条件:

  • Cache-Control: no-cache
  • max-age时间过期

在使用协商缓存时,会先向服务器发送一个请求,如果资源没有发生修改,则请求返回304状态,让浏览器使用本地缓存。如果资源发生修改,则返回修改后的内容

在request headers中的Etag属性和Last-Modified属性,来进行设置。其中,ETage优先于Last-Modified

Etag文件改动 服务器在返回资源的时候,在头信息中添加Etag属性,这个属性是资源的唯一标识符。当资源改变,这个值也会改变。下次请求资源时,会在请求头中添加If-None-Match属性,为上一次请求的资源的Etag值。服务端会通过这个属性和资源最后一次修改时间进行对比,以此来判断资源是否修改。这种方式比Last-Modified更加准确。

Last-Modified 上次修改时间 服务器通过在响应头上添加Last-Modified属性,来指出资源最后一次修改时间。当浏览器发起请求时,会在请求头上添加一个IF-Modified-Since属性,值为上一次资源请求的Last-Modified的值。服务区会通过这个属性和最后修改时间来进行比较,以此来判断资源是否修改。如果没有资源修改,返回304状态,使用本地缓存。如果资源修改,就返回最新资源,200状态。

这种方式有个缺点,Last-Modified标记的时间只能精确到1秒,如果文件在1秒内修改,但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。

区别

  • 强缓存优先级高于协商缓存
  • 协商缓存不论命中与否都会发送一次请求
  • 强缓存返回200,协商缓存命中返回304
  • Ctrl+F5 会强制刷新会跳过所有缓存,而F5刷新跳过强缓存,但是会检查协商缓存。

为什么需要浏览器缓存?

使用浏览器缓存,有以下优点:

  • 减少了服务器的负担,提高了网站的性能
  • 加快了客户端网页的加载速度
  • 减少了多余网络数据传输

常见浏览器所用内核

  • IE浏览器内核,Trident 内核,也是俗称的IE内核;
  • Chrome 浏览器内核,以前是 Webkit 内核,现在是 Blink内核;
  • Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
  • Safari 浏览器内核:Webkit 内核;
  • 360浏览器、猎豹浏览器内核:IE + Chrome 双内核;

浏览器的渲染过程

  • 解析文档,生成DOM树
  • 解析CSS,根据CSS规则生成CSSOM规则树
  • 在CSSOM树和DOM树生成完后,合并DOM、CSSOM树构建渲染树
  • 渲染树构建完成后,开始计算元素大小和位置【回流发生在这个阶段】
  • 根据计算好的位置信息将内容渲染到屏幕上【重绘发生在这个阶段】

浏览器渲染优化

  • 优化javaScript,JavaScript会阻塞HTML的解析,改变JavaScrip加载方式。
    • 将JavaScript放到body最后面
    • 尽量使用异步加载JS资源,这样不会阻塞DOM解析,如defer、async
  • 优化CSS加载,
    • CSS样式少,使用内嵌样式
    • 导入外部样式使用link,而不是@import,因为它会阻塞渲染。
  • 减少回流重绘
    • 避免频繁操作样式
    • 避免频繁操作DOM
    • 复杂动画使用定位脱离文当流
    • 使用transform替代动画

Cookie、LocalStorage、SessionStorage区别

Cookie

  • 大小只有4kb
  • 跨域不能共享
  • 不安全,容易被劫持
  • 只存在请求头中(类似uuid)

SessionStorage

  • 存储在内存中,体积相对较大
  • 页面关闭,数据会消失
  • 相对Cookie安全

LocalStorage

  • 体积大,可以存储更多内容。
  • 生命周期长,除非手动删除,不然会一直存在
  • 存储在硬盘中,不会像Cookie一样被请求携带

什么是同源策略

跨域问题其实就是浏览器的同源策略造成的。 同源指的是:协议端口号域名必须一致。

如何解决跨越问题

** JSONP**:利用的是script标签不会受到同源策略影响的特性,实现的原理是在服务端生成一个函数,客户端使用script标签请求该函数,服务器会返回该函数,并且把需要返回的数据作为参数传入

jsonp实现方式

JSONP(JSON with Padding)是一种跨域数据获取的方法,它通过动态创建 <script> 标签来请求数据,从而绕过浏览器的同源策略限制。以下是如何使用 JSONP 实现跨域请求的基本步骤:

1. 服务器端

服务器端需要能够处理 JSONP 请求。通常,这意味着服务器端会将 JSON 数据包裹在一个回调函数中。假设服务器端使用 Express.js,这里是一个简单的示例:

const express = require('express');
const app = express();


app.get('/data', (req, res) => {
const callback = req.query.callback; // 从请求中获取回调函数名
const data = { key: 'value' }; // 要返回的数据
res.type('application/javascript'); // 设置响应类型为 JavaScript
res.send(${callback}(${JSON.stringify(data)})); // 返回 JSONP 数据
});




app.listen(3000, () => {
console.log('Server running on port 3000');
});

app.listen(3000, () => { console.log('Server running on port 3000'); });

2. 客户端

在客户端,使用 JavaScript 动态创建一个 <script> 标签来发起 JSONP 请求。以下是一个简单的示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSONP Example</title>




</head>




<body>
<script>
function handleResponse(data) {
console.log(data); // 处理服务器返回的数据
}



    // 创建一个随机的回调函数名,以避免与其他回调函数冲突
    const callbackName = 'handleResponse';

    // 创建 JSONP 请求
    const script = document.createElement('script');
    script.src = `http://localhost:3000/data?callback=${callbackName}`;
    document.body.appendChild(script);
&lt;/script&gt;




</body>




</html> 

</html>

3. 解释

  • 服务器端:构造 JSONP 响应时,将数据包裹在一个回调函数中,并通过 callback 查询参数来确定回调函数的名称。
  • 客户端:动态创建 <script> 标签,设置其 src 属性为服务器端的 URL,并包含 callback 查询参数。通过这种方式,服务器返回的数据会被自动执行,从而调用客户端提供的回调函数。

注意事项

  1. 安全性:由于 JSONP 执行的是一个脚本,因此它存在安全风险(如跨站脚本攻击)。只应使用受信任的源,并确保服务器端代码正确验证和处理请求。
  2. 跨域限制:JSONP 可以跨域,但只有 GET 请求。如果需要其他 HTTP 方法(如 POST),则需要其他方法,如 CORS(跨源资源共享)。

JSONP 是一个较早的技术,现今大多数跨域请求可以通过 CORS 解决,因此在现代应用中,JSONP 的使用已较少。

** nginx代理跨域:**

** **反向代理是将客户端的请求转发给真正的服务端,从而来解决跨域问题。反向代理服务器和真正的服务器都在同一个域名下,当客户端想发起请求时只需要向代理服务器发起请求,由代理服务器将请求转发给真正的服务器,最后将响应返回给客户端。

CORS

  cors主要是通过添加一些http头来让浏览器允许浏览器跨域访问资源通过来实现的,服务端需要在响应头中添加Access-Control-Allow-Origin等字段指示允许哪些域名进行跨域请求。

wbsocket:

  • websocket是一种建立在tcp上的全双工协议,他可以允许协议进行跨域通信,因为websocket不会受到同源策略的影响。

简单跨域和复杂跨域

主要区别:请求的类型和所涉及到的请求头

简单跨域请求是指符合某些条件的 HTTP 请求,这些请求不需要进行额外的预检(preflight)请求。简单跨域请求的特征是请求满足以下条件:

简单跨域

请求类型(HTTP Methods):

  • GET
  • POST(只要请求体是简单的,比如 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • HEAD

请求头(Request Headers):

  • 只能使用一些常见的、被允许的 HTTP 请求头。即:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(且只限以下三种值:application/x-www-form-urlencodedmultipart/form-datatext/plain

复杂跨域请求

复杂跨域请求是指请求不符合简单跨域请求的条件,通常是涉及到一些自定义的请求头、特定的 HTTP 方法,或请求体不符合简单条件的情况。复杂跨域请求需要先发送一个 预检请求(preflight request)来询问服务器是否允许跨域请求,然后服务器返回适当的跨域响应,浏览器才会发出实际的请求。

复杂跨域请求的特征:

  • HTTP 方法:使用了不属于简单请求的 HTTP 方法,例如:
    • PUT
    • DELETE
    • PATCH
    • 或者其他自定义方法
  • 请求头:如果请求头中使用了除了简单请求头之外的其他自定义头,浏览器也会认为这是一个复杂跨域请求。例如:
    • Authorization
    • X-Custom-Header
    • X-Requested-With
  • 请求体:请求体包含了一些复杂的内容,例如:
    • application/json(这是常见的自定义请求体类型)

预检请求

当浏览器检测到发起的请求是一个复杂跨域请求时,会先发送一个 预检请求。预检请求是一个 OPTIONS 请求,目的是让服务器告诉浏览器是否允许跨域访问。预检请求的响应中会包含允许跨域的 HTTP 头信息。

事件流

事件流分为三个阶段:捕获阶段目标阶段冒泡阶段

过程如下:

  1. 捕获阶段:事件从最外层的节点,也就是文档对象开始,逐级向下传播,直到事件的目标节点上。
  2. 目标阶段:事件到达目标节点,触发目标节点上的事件处理函数。
  3. 冒泡阶段:事件从目标节点开始,逐级向上传播,直到到达最外层节点(文档对象)

事件冒泡和捕获的区别?

事件冒泡和事件捕获是两种不同的事件传播方式,默认是冒泡,它们的区别在于传播方向不同:

  • 事件冒泡是从自下而上,从子元素冒泡到父元素,执行父元素上的事件处理。
  • 事件捕获是事件从文档的根元素开始,逐级向下传播到较为具体的元素(即从父元素到子元素)。

如何阻止事件冒泡

  • 普通浏览器:event.stopPropagation()
  • IE浏览器:event.cancelBubble = true;

对事件委托的理解

利用浏览器事件冒泡机制。事件在冒泡的过程中会传到父节点,并且父节点可以通过事件对象获取到目标节点,可以吧子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件

回流与重绘

重绘当一个元素的外观发生变化,但是没有改变布局,重新渲染元素的外观。比如background-color、color

回流当DOM变化影响了元素,比如元素的尺寸、布局、显示隐藏等改变了,需要重写构建。每个页面至少需要一次回流,就是在页面第一次加载的时候,这个时候一定会发生回流。

如何避免回流重绘:

  • 避免使用table布局
  • 尽可能在DOM树的最末端改变class
  • 不要频繁的操作元素的样式
  • 避免设置多层内联样式
  • 开启GPU加速
  • 使用absolute或者fixed,脱离标准文档流

回流必将引起重绘,而重绘不一定会引起回流

对浏览器事件循环的理解

事件循环是一种机制,它会不断的轮询任务队列,并将队列中的任务依此执行。

JavaScript的任务分为两种同步异步

  • 同步任务:在主线程上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务,
  • 异步任务:不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。

因为js是单线程,在执行代码的时候将所有函数压入执行栈中。同步任务会按照后进先出的原则以此执行。遇到异步任务时,将其放入任务队列中。当前执行栈里事件执行完毕后,就会从任务队列中取出对应异步任务的回调函数放入执行栈中继续执行。

宏观任务(MacroTask|Task)、微观任务(MicorTask)。

  • 宏任务:script全部代码、setTimeout、setInterval、I/O、UI渲染
  • 微任务:Promise.then、Process.nexTick(Node独有)、MutationObserver

任务队列中的任务分为宏任务微任务,当执行栈清空后,会先检查任务队列中是否有微任务,如果有就按照先进先出的原则,压入执行栈中执行。微任务中产生了新的微任务不会推迟到下一个循环中,而是在当前循环中继续执行。 当执行这一轮的微任务完毕后,开启下一轮循环,执行任务队列中的宏任务。

一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。

执行顺序

因为js是单线程执行的,而单线程在任务比较多的情况下就会产生拥堵,因此就引入了多线程机制,这里的多线程实际上是同步任务和异步任务这两种,而在异步任务里面又分为了宏任务和微任务,宏任务一般是由宿主也就是浏览器发起的包括settimeout、setinterval等,而微任务一般是由由js代码发起的一般是promise和process.nextTick,通常执行的顺序是执行同步代码,遇到异步宏任务代码会放到异步宏任务代码的队列里面,遇到异步微任务代码会放到异步微任务代码队列里面,当所有主线程的代码执行完成之后会从异步微任务代码里面读取出来放到主线程里面进行执行,当所有异步微任务代码执行完成之后就会从异步宏任务代码那边读取出来放到主线程里面执行之后就如此循环。

在这里插入图片描述

Node.js的事件循环

Node事件循环分为6个阶段,每进入一个阶段,都会去对应的回调队列中取出函数执行。

  1. Timers阶段:执行timer(setTimeout、setInterval)的回调,由poll阶段控制;
  2. I/O callbacks阶段:系统调用相关的回调
  3. idle prepare阶段:Nodejs内部执行,可以忽略
  4. poll阶段:轮询 在该阶段如果没有timer的话,会出现一下情况
    • poll队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制;
    • poll队列对空,会出现以下两种情况
      • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调;
      • 如果没有 setImmediate 回调需要执行,就会等待回调被天加到队列中,然后立即执行。 如果设置里有timer,并且 poll 队列为空,就会判断是否有 timer 超时,如果有就回到 timers 阶段执行回调。
  5. check阶段:执行 setImmediate 回调
  6. colse callbacks阶段:执行一些关闭回调,比如socket.on('close', ...)等。 在这里插入图片描述

process.nextTick

它会在轮询的各个阶段结束时,进入到下一个阶段之前立即执行。

setImmediate 和 setTimeout

js
setTimeout(() => {
    console.log('setTimeout')
}, 0)
setImmediate(() => {
    console.log('setImmediate')
})

这种情况下,定时器的执行顺序是随机的

如果把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})

// immediate
// timeout

Node和浏览器事件循环机制的区别

  • 浏览器事件循环会在宏任务结束后,检查微任务。而Node的微任务是在两个阶段之间执行。
  • 浏览器的process.nextTick和其他微任务优先级一样,而node中要高于其他优先级。