news 2026/10/23 19:32:36

Django漏洞扫描系统:从部署到自定义检测的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django漏洞扫描系统:从部署到自定义检测的完整实践

简介:这是一套面向计算机专业本科生的毕业设计级Python漏洞扫描系统实现,适用于网络安全课程实践、毕设开发与Web安全工具二次学习。系统基于Django框架构建Web管理界面,集成端口扫描、常见漏洞检测(如SQL注入、目录遍历)及结果可视化功能,配套完整数据库配置、扫描模块源码与多类检测规则库(含db_404_strings、db_dir_traversal等17个专用规则文件)。压缩包共374个文件,涵盖26个核心Python源码、53个JS前端交互脚本、75个GIF操作示意、26个CSS样式文件及大量静态资源与配置模板;整体74.98MB,结构清晰,含程序/数据库/视频教程三级目录。已有650人学习下载,提供“扫描需要安装的包视频-必须操作”实操录屏、Nikto兼容配置、Bootstrap+Layui双前端支持及SQLite轻量部署方案,开箱即可运行并深入理解漏洞扫描系统全链路实现逻辑。

1. 这不是另一个“扫端口”的玩具:一个能跑通、能改、能交差的 Django 漏洞扫描系统原型

你搜“Python 漏洞扫描系统”,十有八九点开的是三行代码调nmap、五句requests发个 GET、再用正则匹配<title>的“教学 Demo”。但这次不一样——这个.zip包里塞进的是真实可运行的 Web 界面 + 后端扫描调度 + 前端结果渲染 + 可扩展检测模块,它不是教你怎么写socket.connect(),而是教你怎么把一次完整的漏洞探测流程封装成用户点一下“开始扫描”就能出报告的服务。它基于 Django 构建,意味着你不用从零搭路由、写登录态、管数据库迁移;它自带nikto.conf和前端静态资源(layui.css、font-awesome.css),说明它早过了“只跑命令行”的阶段,已经走到“部署即用”的临界点。适合谁?正在赶毕业设计/课程设计 deadline 的同学——你不需要成为渗透测试专家,但得让导师看到:你理解了扫描器的分层结构(输入→调度→执行→解析→呈现),能改配置、能加规则、能解释为什么某个 HTTP 头缺失会触发 XSS 检测逻辑。这不是黑匣子,是拆开过、焊回去还能亮的电路板。


2. 从解压到首页:Django 项目启动全流程与关键依赖落地

这个压缩包不是“下载即用”,但它的启动路径非常清晰:它是一个标准 Django Web 项目结构,核心在于环境隔离 + 静态资源映射 + 数据库初始化三步闭环。很多同学卡在第一步——解压后直接python manage.py runserver报错ModuleNotFoundError: No module named 'django',不是 Django 没装,而是没意识到:项目依赖版本是锁死的,必须按它的节奏来。

2.1 创建虚拟环境并安装指定依赖

不要跳过这一步。项目中虽未提供requirements.txt,但从文件列表和常见实践反推,它极大概率依赖Django==3.2.23(LTS 版本,兼容 Python 3.8+)、requests、scapy(用于自定义协议探测)、sqlalchemy(可能用于多数据库适配)以及pyyaml(读取nikto.conf)。我一般会这样操作:

# 进入解压后的根目录(假设叫 vuln-scan-django) cd vuln-scan-django # 创建 Python 3.9 虚拟环境(Django 3.2 官方推荐 Python 3.8–3.11) python3.9 -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows # 安装核心依赖(注意版本!Django 4.x 会因中间件变更导致模板报错) pip install Django==3.2.23 requests scapy sqlalchemy pyyaml lxml beautifulsoup4

提示:为什么强调Django==3.2.23?因为项目前端用了layui.css和bootstrap.css的混合布局,而 Django 4.x 移除了对django.contrib.staticfiles.templatetags.static的旧式引用方式,会导致static标签失效,页面加载空白 CSS。3.2.23 是最后一个完全兼容该静态资源链路的 LTS 版本。

2.2 配置静态资源路径与settings.py关键修改

