当前位置: 首页 > news >正文

彻底理解前端安全面试题(1)—— XSS 攻击,3种XSS攻击详解,建议收藏(含源码)

前言

前端关于网络安全看似高深莫测,其实来来回回就那么点东西,我总结一下就是 3 + 1  = 4,3个用字母描述的【分别是 XSS、CSRF、CORS】 + 一个中间人攻击。当然 CORS 同源策略是为了防止攻击的安全策略,其他的都是网络攻击。除了这 4 个前端相关的面试题,其他的都是一些不常用的小喽啰。

我将会在我的《面试题一网打尽》专栏中先逐一详细介绍,然后再来一篇文章总结,预计一共5篇文章,欢迎大家关注~

本篇文章是前端网络安全相关的第一篇文章,内容就是 XSS 攻击。

一、准备工作

跨站脚本攻击(cross-site scripting),为了和 css 区分所有才叫 XSS【也叫作代码注入攻击】,重点在【脚本】两个字,所以同样都是利用 script 标签,XSS 和后面说的 CSRF 还是有区别的。通过在网站注入恶意脚本,使脚本在用户的浏览器上运行,从而盗取用户的信息或者破环页面的结构。

1.1 拉取仓库

很多知识都需要结合实际的代码来学习,所以本篇文章的基础是需要一个服务端的项目,可以跟着我的这篇文章搭建自己的服务端项目。或者直接克隆我的仓库代码在这个提交上拉一个新分支,本篇文章所有的代码都是在这个提交基础上进行的。


 

1.2 新增 xss 文件夹

在项目的根目录增加一个 xss 文件夹,并且在 xss 下面新建 index.html 和 index.js

__dirname 是 Node.js 中的一个特殊变量,表示当前执行脚本所在的目录的绝对路径。它是全局对象 global 的属性之一,可在任何地方使用。具体可以看这个

至于运行的时候为什么加上目录参数,请看这篇文章 。

1.3 提交代码

二、攻击方式

顾名思义,xss 的攻击方式重点是脚本,就是利用 js 脚本干的一些坏事,主要的攻击方式如下:

  1. 利用脚本获取页面的数据,盗用 cookie、localStorage 等
  2. 破坏页面结构,操作 dom
  3. DOS 攻击,拒绝服务请求,恶意发送请求,占用服务器资源

反正 js 能干的事情都可以利用。

三、攻击类型

xss 的攻击类型分别是 存储型、反射型、DOM 型,下面开始做详细的讲解

3.1 存储型 XSS 攻击

恶意脚本由前端生成、发送并存在目标服务器上,属于服务器端漏洞,重点是:

  1. 这个脚本是前端某个用户写的,好好想想,肯定是这样的,服务端不会无缘无故的多出来数据;
  2. 服务器在接收到前端传的内容后,没有经过检查就存到数据库中
  3. 攻击发生在前端再一次访问存储的数据的时候

3.1.1 具体流程

  1. 一个用户修改一个公共【文件名】为一段脚本,如 
    <img src='invalid-image' onerror='alert("我是秦始皇,加v给我100万")'>
  2. 服务器对【文件名】没有过滤(对不合法的文件名没有进行转义等处理),就保存在服务端数据库了
  3. 当其他用户访问这个公共【文件名】的时候就会触发攻击
  4. 脚本中可以获取网站的 cookie,把 cookie 数据传到黑客服务器【或者是利用脚本破坏页面结构等】
  5. 拿到用户 cookie 信息后,就可以利用 cookie 信息在其他机器上登录改用户的账号,并利用用户账号进行一些恶意操作

3.1.2 代码模拟

我模拟的是【破坏页面结构】的 XSS 攻击,我们再在 XSS 文件夹下新建两个文件

  1. data.txt 用来存数据来模拟数据库
  2. index2.html 用来展示第二个页面,用于访问数据库中的数据

现在我们总共有四个文件了。

系统功能描述:

  1. 一个公开可访问的书籍列表,一个管理员用户 1 可以在页面 1 (index.html) 修改书籍的名称;
  2. 一个普通用户 2, 可以在页面 2(index2.html)访问书籍列表(也就是书籍名)
