大厂技术 高级前端 Node进阶点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
hello大家好,我是考拉🐨。
不知道大家感受到了没有,这几年系统安全越来越受到重视,不仅仅是公司层面,而是国家层面的,比如最近就有国家层面的安全攻防演练,专门的安全公司来“找茬”。
安全不仅仅是后端关注的,前端在这方面也有很多的点需要注意,不仅是日常工作中,在面试中对于安全也是重点的考察方向。今天带来一篇前端安全相关的文章,比较详细的介绍了前端面临的一些安全问题以及应对办法,希望大家看完有收获。
下面是正文部分。
前言
在前端开发领域,前端安全经常被公司忽视,这些公司大部分都是以业务为中心的公司,认为前端只是页面仔,画个页面调个接口,能有多大安全隐患?下面我将举几个前端安全相关的例子!
下面这些内容都还是基于前端开发层面的思考和总结,在处理安全漏洞方面,前端是存在一定的局限性,所以前端的防护措施只是第一道防线,后端的安全验证和防护同样很重要。
先给自己挖个坑,后面我会把文中提到的每一项漏洞攻击都单独写一篇详细的文章,争取可以把这些内容写的更细更全,感兴趣的同学可以关注一波。
跨站脚本攻击(XSS)
什么是XSS
它指的是攻击者通过在网页中注入恶意的脚本代码(一般是 JavaScript 代码),当其他用户访问该网站时,浏览器会执行这些恶意脚本。恶意脚本可以窃取用户的会话 Cookie 信息,从而劫持用户会话,获取用户的登录凭证,或者篡改网页内容,将用户重定向到恶意网站,还能够收集用户的敏感信息。
例如,一个论坛网站允许用户发表评论,如果攻击者在评论中插入一段恶意的 JavaScript 代码,当其他用户查看该评论时,浏览器就会执行这段恶意代码。因为当浏览器加载网页时,它会按照 HTML 规范来解析和执行其中的代码,攻击者插入的恶意 JavaScript 代码被包含在网页中,浏览器就会视为它是合法的脚本来执行。
XSS 攻击通常发生在以下场景
页面没有对表单输入内容进行充分的过滤。
直接将用户输入的数据在页面上输出,没有进行编码处理。
XSS 攻击解决方案
通过设置 HTTP 响应头来限制页面可以加载和执行的资源。以 Apache 服务器和Nginx 服务器为例:
在将用户输入的数据输出到页面时,进行适当的编码,以防止脚本被执行。
encodeURI() 函数:对整个 URI 进行编码,除了一些特殊字符
(如 : 、 / 、 ? 、 # 、 [ 、 ] 、 @ 、 ! 、 $ 、 & 、 ' 、 ( 、 ) 、 * 、 + 、 , 、 ; 、 = )
不会被编码。encodeURIComponent() 函数:对 URI 的组成部分进行编码,会对更多的字符进行编码,包括 / 等。
// 以使用 encodeURIComponent() 函数为示例 function outputUserData(userInput) { let encodedInput = encodeURIComponent(userInput); document.write(encodedInput); } let userData = "<script>alert('XSS')</script>"; outputUserData(userData);
在实际应用中,需要根据具体的输出场景选择合适的编码函数。通常,如果要将用户输入作为 URL 的一部分输出,使用 encodeURIComponent() 更合适。如果是要在页面中直接显示用户输入的内容,可能需要根据具体情况进一步处理编码后的结果。
对用户输入的内容进行严格的验证,只允许符合预期格式和内容的数据通过。
使用白名单机制,明确规定允许的输入字符和格式。
对输入数据进行消毒处理,去除可能的恶意脚本代码或特殊字符。
输入验证与消毒
// 一段简单的示例,用于防止 XSS 攻击的 JavaScript 输入验证和清理函数 function sanitizeInput(input) { // 去除 HTML 和 XML 标签 input = input.replace(/<[^>]*>|&[^;]*;/g, ''); // 编码特殊字符 input = encodeURIComponent(input); // 去除换行符和制表符 input = input.replace(/\n|\r|\t/g,''); // 限制输入长度 if (input.length > 1000) { input = input.substring(0, 1000); } return input; } // 使用示例 let userInput = "<script>alert('XSS')</script>"; let sanitizedInput = sanitizeInput(userInput); console.log(sanitizedInput); // "alert('XSS')"
输出编码
使用内容安全策略(CSP)
// Apache 服务器在 .htaccess 文件中添加以下内容来设置 CSP 响应头 Header set Content-Security-Policy "default-src 'elf'; script-src 'elf' https://example.com; style-src 'elf' https://fonts.googleapis.com" // Nginx 服务器可以在 server 或 location 块中添加类似以下的配置 add_header Content-Security-Policy "default-src 'elf'; script-src 'elf' https://example.com; style-src 'elf' https://fonts.googleapis.com"; // 配置完成后,重新启动服务器以使更改生效
跨站请求伪造(CSRF)
什么是CSRF
它利用用户在已登录的受信任网站上的身份认证信息,在用户不知情的情况下,以用户的名义向网站发送恶意请求,所以CSRF 攻击通常依赖于网站对用户请求的信任和浏览器在发送请求时自动包含登录凭证(如 Cookie)的特性。
例如,我们登录了一个银行网站,该网站在登录期间会信任我们的浏览器发出的请求。如果攻击者设法诱使我们访问了一个恶意网站,这个恶意网站可能会向银行网站发送一个请求,比如转账请求,而由于我们的浏览器在银行网站的登录状态仍然有效,银行网站可能会处理这个恶意请求,导致资金被非法转移。
CSRF 攻击通常发生在以下场景
交易场景:如在线银行转账、支付平台的资金操作、股票交易等。攻击者可能诱导用户在不知情的情况下执行资金转移、支付或其他重要的金融操作。
账户管理:包括修改密码、更改个人信息、注销账户等。如果用户被诱骗访问恶意页面,可能会在不知情中触发这些关键的账户管理操作。
社交网络:例如发布状态、发送消息、添加或删除好友等。
购物平台:如购买商品、修改订单信息、删除购物车中的物品等操作。
博客系统:发表文章、删除评论、修改个人设置等功能。
内部系统:如请假申请、审批流程、资源分配等业务操作。
CSRF 攻击解决方案
同步令牌模式(Synchronizer Token Pattern): 在服务器生成一个随机且难以预测的令牌(Token),并将其嵌入到表单或请求参数中,前端在提交表单或发送请求时,必须携带这个令牌。服务器端验证请求中的令牌是否有效,如果无效则拒绝请求。
双重提交 Cookie 模式(Double Submit Cookie Pattern): 服务器在用户登录成功后设置一个包含随机令牌的 Cookie,前端在发送请求时,将 Cookie 中的令牌值作为请求参数或请求头的一部分发送,服务器端验证这两个值是否一致来判断请求的合法性。
添加验证码: 在关键操作(如修改密码、转账等)时要求用户输入验证码,这个虽不能直接防止,但可以增加攻击者自动执行请求的难度。
限制请求来源(Origin 和 Referer 检查): 检查请求的来源(Origin 头)或引用页面(Referer 头)是否合法,但需要注意,这些头信息可能会被伪造或不可靠。
使用 SameSite 属性设置 Cookie: 将 Cookie 的 SameSite 属性设置为 Strict 或 Lax,以限制 Cookie 在跨站请求中的发送。
避免使用 GET 请求进行重要操作: GET 请求的参数会显示在 URL 中,容易被利用,尽量使用 POST、PUT 等请求方法进行重要操作。
SQL 注入
什么是SQL 注入
它可以通过前端输入恶意数据,对后端数据库进行非法操作,如果对用户输入的数据没有进行充分的验证和处理,攻击者就可以通过在输入中嵌入特定的恶意 SQL 语句,来改变原本预期的 SQL 操作逻辑。SQL 注入攻击的危害极大,可能导致敏感信息泄露、数据被篡改或删除、服务中断等严重后果,至于多严重,我举两个例子你就知道了:
如果攻击者用
' UNION SELECT * FROM another_table; --
,那么组合后的 SQL 语句就变成了:--
用于注释掉后续的内容,UNION
操作符用于合并两个或多个SELECT
语句的结果集,从而获取到其他表中的数据。假设后端的原始 SQL 查询语句是这样的
如果攻击者将
' OR '1'='1
作为用户名输入,那么组合后的 SQL 语句就变成了:由于 '1'='1 始终为真,所以这个条件总是成立,无论输入的密码是什么,都能绕过正常的身份验证,获取所有用户的信息。
通过无视密码绕过正常的身份验证
SELECT * FROM users WHERE username = '$username' AND password = '$password';
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '$password';
获取其它表中的数据
SELECT * FROM users WHERE username = '' UNION SELECT * FROM another_table; --' AND password = '$password';
SQL注入和上面提到的XSS攻击比较相似,比如双方都是都源于输入验证不足,导致让攻击者有机会利用输入漏洞发起攻击,而且这两种攻击后果都是比较严重的。但它们两者攻击目标不同,SQL 注入攻击侧重于破坏后端数据库,而 XSS 攻击侧重于影响前端用户的浏览器环境和个人信息安全。
SQL 注入攻击通常发生在以下场景
登录页面:攻击者可能在用户名或密码字段中输入恶意的 SQL 语句,试图绕过正常的登录验证逻辑。
搜索功能:当用户输入搜索词时,如果应用程序未对输入进行恰当处理,攻击者可能构造特殊的输入来获取超出预期的搜索结果或访问受限数据。
表单提交:例如用户注册、信息更新等表单,如果输入数据的验证不足,可能成为 SQL 注入的攻击点。
数据排序和筛选:在对数据进行排序或筛选的操作中,攻击者可能通过输入恶意语句来改变数据的处理方式。
动态网页参数:通过在 URL 中的参数部分输入恶意的 SQL 代码,来影响数据库的操作。
API 接口:如果外部调用的 API 接口没有对输入进行严格的验证和处理,也可能遭受 SQL 注入攻击。
后台管理界面:这类界面通常具有更多的权限和操作功能,若防护不当,更容易成为攻击目标。
SQL 注入攻击解决方案
输入验证: 对用户输入的数据进行严格的格式和内容验证。例如,限制用户名和密码只能包含特定的字符类型,禁止特殊字符的输入。
数据类型检查: 确保输入的数据类型与预期相符。比如,如果期望是数字,就检查输入是否为有效的数字。
长度限制: 设定输入字段的最大长度,防止过长的异常输入。
编码处理: 对用户输入进行适当的编码,如 HTML 编码,以防止恶意代码被直接执行。
客户端校验: 使用 JavaScript 等前端脚本在客户端进行初步的输入校验。
点击劫持
什么是点击劫持
在这种攻击中,攻击者通过欺骗手段将一个合法的网站页面嵌套在一个不可见的框架(iframe)内,然后在其上层覆盖一层具有诱导性的界面元素,如按钮、链接等。用户看到的是攻击者设计的具有误导性的界面,当用户进行点击操作时,实际上是在不知情的情况下点击了被隐藏在下方的合法页面中的元素,从而执行了攻击者期望的操作,例如关注某个账号、下载恶意软件、提交敏感信息等。
例如,攻击者创建一个恶意网页,在这个网页中,他们使用 HTML 的 标签来嵌入目标合法网站的页面,通过设置 iframe 的 style 属性,将 width、height 设置为与屏幕相同的大小,并将 opacity(透明度)设置为 0 或接近 0 的值,让嵌入的合法页面不可见(访问典宝题试面端前了解更多安全知识。)。然后,攻击者在这个不可见的 iframe 上层,使用 HTML 和 CSS 来创建具有诱导性的界面元素,比如看似吸引人的按钮、链接或者其他交互元素,并通过精确的定位(使用 CSS 的 position 属性)将这些诱导元素覆盖在合法页面中他们期望用户点击的位置上。当用户访问攻击者的恶意网页时,看到的只是攻击者精心设计的诱导界面,而在不知情的情况下,他们的点击操作实际上作用在了被隐藏在下方的合法页面上。
点击劫持攻击的解决方案
以下是一个使用 JavaScript 检测的简单示例代码:
// window.top!== window.self 用于判断当前窗口是否在一个框架中 // 如果这两个值不相等,说明当前窗口被嵌套在一个框架内 // 如果相等,则表示当前窗口没有被嵌套在框架中,它本身就是顶层窗口 if (window.top!== window.self) { // 被嵌入在 iframe 中 alert('此页面不允许在 iframe 中显示!'); window.top.location.href = window.location.href; }
避免使用透明页面: 尽量避免设计全透明或高度透明的页面,减少被攻击者利用来隐藏恶意界面的可能性。
对敏感操作添加额外的确认步骤: 例如在执行重要操作(如转账、提交个人信息等)之前,要求用户再次输入密码或进行其他形式的二次确认,增加攻击的难度。
使用 X-Frame-Options 响应头: 通过设置此响应头,明确告知浏览器该页面是否允许被嵌套在 iframe 中。常见的取值有 DENY(完全禁止被嵌套)、SAMEORIGIN(仅允许在同域内嵌套)和 ALLOW-FROM uri(指定允许嵌套的来源)。
启用 Content Security Policy(CSP): 通过配置 CSP 策略,可以限制页面中资源的加载来源,包括 iframe 的来源。
JavaScript 检测: 在页面加载时,使用 JavaScript 检测页面是否被嵌入在 iframe 中。如果是,则采取相应的措施,如弹出警告提示或者重定向到正常页面。
DNS 劫持
什么是DNS 劫持
在 DNS 劫持中,攻击者通过篡改 DNS 服务器的解析结果或用户计算机的 DNS 设置,使得用户在访问特定域名时,被错误地引导到攻击者指定的恶意网站,导致用户无法正常访问原本想要访问的合法网站,而是被重定向到包含欺诈内容、恶意软件下载、虚假广告等的不良页面。这个攻击的方式比前面提到的几个攻击要复杂,举几个例子:
通过利用 DNS 服务器软件的漏洞,获取服务器的控制权,从而篡改 DNS 记录。
如果家庭或企业路由器存在安全漏洞,攻击者可以入侵并修改其 DNS 设置,影响连接到该路由器的所有设备。
在网络传输过程中,攻击者拦截用户的 DNS 请求,并返回虚假的 DNS 响应,实现劫持。
诱导用户自己手动更改 DNS 设置为攻击者指定的服务器。
DNS 劫持攻击通常发生在以下场景
公共无线网络环境:如机场、咖啡馆、酒店等提供的免费公共 Wi-Fi,这些网络的安全性相对较低,更容易被 DNS 劫持。
家庭或小型办公网络:如果路由器的密码设置过于简单或者没及时更新路由器的固件以修复安全漏洞,也可能会被攻击者入侵并篡改 DNS 设置。
网络服务提供商(ISP)层面:某些不良的 ISP 可能会出于商业目的或其他恶意原因,对用户的 DNS 进行劫持。
应用程序漏洞:如果用户的操作系统或某些应用程序存在可被利用的漏洞,攻击者可能通过这些漏洞篡改 DNS 设置。
DNS 劫持攻击解决方案
// 在 HTML 中强制使用 HTTPS
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
// 在 JavaScript 中确保请求使用 HTTPS
function makeSecureRequest(url) {
if (!url.startsWith('https://')) {
url = url.replace('http://', 'https://');
}
// 进行后续的请求操作
}
// 简单的证书验证示例(不完整,仅作概念展示)
function validateCertificate(certificate) {
// 检查证书是否在有效期内
const currentDate = new Date();
if (currentDate < certificate.notBefore || currentDate > certificate.notAfter) {
return false;
}
// 检查域名是否匹配
if (window.location.hostname!== certificate.subject.commonName) {
return false;
}
return true;
}
监测 DNS 解析: 可以通过 js 定期检测 DNS 解析是否正常,例如检测访问的域名是否与预期的 IP 地址匹配。但这种方式的效果有限,因为如果 DNS 已经被劫持,检测本身的请求也可能被误导。
验证证书: 在前端代码中,可以对服务器返回的 SSL 证书进行一些基本的验证,例如检查证书的有效期、域名匹配等。
加密通信(使用 HTTPS 协议): HTTPS 通过在客户端和服务器之间建立加密连接来保护数据传输的安全,确保启用了有效的 SSL/TLS 证书,并在前端代码中强制所有请求都使用 HTTPS。
JSON 劫持
什么是JSON 劫持
JSON劫持也是一种网络攻击手段,利用浏览器的同源策略漏洞和 JavaScript 的一些特性,尝试获取其他网站以 JSON 格式返回的数据。
例如,如果一个网站以 JSON 格式返回一些数据,并且没有采取适当的防护措施(如设置正确的 HTTP 响应头),攻击者可以通过在自己的网页中使用
<script>
标签来发起跨域请求,并尝试访问和操作返回的 JSON 数据。因为对于<script>
标签加载脚本资源的请求,浏览器在某些情况下允许跨域获取数据。如果服务器返回的是 JSON 格式的数据,并且没有正确设置相关的 HTTP 响应头来限制跨域访问,浏览器会将其视为可解析和操作的 JavaScript 代码,从而让攻击者有机会获取和处理这些数据,这是利用了浏览器的默认行为和服务器端配置的疏忽,从而实现了跨域请求并尝试劫持 JSON 数据。
JSON 劫持解决方案
对敏感数据进行加密处理
确保服务器正确配置 Access-Control-Allow-Origin 等 CORS 响应头,只允许受信任的源进行访问。
在服务器端对请求的来源进行验证,确保请求来自合法的网站。
JSON 劫持有时会与 CSRF 攻击结合,所以要采取措施添加 CSRF 令牌等来防范。
通过配置 CSP 策略,限制脚本的来源和执行。
在传输之前对 JSON 数据进行加密,在客户端使用相应的密钥进行解密。
目录遍历攻击
什么是目录遍历攻击
攻击者可以通过操纵输入参数来突破应用程序对文件和目录访问的限制,访问服务器上不应被公开访问的内容,这有可能会导致敏感信息泄露、服务器被非法控制等严重后果。关于如何突破限制,办法还是有很多的,比如直接更改网页 URL 中与文件下载或浏览功能相关的路径参数、利用自动化工具、利用 API 漏洞等。访问典宝题试面端前了解更多安全知识。
目录遍历攻击解决方案
function validatePath(inputPath) { // 定义合法的模式,例如只允许特定的文件名和目录结构 const validPattern = /^[a-zA-Z0-9_]+(\/[a-zA-Z0-9_]+)*$/; return validPattern.test(inputPath); }
function sanitizePath(inputPath) { return inputPath.replace(/\.\.\//g, ''); }
const allowedPaths = ['/uploads/user1/', '/downloads/public/']; function isAllowedPath(inputPath) { return allowedPaths.includes(inputPath); }
通过清晰的界面提示和说明,引导用户正确输入合法的路径,减少错误和恶意输入的可能性。
建立一个可接受的文件路径或目录的白名单,只允许用户在这个范围内进行操作。
在将用户输入提交到服务器之前,在前端进行初步的过滤和清理,去除可能用于目录遍历的特殊字符,如
../
。
对用户输入的与文件路径或目录相关的内容进行严格的验证。确保输入符合预期的格式和范围,例如只接受特定格式的文件名或限制目录的层级。
会话固定攻击
什么是会话固定攻击
在这种攻击中,攻击者首先获取或预设一个有效的会话标识符(Session ID),然后诱使用户使用这个预先设定好的会话 ID 来登录网站或应用程序。这样攻击者就能够通过相同的会话 ID 访问用户在登录后的会话,获取用户的敏感信息或执行未经授权的操作。
例如,攻击者可能通过发送包含特定会话 ID 的链接给用户,当用户点击该链接并登录系统时,就使用了攻击者预设的会话 ID,此后攻击者凭借这个会话 ID 就能以用户的身份与系统进行交互。
会话固定攻击解决方案
每次页面加载或用户操作时生成新的会话 ID:在前端代码中,可以通过 JavaScript 与后端进行交互,请求后端为每次页面加载或重要操作重新生成新的会话 ID。
禁止客户端设置会话 ID:确保前端代码无法直接设置或修改会话 ID,只接受后端分配的会话 ID。
登录和重要操作后重定向:在用户成功登录或进行重要操作(如修改密码、进行支付等)后,执行页面重定向,以触发后端重新生成会话 ID。
检查会话 ID 的有效性:在前端可以定期(但不过于频繁)与后端通信,检查当前会话 ID 的有效性和状态。
HTTP 请求走私
什么是HTTP 请求走私
攻击者通过发送不符合服务器预期的 HTTP 请求,利用不同服务器组件(如前端代理服务器和后端应用服务器)对 HTTP 请求规范理解和处理方式的不一致,来达到绕过安全机制、干扰正常请求处理流程或获取未经授权访问的目的。
发送不符合预期请求的方式很多,例如篡改分隔请求头和请求体的特定分隔符(如“\r\n”),在一个请求中同时使用不同的分隔符,导致前端和后端服务器对请求边界产生不同理解。还会利用“Content-Length”头指定请求体长度和“Transfer-Encoding: chunked”表示分块传输的不一致,同时设置且使其值相互矛盾,让不同服务器以不同方式处理请求。此外,攻击者可能在看似正常的请求中嵌入隐藏请求,使前端服务器处理外层请求,而后端服务器能解析出隐藏请求。甚至利用服务器处理请求的时间差异,发送延迟处理或顺序处理会产生不同效果的请求,造成服务器处理逻辑混乱。
HTTP 请求走私解决方案
确保前端代码遵循 HTTP 协议的标准,不进行任何可能导致请求格式异常的操作。
对用户输入的数据进行严格验证和清理,防止恶意构造的请求数据被发送到服务器。
选择经过安全审计和广泛使用的前端框架和库,因为这些通常会遵循安全的 HTTP 请求处理方式。
设置监控机制,检测异常的请求模式或流量,及时发出报警并采取相应措施。
浏览器缓存中毒
什么是浏览器缓存中毒
浏览器缓存中毒是一种相对复杂的攻击方式,在正常情况下,当用户访问一个网页时,浏览器会根据网页的缓存策略将部分资源(如 HTML 文件、CSS 样式表、JavaScript 脚本、图片等)存储在本地缓存中。这样当用户再次访问该网页或者访问同一网站的其他页面时,如果缓存中的资源仍然有效,浏览器就会直接从缓存中加载这些资源,以提高网页加载速度和用户体验。
攻击者会利用各种漏洞和手段来篡改这些缓存的内容,假设攻击者在我们网站上发现了一个 XSS 漏洞,他会构造一个包含恶意脚本的特殊链接,设法诱使用户点击这个链接,当用户点击后,这个恶意脚本会尝试获取浏览器缓存中与网站相关的资源,并对其进行篡改。比如它可能修改 CSS 样式表或者修改 JavaScript 脚本插入恶意代码等。
另外,攻击者还可能利用中间人攻击(MITM),在数据传输过程中拦截和篡改网页资源,然后将被篡改的资源存入用户的浏览器缓存,一旦缓存被成功篡改,后续当用户再次访问该网站时,浏览器就会加载这些被中毒的缓存资源,导致用户看到不正常的页面内容、遭受信息窃取或者受到其他恶意行为的影响。
这种攻击的危害在于用户可能在毫无察觉的情况下受到欺骗和攻击,并且由于问题出在本地缓存中,可能难以被及时发现和解决。
浏览器缓存中毒解决方案
实施严格的内容安全策略(CSP),这个方案在前面也提到过。
对静态资源使用版本控制和唯一标识符,构建或发布前端应用时,给静态资源生成唯一的版本号或哈希值,在 HTML 页面中引用这些资源时,使用带有版本号或哈希值的文件名,当资源内容有更新时,新的版本号或哈希值会使浏览器认为这是一个新的资源,从而避免使用缓存的旧版本。
设置缓存控制头,对于动态生成的页面内容,设置
Cache-Control: no-cache, no-store, must-revalidate
,表示每次请求都需要从服务器获取最新内容,不允许使用缓存。对于静态资源,根据更新频率设置合适的缓存时间,比如Cache-Control: max-age=31536000 (一年)
,但同时添加 ETag 或 Last-Modified 头,以便浏览器在后续请求时可以进行条件请求,验证资源是否有更新。
当然网络攻击的形式远不止于此,前端安全作为整个网络应用安全体系的第一环,尽管存在一定局限性,但也可以大大提高应用的安全。大家在日常的开发中,都做过哪些与数据加密、用户认证或者防范网络攻击等安全相关的工作,有兴趣的同学可以在评论区里聊一聊。
原文链接:https://juejin.cn/post/7388827547513552948
作者:漠水
最后
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波👍