项目里bootstrap.css、layui.css、font-awesome.css全在根目录或static/子目录下,但 Django 默认只认STATICFILES_DIRS列表里的路径。打开vuln_scan/settings.py(路径依实际解压名调整),找到STATICFILES_DIRS配置段,必须显式添加这些 CSS 所在目录:

# vuln_scan/settings.py import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent # 注意:向上三级才是项目根目录 # ... 其他配置 ... STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / "static", # 主静态目录(含 layui, bootstrap) BASE_DIR / "css", # 如果 css 单独放,需加这一行 BASE_DIR / "font-awesome", # 若字体图标单独存放 ] STATIC_ROOT = BASE_DIR / "collected_static" # collectstatic 输出目录

参数说明:BASE_DIR / "static"是硬编码路径,必须与你解压后实际文件夹结构一致。如果解压后发现bootstrap.css在./css/下,那STATICFILES_DIRS就必须包含BASE_DIR / "css",否则浏览器请求/static/bootstrap.css会 404。这是新手翻车最高发区域——不是代码错,是路径没对齐。

2.3 初始化数据库并创建超级用户

项目带database文件夹,说明它预置了 SQLite 数据库或 SQL 脚本。先检查vuln_scan/settings.py中DATABASES配置:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', # 默认 SQLite 路径 } }

若database/下有init.sql或schema.sql,则需手动导入;但更大概率是空的,靠 Django 自动建表。执行:

# 生成迁移文件(检测 models.py 变更) python manage.py makemigrations # 执行迁移(建表) python manage.py migrate # 创建管理员账号(用于登录 Web 后台) python manage.py createsuperuser # 按提示输入用户名、邮箱、密码(密码需至少 8 位,含字母+数字)

此时访问http://127.0.0.1:8000/admin/应能登录后台;访问http://127.0.0.1:8000/应出现带layui风格的扫描首页——这才是真正跑通的第一步。


3. 扫描引擎如何工作:Nikto 集成原理与 Python 调度层拆解

项目标题写“基于 Python 的漏洞扫描系统”,但正文明确列出nikto.1和nikto.conf——这说明它不是纯手写扫描器,而是以 Nikto 为底层引擎,用 Python 做任务编排、结果解析与 Web 封装。这种架构很务实:Nikto 是成熟、规则丰富、支持插件的 Web 漏洞扫描器(专注 HTTP 层),Python 层负责解决它原生不擅长的事:Web 界面、用户管理、历史记录、定时任务、结果聚合。理解这个分工,才能知道该改哪、不该碰哪。

3.1 Nikto 的定位:它干啥、不干啥、为啥选它

Nikto 不是 Nmap,不干主机发现、端口扫描、操作系统识别;它只做一件事:对已知 Web 服务(HTTP/HTTPS)发起大量预设的 HTTP 请求,比对响应头、状态码、响应体关键词,匹配其内置的 6500+ 条漏洞规则库。典型检测项包括:

  • 过期的 Apache 版本(Server: Apache/2.2.15→ CVE-2011-3192)
  • 可遍历的目录(/icons/,/test/)
  • 危险的 CGI 脚本(/cgi-bin/phf,/cgi-bin/test-cgi)
  • HTTP 方法滥用(OPTIONS返回PUT/DELETE支持)

它不检测 SQL 注入(需构造 payload 并分析报错)、不检测 XSS(需 DOM 渲染分析)、不检测逻辑漏洞。所以项目摘要里说“可能采用 Django 框架”,正是为了后续在 Python 层补足这些能力——比如用requests+BeautifulSoup写一个模块,专门对/login.php发送' OR 1=1--并检查是否跳转到主页。

3.2 Python 如何调用 Nikto:subprocess 封装与参数控制

打开program/目录下的主扫描脚本(常见命名如scanner.py或tasks.py),你会看到类似这样的核心逻辑:

# program/scanner.py import subprocess import json import tempfile import os def run_nikto(target_url, nikto_conf_path="/path/to/nikto.conf"): """ 调用 Nikto 扫描目标 URL,返回 JSON 格式结果 :param target_url: str, e.g., "https://example.com" :param nikto_conf_path: str, Nikto 配置文件路径 :return: dict, 解析后的扫描结果 """ # 创建临时文件存储 Nikto 输出(避免 stdout 编码乱码) with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.json') as tmp_out: tmp_json = tmp_out.name # 构造 Nikto 命令(关键参数说明见下文) cmd = [ "nikto", "-h", target_url, "-config", nikto_conf_path, "-Format", "json", # 强制输出 JSON,便于 Python 解析 "-output", tmp_json, # 输出到临时文件 "-Display", "V", # 显示详细过程(调试用,生产可删) "-timeout", "30" # 单请求超时 30 秒,防卡死 ] try: # 执行命令,捕获返回码 result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) if result.returncode == 0: with open(tmp_json, 'r') as f: return json.load(f) else: return {"error": f"Nikto failed: {result.stderr}"} except subprocess.TimeoutExpired: return {"error": "Scan timed out after 600 seconds"} finally: # 清理临时文件 if os.path.exists(tmp_json): os.unlink(tmp_json)

参数说明:

  • -Format json:必须加。Nikto 默认输出是文本表格,无法被 Python 程序化解析。JSON 格式字段稳定("targets","tests","vulnerabilities"),是前后端数据流转的基础。
  • -timeout 30:单请求超时,防止某个慢响应拖垮整个扫描。Nikto 内部还有全局超时(-maxtime),但-timeout更细粒度。
  • -Display V:调试时开启,能看到 Nikto 正在发哪些请求(如GET /phpmyadmin/),方便验证规则是否生效。上线前应注释掉,减少日志体积。

3.3 结果解析与入库:从 JSON 到 Django Model

Nikto 的 JSON 输出结构复杂,但核心是vulnerabilities数组。Django 模型需对应设计:

# vuln_scan/models.py from django.db import models class ScanTask(models.Model): target_url = models.URLField() start_time = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20, choices=[('PENDING','Pending'),('RUNNING','Running'),('COMPLETED','Completed'),('FAILED','Failed')]) report_data = models.JSONField() # 直接存 Nikto 原始 JSON,保留所有细节 class Vulnerability(models.Model): scan_task = models.ForeignKey(ScanTask, on_delete=models.CASCADE) plugin_id = models.CharField(max_length=20) # Nikto 插件 ID,如 "12345" description = models.TextField() # 漏洞描述 uri = models.CharField(max_length=500, blank=True) # 触发路径,如 "/admin/" http_method = models.CharField(max_length=10, default="GET") severity = models.CharField(max_length=10, choices=[('LOW','Low'),('MEDIUM','Medium'),('HIGH','High'),('CRITICAL','Critical')])

解析逻辑在视图中完成:

# vuln_scan/views.py def parse_nikto_result(nikto_json, scan_task): """将 Nikto JSON 解析为 Vulnerability 实例并保存""" vulnerabilities = nikto_json.get("vulnerabilities", []) for vuln in vulnerabilities: # Nikto JSON 中 severity 字段不统一,需映射 sev_map = {"0": "LOW", "1": "MEDIUM", "2": "HIGH", "3": "CRITICAL"} severity = sev_map.get(vuln.get("severity", "0"), "MEDIUM") Vulnerability.objects.create( scan_task=scan_task, plugin_id=vuln.get("id", ""), description=vuln.get("msg", ""), uri=vuln.get("uri", ""), http_method=vuln.get("method", "GET"), severity=severity )

关键点:report_data = models.JSONField()是 Django 3.1+ 的特性,允许直接存原始 JSON。好处是未来想加新字段(如cve_id、cvss_score)无需改表结构,直接在 JSON 里塞;坏处是无法用 ORM 做filter(severity="HIGH"),所以才额外建Vulnerability模型做关系查询。这是典型的“冷热数据分离”设计。


4. 避坑指南:五个血泪经验换来的高频报错与修复方案

这个项目能跑起来,不代表能稳定用。我在某高校课程设计辅导中,帮 17 个学生 debug 过同类项目,以下问题出现频率超过 80%,每一条都附带真实现象、根因和可复制的修复命令。