(1)index.html  的代码
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=`, initial-scale=1.0" /><title>Document</title><style>h1,h2 {margin: 0;}.box {display: inline-flex;flex-direction: column;}.book-name {margin: 10px 0;width: 400px;height:100px}.button {width: 100px;height: 38px;}</style></head><body><div class="box"><h1>【首页,用户1】</h1><h1>设置一个公开的书籍的名称</h1><div>这个书籍是所用用户都可以访问的</div><div>修改书名:</div><textarea class="book-name" id="name" ></textarea><button class="button" onclick="saveName()">保存</button></div><script>function saveName() {const name = document.getElementById('name').value;if (name) {fetch('saveName', {method: 'post',headers: {'content-type': 'application/json'},body: JSON.stringify({name: name}),}).then(() => {console.log('保存成功 姓名:', name)})}}</script></body>
</html>
(2)index2.html 的代码
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=`, initial-scale=1.0" /><title>Document</title><style>h1,h2 {margin: 0;}.box {display: inline-flex;flex-direction: column;}.book-name {margin: 10px 0;width: 400px;height: 100px;}.button {width: 100px;height: 38px;}</style></head><body><div class="box"><h1>【另一个页面,用户2】</h1><h1>书籍列表页面</h1><div>访问公开的书籍</div><div>书名</div><div id="name"></div></div><script>// 初始化的时候就获取数据fetch('/getName').then((res) => {return res.json();}).then((data) => {const name = data.name;const ele = document.getElementById('name');// 设置书名ele.innerHTML = name;});</script></body>
</html>
(3)index.js 的代码
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser')
const fs = require('fs')const app = express();app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());// 首页 -> 用户1 保存数据
app.get('/', function (req, res) {res.sendFile(path.join(__dirname, '/index.html'));
});// 首页 -> 保存书名
app.post('/saveName', function (req, res) {const { name } = req.bodyfs.writeFileSync(path.resolve(__dirname, 'data.txt'), name)res.send('保存成功');
});// 新建另一个页面 
app.get('/index2', function (req, res) {res.sendFile(path.join(__dirname, '/index2.html'));
});
// 另一个页面 -> 用户2 获取数据
app.get('/getName', function (req, res) {// 从 data.txt 中取出存储的书名const name = fs.readFileSync(path.resolve(__dirname, 'data.txt')).toString()console.log(name)res.send(JSON.stringify({name: name,}));
});app.listen(3000);

 注意 xss/index.js 中关于使用 express 写服务端代码,有两个知识点

(1)post 请求需要使用 body-parser 解析 body 的 json 数据

(2)写入/读取 data.txt 是用node 的核心模块 fs,方法 fs.writeFileSync / s.readFileSync

(3)fs.readFileSync 的结果是 buffer 需要转成字符串 toString()

data.txt 里面不必输入任何内容,我们会通过代码进行写入。

(4)运行代码
npm run dev xss

访问 localhost:3000,输入一段恶意的代码作为书名,点击保存


 

点击保存之后,输入的内容会保存在 data.txt 中

 再打开 localhost:3000/index2.html,这样一个存储型的、对于 dom 结构的破环的 XSS 攻击就完成了。

这里面有一个知识点就是我们在 index.html 中保存的恶意脚本是 

<img src='invalid-image' onerror='alert("我是秦始皇,加v给我100万")'>

我们用了一个 img 标签,然后使用 onerror 事件里面写一些脚本,而没有这么写,为啥呢?

<script>alert(1)</script>

这是因为我们在 index2.html 里面使用 innerHTML 将数据渲染在页面上,使用 innerHTML 直接渲染 script 字符串,脚本是不会被执行的。 而事件处理器,如 img 的 onerror 事件却可以触发。

(5)提交代码

3.2 反射型 XSS 攻击

恶意脚本在前端访问的 URL 中,要用户主动点击 URL,服务器解析 URL, 并返回恶意脚本,属于服务端漏洞,重点是:

  1. 恶意脚本不是前端用户手动写的,和存储型有区别
  2. 恶意脚本在 URL 上,需要用户手动点击才能触发攻击
  3. 服务器收到访问 URL 的请求时,解析 URL 得到恶意脚本,然后返回给客户端
  4. 服务器不会存储恶意脚本,只会返回(反射)它