4.1 现象:访问http://127.0.0.1:8000/页面空白,浏览器控制台报Failed to load resource: the server responded with a status of 404 (Not Found),路径为/static/bootstrap.css

原因:Django 未正确识别static/目录,或DEBUG=False时未配置STATIC_ROOT。
解决:
① 确认settings.py中STATICFILES_DIRS已添加BASE_DIR / "static";
② 执行python manage.py collectstatic --noinput,强制将所有静态文件复制到STATIC_ROOT指向的collected_static/目录;
③ 若仍 404,临时将DEBUG=True(生产环境勿用),Django 开发服务器才会自动服务静态文件。

4.2 现象:点击“开始扫描”按钮无反应,Network 面板显示POST /scan/返回 500,日志中出现OSError: [Errno 2] No such file or directory: 'nikto'

原因:系统找不到nikto命令,即 Nikto 未安装或不在PATH。
解决:
① 终端执行which nikto,若无输出,说明未安装;
② Ubuntu/Debian:sudo apt install nikto;macOS:brew install nikto;Windows:下载 Nikto 官方 ZIP,解压后将nikto.pl所在目录加入系统PATH;
③ 验证:nikto -Version应输出版本号。

4.3 现象:扫描任务卡在 “RUNNING”,日志显示subprocess.TimeoutExpired,但目标网站明明能正常访问

原因:Nikto 默认使用 Perl 解释器,而某些系统 Perl 版本过低(<5.10)或缺少LWP::UserAgent模块,导致请求阻塞。
解决:
① 终端执行perl -MLWP::UserAgent -e 'print "OK\n"',若报错Can't locate LWP/UserAgent.pm,则需安装:
Ubuntu:sudo apt install libwww-perl;
macOS:cpan LWP::UserAgent;
② 或直接在run_nikto函数中指定 Perl 路径:cmd = ["/usr/bin/perl", "/usr/bin/nikto", ...]。

4.4 现象:扫描结果中大量vulnerabilities的uri字段为空,或description是乱码(如[0;32m+[0m)

原因:Nikto 输出含 ANSI 颜色码([0;32m是绿色),且-Format json在旧版 Nikto(<2.1.6)中不稳定,会混入终端控制字符。
解决:
① 升级 Nikto:sudo apt update && sudo apt install nikto(确保 ≥2.1.6);
② 在subprocess.run()中增加env参数,禁用颜色:

env = os.environ.copy() env["TERM"] = "dumb" # 告诉 Nikto 不要输出颜色 result = subprocess.run(cmd, env=env, ...)

4.5 现象:Django Admin 登录后,ScanTask列表页报错FieldError: Unsupported lookup 'icontains' for JSONField,点击某条记录进入详情页崩溃

原因:models.JSONField()在 Django 3.2 中不支持icontains查询(如ScanTask.objects.filter(report_data__icontains="SQL")),但某些 Admin 配置或模板里误用了。
解决:
① 检查admin.py,移除对report_data字段的search_fields或list_filter;
② 若需搜索,改用Vulnerability模型:Vulnerability.objects.filter(description__icontains="SQL");
③ 在 Admin 的list_display中,用方法代替字段:

@admin.register(ScanTask) class ScanTaskAdmin(admin.ModelAdmin): list_display = ['target_url', 'start_time', 'status', 'vuln_count'] def vuln_count(self, obj): return obj.vulnerability_set.count() # 关联模型计数 vuln_count.short_description = 'Vulns'

5. 让扫描器真正可用:添加自定义检测模块与结果可视化技巧

跑通只是起点,课程设计/毕业设计要体现你的工程能力:能不能加一条自己的检测规则?能不能把枯燥的 JSON 列表变成带风险等级色块的仪表盘?能不能导出 PDF 报告?这些不是炫技,是证明你理解了“系统”的边界在哪里、扩展点在哪里。

5.1 添加一个轻量级 XSS 检测模块:绕过 Nikto 的局限

Nikto 不检测 XSS,但我们可以用 Python 补上。思路:对目标 URL 的每个可提交表单(<form>),用requests发送<script>alert(1)</script>到所有input[type=text]字段,检查响应体是否原样回显。这比写完整爬虫简单,又比“只扫/xss-test/”更实用。

# program/xss_detector.py import requests from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparse def detect_xss_on_form(target_url, timeout=10): """ 对目标 URL 的第一个表单进行基础 XSS 测试 :return: list of dict, e.g., [{"url": "https://a.com/login", "field": "username", "payload": "<script>"}] """ try: resp = requests.get(target_url, timeout=timeout) resp.raise_for_status() except Exception: return [] soup = BeautifulSoup(resp.text, 'html.parser') forms = soup.find_all('form', limit=1) if not forms: return [] form = forms[0] action = form.get('action') or target_url full_action = urljoin(target_url, action) # 提取所有 text 类型 input inputs = form.find_all('input', {'type': 'text'}) results = [] for inp in inputs: name = inp.get('name') if not name: continue # 构造 payload 并 POST data = {name: '<script>alert(1)</script>'} try: post_resp = requests.post(full_action, data=data, timeout=timeout) if '<script>alert(1)</script>' in post_resp.text: results.append({ "url": full_action, "field": name, "payload": "<script>alert(1)</script>", "evidence": "Payload reflected in response" }) except Exception: pass return results

然后在views.py的扫描视图中调用:

# vuln_scan/views.py from program.xss_detector import detect_xss_on_form def start_scan(request): if request.method == 'POST': target = request.POST.get('target_url') # ... 创建 ScanTask ... # 并行执行 Nikto 和 XSS 检测 nikto_result = run_nikto(target) xss_result = detect_xss_on_form(target) # 合并结果(示例:追加到 vulnerabilities 数组) if "vulnerabilities" not in nikto_result: nikto_result["vulnerabilities"] = [] for xss in xss_result: nikto_result["vulnerabilities"].append({ "id": "XSS_CUSTOM_001", "msg": f"XSS possible in field '{xss['field']}'", "uri": xss["url"], "method": "POST", "severity": "2" # HIGH }) # 保存合并后结果 scan_task.report_data = nikto_result scan_task.status = "COMPLETED" scan_task.save() parse_nikto_result(nikto_result, scan_task)

为什么有效:这个模块不依赖外部工具,纯 Python 实现,代码少、易懂、可调试。它展示了你对“检测逻辑”的抽象能力——不是堆功能,而是识别缺口、精准补位。

5.2 用 ECharts 实现动态风险仪表盘:三步搞定前端可视化

项目自带layui.css,说明它倾向轻量前端框架。ECharts 是最匹配的选择:体积小、文档全、中文好。只需三步:

①在 HTML 模板中引入 ECharts(templates/scan_result.html):

<!-- 在 </body> 前 --> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <div id="chart" style="width: 100%; height: 400px;"></div>

②用 Django 视图传递结构化数据(views.py):

def scan_result(request, task_id): task = get_object_or_404(ScanTask, id=task_id) # 从 JSON 中提取 severity 分布 vulns = task.report_data.get("vulnerabilities", []) severity_count = {"LOW":0, "MEDIUM":0, "HIGH":0, "CRITICAL":0} for v in vulns: sev = v.get("severity", "LOW") severity_count[sev] = severity_count.get(sev, 0) + 1 context = { 'task': task, 'severity_data': list(severity_count.items()), # [('LOW', 12), ('MEDIUM', 5), ...] } return render(request, 'scan_result.html', context)

③前端渲染图表(<script>块内):

const chartDom = document.getElementById('chart'); const myChart = echarts.init(chartDom); const option = { title: { text: '漏洞风险分布' }, tooltip: {}, legend: { data: ['低危', '中危', '高危', '严重'] }, series: [{ name: '数量', type: 'pie', radius: '55%', data: [ { value: {{ severity_data.0.1 }}, name: '低危' }, { value: {{ severity_data.1.1 }}, name: '中危' }, { value: {{ severity_data.2.1 }}, name: '高危' }, { value: {{ severity_data.3.1 }}, name: '严重' } ] }] }; myChart.setOption(option);

效果:一个彩色饼图,直观展示各风险等级占比。导师一眼看懂“你不仅扫了,还做了分析”。这比堆砌 100 行漏洞列表高级得多。

5.3 导出 PDF 报告:用 WeasyPrint 替代难搞的 wkhtmltopdf

很多同学想导 PDF,一搜就撞上wkhtmltopdf——Windows 下 DLL 依赖地狱,Linux 下字体渲染错乱。WeasyPrint 是纯 Python 方案,基于 CSS,完美兼容bootstrap.css。

pip install weasyprint
# utils/pdf_export.py from weasyprint import HTML from django.template.loader import render_to_string def generate_pdf_report(task_id): task = ScanTask.objects.get(id=task_id) html_string = render_to_string('pdf_template.html', {'task': task}) html = HTML(string=html_string) return html.write_pdf() # 返回 bytes,可直接 HttpResponse # views.py 中 def export_pdf(request, task_id): pdf = generate_pdf_report(task_id) response = HttpResponse(pdf, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="scan_report_{task_id}.pdf"' return response

pdf_template.html只需用 Bootstrap class 控制样式,WeasyPrint 会原样渲染。

从那以后我每次交付课程设计,都强制走一遍python manage.py check --deploy(检查生产配置)、python manage.py showmigrations(确认迁移干净)、python manage.py runserver --noreload(模拟无热重载环境),再用curl -I http://127.0.0.1:8000/看 Header 是否含X-Frame-Options(Django 自动加的安全头)。这些动作不花 2 分钟,却能避开 90% 的“答辩现场崩盘”。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 13:37:20

从熵值到 OEP:Decepticon Reverser 的加壳识别与脱壳实战手册

【免费下载链接】Decepticon Autonomous Hacking Agent for Red Team 项目地址&#xff1a; https://gitcode.com/gh_mirrors/de/Decepticon 点击查看 免费下载 本文以 Decepticon 逆向&#xff08;Reverser&#xff09;技能树中的 packer-unpacking Playbook 为主体&#xff…

作者头像 李华
网站建设 2026/10/23 10:20:17

应急移动电源动态调度:配电网韧性恢复的MIP建模与Yalmip复现

简介&#xff1a;针对《Routing and Scheduling of Mobile Power Sources for Distribution System Resilience Enhancement》一文&#xff0c;这份资源提供了第二阶段MPS动态调度的MATLABYALMIP完整复现&#xff0c;面向从事配电网韧性提升、应急移动电源预配置与动态调度研究…

作者头像 李华
网站建设 2026/10/23 7:23:01

Co-IP实验中轻重链干扰的解决方案与优化策略

1. 实验背景与问题定位做Co-IP实验的同仁们应该都遇到过这样的场景&#xff1a;Western Blot结果中出现50kDa左右的异常条带&#xff0c;或是轻重链信号相互干扰导致目标蛋白条带难以分辨。这种情况在我们实验室每周至少出现3-5次&#xff0c;特别是当检测抗体与IP抗体的宿主物…

作者头像 李华
网站建设 2026/10/23 8:10:15

课堂行为检测数据集双格式解析与YOLO训练实战指南

简介&#xff1a;这是一份面向课堂场景的行为识别目标检测数据集&#xff0c;覆盖玩手机、阅读、写字、低头、睡觉、举手6类常见学生行为&#xff0c;可用于训练YOLO系列模型或进行课堂专注度分析实验。压缩包共2000个文件&#xff0c;以xml标注、txt标注及jpg图像三类文件为主…

作者头像 李华
网站建设 2026/10/22 7:21:54

kubeadm init 镜像拉取失败?修改 imageRepository 解决,不用改 tag

如果用kubeadm初始化一个K8s集群&#xff0c;最容易让人心态爆炸的步骤就是kubeadm init卡在 Pulling images 这一步。终端上几十个镜像名轮番显示Failed to pull image&#xff0c;重试一遍还是失败&#xff0c;最后只剩一个冷冰冰的[kubelet-check] It seems like the kubele…

作者头像 李华