3.2.1 具体流程

  1. 黑客诱导用户访问有恶意代码的 URL ,如 https://danger.com?xss=<script>alert('attack)</script>
  2. 服务器接收到访问 URL 的请求
  3. 服务器解析 URL ,得到 XSS 的值,但是并没有对 XSS 的值做校验是否合法,对于不合法的没有进行转义
  4. 服务器将解析后的结果,反射给浏览器,如返回 { xss : <script>alert('attack)</script> }
  5. 浏览器的恶意代码的 URL 页面有渲染 XSS 值的逻辑
  6. 此时浏览器弹出警告框
  7. 一个反射型 XSS 攻击就完成了

黑客经常会通过 qq群或者邮件等渠道诱导用户去点击这些恶意链接。

看到没?没事别惦记乱七八糟的连接,谨防电信诈骗!

3.2.2 代码模拟

(1)新建 xss/ index3.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div>反射型 xss 攻击</div><h1><a href="http://localhost:3000/reflect?xss=<script>alert('哈哈,你上当了')</script>"> 点击收款100万 </a></h1></body>
</html>
(2)修改 index.js 

注意看这个服务端代码关于 reflect 方法的 get 请求,他就是简单的把参数上的获取并返回,浏览器就能运行这段代码。 

(3)运行代码
npm run dev xss

访问 localhost:3000/index3

 跳转之后发生了攻击

(4)提交代码

3.3 DOM 型 XSS 攻击

攻击者通过操纵 DOM 来触发攻击,是前端漏洞,不牵扯到服务器。重点是:

  1. 整个过程服务器不参与
  2. 脚本的具体来源还是利用网页中用户交互的部分, URL 参数、表单输入、cookie 等
  3. 多发生在使用 innerHTML 的场景

3.3.1 具体流程

  1. 从 URL 中取出恶意代码/或者从用户输入的表单/ cookie 中
  2. 把恶意代码使用 innerHTML 插入页面,改变 dom 结构

尤其是在使用 innerHTML 的时候会出现这种问题,可以使用插件如 xss-filters 避免这一类问题,这个插件的原理是将某些字符进行转义,将 < 转成 &lt 等

3.3.2 代码模拟

(1)新增 index4.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=<device-width>, initial-scale=1.0"><title>Document</title><style>textarea {width: 400px;height: 50px;}</style>
</head>
<body><h1>dom 型 xss 攻击</h1><div>输入</div><textarea id="input"></textarea><button onclick="save()">保存</button><hr><div>输出</div><div id="output"></div><script>function save() {const text = document.getElementById('input').valueif (text) {const output = document.getElementById('output')output.innerHTML = text}}</script>
</body>
</html>
(2)修改 xss/index.js

(3)运行代码
npm run dev xss

 点击保存按钮之后,就出现攻击了。

注意,这全程没有服务端的参与,但是我们还是修改 index.js 。但是其实 index.js 里面的代码就是为了起一个服务运行 index4.html 而已, 应该很好理解吧。

(4)提交代码

好了至此三种类型的 XSS 攻击我们都用代码的形式实现。一点都不难吧,光说不练假把式,如果只看理论知识,肯定云里雾里,你跟着一起写一遍代码就理解了,不用背诵就记下来了。

3.4 XSS 攻击防御方法

  1. 不使用服务端渲染,前两种是服务端的安全漏洞,服务器不能信任前端输入的任何东西,服务端要对输入脚本进行过滤或者转码。<script> 标签被转换为 &lt;script&gt; ,注意,https 不能防止安全问题,只会增加攻击难度和成本;
  2. 对于 dom 型,对要插入的 html 做好充分的转义, npm 包:xss-filters;
  3. 使用内容安全策略,csp 建立白名单,告诉浏览器可以执行和加载哪些功能 Content-Security-Policy 服务端设置【设置 csp 有两种方式 http 头部、meta 标签】
  4. x-content-type-options\x-frame-options\x-xss-protentcion 等httip头部设置
  5. 敏感信息 cookie 设置 httpOnly

总结

xss 攻击的三种类型都用代码模拟了,我的仓库地址如下,欢迎查看

yangjihong2113/learn-express

内容较多,难免疏漏,如有问题,欢迎指正。

这是一系列的文章,关于网络安全的内容还有 CSRF、CORS 和中间人攻击的内容没有总结,持续更新中,欢迎关注。

相关文章:

彻底理解前端安全面试题(1)—— XSS 攻击,3种XSS攻击详解,建议收藏(含源码)

前言 前端关于网络安全看似高深莫测&#xff0c;其实来来回回就那么点东西&#xff0c;我总结一下就是 3 1 4&#xff0c;3个用字母描述的【分别是 XSS、CSRF、CORS】 一个中间人攻击。当然 CORS 同源策略是为了防止攻击的安全策略&#xff0c;其他的都是网络攻击。除了这…...

UE5.1_AI随机漫游

UE5.1_AI随机漫游 目录 UE5.1_AI随机漫游 AI随机漫游方法 方法1:AI角色蓝图直接写方法...

智慧城市新型基础设施建设综合方案:文件全文52页,附下载

关键词&#xff1a;智慧城市建设方案&#xff0c;智慧城市发展的前景和趋势&#xff0c;智慧城市项目方案&#xff0c;智慧城市管理平台&#xff0c;数字化城市&#xff0c;城市数字化转型 一、智慧城市新基建建设背景 1、城市化进程加速&#xff1a;随着城市化进程的加速&am…...

GitHub Copilot 终极详细介绍

编写代码通常是一项乏味且耗时的任务。现代开发人员一直在寻找新的方法来提高编程的生产力、准确性和效率。 像 GitHub Copilot 这样的自动代码生成工具可以使这成为可能。 GitHub Copilot 到底是什么&#xff1f; GitHub Copilot 于 2021 年 10 月推出&#xff0c;是 GitHub 的…...

LeetCode第63题 - 不同路径 II

题目 解答 class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int m obstacleGrid.length;int n obstacleGrid[0].length;if (obstacleGrid[0][0] 1) {return 0;}if (obstacleGrid[m - 1][n - 1] 1) {return 0;}int[][] dp new int[m][n];dp…...

python+django网上银行业务综合管理系统vue_bvj8b

本课题主要研究如何用信息化技术改善传统网上银行综合管理行业的经营和管理模式&#xff0c;简化网上银行综合管理的难度&#xff0c;根据管理实际业务需求&#xff0c;调研、分析和编写系统需求文档&#xff0c;设计编写符合银行需要的系统说明书&#xff0c;绘制数据库结构模…...

【软件工程】走进瀑布模型:传统软件开发的经典之路

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a; 软件工程 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言&#xff1a; 正文 主要阶段&#xff1a; 优点&#xff1a; 缺点&#xff1a; 应用范围&#xff1a; 结语 我的其他博客 前言&am…...

两个字符串间的最短路径问题 (100%用例)C卷 (JavaPythonNode.jsC语言C++)

给定两个字符串,分别为字符串A与字符串B。例如A字符串为ABCABBA,B字符串为CBABAC可以得到下图m*n的二维数组,定义原点为(0,0),终点为(m,n),水平与垂直的每一条边距离为1,映射成坐标系如下图 从原点(0,0)到(0,A)为水平边,距离为1,从(0,A)到(A,C)为垂直边,距离为1;假设两…...

通过ADB来实现脚本来控制手机

ADB 简介 adb的全称为Android Debug Bridge,安卓调试桥,可以通过调试命令来控制手机,诸如开机,关机等按键控制;或者启动,关闭应用;异或进行触摸模拟. 通过学习adb,可以实现简单的脚本控制,最大的特点是不需要root,对于普通手机都可以进行,帮助我们完成一些简单的重复性事件,…...

机器学习之K-means聚类

概念 K-means是一种常用的机器学习算法,用于聚类分析。聚类是一种无监督学习方法,它试图将数据集中的样本划分为具有相似特征的组(簇)。K-means算法的目标是将数据集划分为K个簇,其中每个样本属于与其最近的簇中心。 以下是K-means算法的基本步骤: 选择簇的数量(K值)…...

SSH 端口转发:如何将服务绑定到本地 IP 地址

在日常工作中&#xff0c;我们经常需要访问位于远程服务器上的服务&#xff0c;如数据库、Web 应用程序或其他类型的服务器。直接访问这些服务可能会因为安全限制或网络配置而变得复杂或不可能。这时&#xff0c;SSH 端口转发就成了我们的得力助手。在本篇博客中&#xff0c;我…...

回归预测 | MATLAB实ZOA-LSTM基于斑马优化算法优化长短期记忆神经网络的多输入单输出数据回归预测模型 (多指标,多图)

回归预测 | MATLAB实ZOA-LSTM基于斑马优化算法优化长短期记忆神经网络的多输入单输出数据回归预测模型 &#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实ZOA-LSTM基于斑马优化算法优化长短期记忆神经网络的多输入单输出数据回归预测模型 &#xff08;…...

python实现图像的二维傅里叶变换——冈萨雷斯数字图像处理

原理 二维傅里叶变换是一种在图像处理中常用的数学工具&#xff0c;它将图像从空间域&#xff08;我们通常看到的像素排列&#xff09;转换到频率域。这种变换揭示了图像的频率成分&#xff0c;有助于进行各种图像分析和处理&#xff0c;如滤波、图像增强、边缘检测等。 在数学…...

We are a team - 华为OD统一考试

OD统一考试 题解&#xff1a; Java / Python / C 题目描述 总共有 n 个人在机房&#xff0c;每个人有一个标号 (1<标号<n) &#xff0c;他们分成了多个团队&#xff0c;需要你根据收到的 m 条消息判定指定的两个人是否在一个团队中&#xff0c;具体的: 消息构成为 a b …...

NFC物联网智慧校园解决方案

近场通信(Near Field Communication&#xff0c;NFC)又称近距离无线通信&#xff0c;是一种短距离的高频无线通信技术&#xff0c;允许电子设备之间进行非接触式点对点数据传输交换数据。这个技术由免接触式射频识别(RFID)发展而来&#xff0c;并兼容 RFID&#xff0c;主要用于…...

鸿蒙系列--组件介绍之容器组件

一、Badge 描述&#xff1a;给其他组件添加标记 子组件&#xff1a;支持单个子组件 1.创建数字标记 Badge(value: {count: number, position?: BadgePosition, maxCount?: number, style: BadgeStyle}) 2.创建字符串标记 Badge(value: {value: string, position?: Badge…...

perl使用find函数踩坑

前言 写了一个脚本可以同时检查多个仿真log文件&#xff0c;并生成html表格。按照文件修改时间从新到旧排序。但是一直无法使用stat函数获取修改时间。 结论&#xff1a;find函数会改变程序执行的当前目录&#xff0c;find(\&process_files, $dir);函数是在$dir目录下运行…...

Java IDEA JUnit 单元测试

JUnit是一个开源的 Java 单元测试框架&#xff0c;它使得组织和运行测试代码变得非常简单&#xff0c;利用JUnit可以轻松地编写和执行单元测试&#xff0c;并且可以清楚地看到哪些测试成功&#xff0c;哪些失败 JUnit 还提供了生成测试报告的功能&#xff0c;报告不仅包含测试…...

深入理解 c++ 函数模板

函数模板是C中的一种强大特性&#xff0c;它允许程序员编写一个可以处理多种数据类型的函数。通过使用模板&#xff0c;我们可以编写一次函数&#xff0c;然后在多种数据类型上使用它&#xff0c;这大大提高了代码的复用性。 1. 基本概念 函数模板是一种参数化类型的工具&…...

系列十二、Linux中安装Zookeeper

一、Linux中安装Zookeeper 1.1、下载安装包 官网&#xff1a;Index of /dist/zookeeper/zookeeper-3.4.11 我分享的链接&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/14Hugqxcgp89f2hqGWDwoBw?pwdyyds 提取码&#xff1a;yyds 1.2、上传至/opt目录 1.3、解…...

docker详细操作--未完待续

docker介绍 docker官网: Docker&#xff1a;加速容器应用程序开发 harbor官网&#xff1a;Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台&#xff0c;用于将应用程序及其依赖项&#xff08;如库、运行时环…...

循环冗余码校验CRC码 算法步骤+详细实例计算

通信过程&#xff1a;&#xff08;白话解释&#xff09; 我们将原始待发送的消息称为 M M M&#xff0c;依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)&#xff08;意思就是 G &#xff08; x ) G&#xff08;x) G&#xff08;x) 是已知的&#xff09;&#xff0…...

Leetcode 3577. Count the Number of Computer Unlocking Permutations

Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接&#xff1a;3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯&#xff0c;要想要能够将所有的电脑解锁&#x…...

【Go】3、Go语言进阶与依赖管理

前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课&#xff0c;做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程&#xff0c;它的核心机制是 Goroutine 协程、Channel 通道&#xff0c;并基于CSP&#xff08;Communicating Sequential Processes&#xff0…...

PL0语法,分析器实现!

简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

HTML前端开发:JavaScript 常用事件详解

作为前端开发的核心&#xff0c;JavaScript 事件是用户与网页交互的基础。以下是常见事件的详细说明和用法示例&#xff1a; 1. onclick - 点击事件 当元素被单击时触发&#xff08;左键点击&#xff09; button.onclick function() {alert("按钮被点击了&#xff01;&…...

第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词

Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵&#xff0c;其中每行&#xff0c;每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid&#xff0c;其中有多少个 3 3 的 “幻方” 子矩阵&am…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”

2025年#高考 将在近日拉开帷幕&#xff0c;#AI 监考一度冲上热搜。当AI深度融入高考&#xff0c;#时间同步 不再是辅助功能&#xff0c;而是决定AI监考系统成败的“生命线”。 AI亮相2025高考&#xff0c;40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕&#xff0c;江西、…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表

##鸿蒙核心技术##运动开发##Sensor Service Kit&#xff08;传感器服务&#xff09;# 前言 在运动类应用中&#xff0c;运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据&#xff0c;如配速、距离、卡路里消耗等&#xff0c;用户可以更清晰…...

Java编程之桥接模式

定义 桥接模式&#xff08;Bridge Pattern&#xff09;属于结构型设计模式&#xff0c;它的核心意图是将抽象部分与实现部分分离&#xff0c;使它们可以独立地变化。这种模式通过组合关系来替代继承关系&#xff0c;从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...