安全 信息打点 第21天——25天php安全开发 Yatming的博客 2025-09-12 2025-06-08
1 2 3 4 5 6 留言板的功能(或者可以说是评论区的功能实现流程): 1. 浏览器输入昵称以及内容进行提交,然后发送给web 2. 再由web,将数据传输给后端的数据库 3. 数据库记录/写入传输过来的值,将其保存到数据库中 4. 然后在将保存的结果(保存成功/保存失败)返回给web 5. web在将这个结果回显到客户机上
创建一个新的数据库
在刚刚创建的数据库中,新建一个表:
创建一些列名,然后再给表名取一个名字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 数据库的结构: 1. 数据库名—>数据库表名—>数据库列名 2. 数据库数据:格式类型,长度,键等 数据库连接的一些常见函数: mysqli_connect() 打开一个到 MySQL 的新的连接。 mysqli_select_db() 更改连接的默认数据库。 mysqli_query() 执行某个针对数据库的查 询。 mysqli_fetch_row() 从结果集中取得一行,并作为枚举数组返回。 mysqli_close() 关闭先前打开的数据库连接。 MYSQL增删改查: 查:select * from 表名 where 列名 条件 增:insert into 表名 列名 1`, ` 列名 2`) value(' 列 1 值 1', ' 列 2 值 2'); 删:delete from 表名 where 列名 = 条件 改:update 表名 set 列名 = 数据 where 列名 = 条件
配置php的解析器:
输出一个hello world看看是否可以正常使用php解析:
php和mysql的一些必要知识点 1 2 3 4 5 6 7 8 9 10 11 12 13 mysql的增删改查: 查:select * from 表名 where 列名='条件'; 增:insert into 表名(`列名1`,`列名2`) value('列1值1','列2值2'); 删:delete from 表名 where 列名='条件'; 改:update 表名 set 列名 = 数据 where 列名='条件'; php中关于mysql操作的一些函数 mysqli_connect() #打开一个mysql的新的连接 mysqli_select_db() #更改连接的默认数据库。 mysqli_query() #执行某个针对数据库的查询 mysqli_fetch_row() #从结果集中取得一行,并作为枚举数组返回 mysqli_close() #关闭先前打开的数据库连接
php使用mysqli_result类处理结果的几种方法
fetch_all()
抓取所有的结果行并且以关联数据,数值索引数组,或者两者皆有的方式返回结果集。
fetch_array()
以一个关联数组,数值索引数组,或者两者皆有的方式抓取一行结果。
fetch_object()
以对象返回结果集的当前行。
fetch_row()
以枚举数组方式返回一行结果
fetch_assoc()
以一个关联数组方式抓取一行结果。
fetch_field_direct()
以对象返回结果集中单字段的元数据。
fetch_field()
以对象返回结果集中的列信息。
fetch_fields()
以对象数组返回代表结果集中的列信息。
php的一些全局变量 1. $GLOBALS
2. $_SERVER
用途 :存储服务器和执行环境的信息(如请求头、路径、脚本位置等)。
常用键值 :
$_SERVER['PHP_SELF']
:当前脚本文件名(如 /index.php
)。
$_SERVER['REQUEST_METHOD']
:请求方法(如 GET
、POST
)。
$_SERVER['HTTP_USER_AGENT']
:客户端浏览器信息。
$_SERVER['REMOTE_ADDR']
:客户端 IP 地址。
注意 :不同服务器环境可能返回不同的值。
3. $_REQUEST
4. $_POST
5. $_GET
6. $_FILES
7. $_ENV
8. $_COOKIE
用途 :读取客户端发送的 HTTP Cookie。
示例 :
1 2 setcookie('user', 'Alice', time()+3600); echo $_COOKIE['user']; // 输出 Alice
注意 :Cookie 数据可能被篡改,需验证来源。
9. $_SESSION
由于22—25天就是开发一个使用了cookie,session,token,文件上传,文件编辑,文件查看,文件下载。拥有管理页面的。一个登录页。所以这里直接就包含所有,一次写完吧。
首先使用deepseek生成一个好看的前端页面,这里我叫login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Yatming的登录页</title> <style> :root { --primary-color: rgba(255, 255, 255, 0.95); --accent-color: #4a90e2; --border-radius: 15px; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; } body { display: flex; justify-content: center; align-items: center; min-height: 100vh; /* 修改后的背景设置 */ background: url('wallhaven-7jggko.jpg') no-repeat center center fixed; background-size: cover; } .container { background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(12px); border-radius: var(--border-radius); padding: 2.5rem; width: 420px; position: relative; border: 1px solid rgba(255, 255, 255, 0.2); } .tabs { display: flex; gap: 1rem; margin-bottom: 2rem; } .tab { flex: 1; padding: 1.2rem; background: rgba(255, 255, 255, 0.1); border: none; border-radius: 8px; font-size: 1.1rem; color: var(--primary-color); cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .tab.active { background: var(--accent-color); transform: translateY(-2px); box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3); } .form-container { position: relative; min-height: 400px; } .form { position: absolute; width: 100%; opacity: 0; transform: translateY(20px); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); visibility: hidden; } .form.active { opacity: 1; transform: translateY(0); visibility: visible; } .input-group { margin-bottom: 1.8rem; position: relative; } input { width: 100%; padding: 1rem; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; font-size: 1rem; color: var(--primary-color); transition: all 0.3s ease; } input::placeholder { color: rgba(255, 255, 255, 0.7); } input:focus { background: rgba(255, 255, 255, 0.2); outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2); } button[type="submit"] { width: 100%; padding: 1.2rem; background: var(--accent-color); color: white; border: none; border-radius: 8px; font-size: 1.1rem; cursor: pointer; transition: all 0.3s ease; letter-spacing: 1px; } button[type="submit"]:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(74, 144, 226, 0.4); } @media (max-width: 480px) { .container { width: 90%; padding: 1.8rem; } .tab { padding: 1rem; font-size: 1rem; } } </style> </head> <body> <div class="container"> <div class="tabs"> <button class="tab active" onclick="switchForm('login')">登录</button> <button class="tab" onclick="switchForm('register')">注册</button> </div> <div class="form-container"> <!-- 登录表单 --> <form id="loginForm" class="form active" action="./login.php" method="POST"> <div class="input-group"> <input type="username" name="username" placeholder="用户名" required> </div> <div class="input-group"> <input type="password" name="password" placeholder="密码" required> </div> <input type="submit" value="登录"> </form> <!-- 注册表单 --> <form id="registerForm" class="form" action="./register.php" method="POST"> <div class="input-group"> <input type="username" placeholder="用户名" required> </div> <div class="input-group"> <input type="email" placeholder="电子邮箱" required> </div> <div class="input-group"> <input type="password" placeholder="密码" required> </div> <div class="input-group"> <input type="password" placeholder="确认密码" required> </div> <input type="submit" name="register" value="注册"> </form> </div> </div> <script> function switchForm(formType) { // 切换选项卡样式 document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); event.target.classList.add('active'); // 切换表单显示 document.querySelectorAll('.form').forEach(form => { form.classList.remove('active'); }); document.getElementById(formType + 'Form').classList.add('active'); } </script> </body> </html>
这里要登录,所以就会涉及数据库,关于php连接mysql有三种方式:
php连接mysql的三种方式 MySQLi - 面向对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php $servername = "localhost" ;$username = "username" ;$password = "password" ; $conn = new mysqli ($servername , $username , $password ); if ($conn ->connect_error) { die ("连接失败: " . $conn ->connect_error); } echo "连接成功" ;?>
MySQLi - 面向过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php $servername = "localhost" ;$username = "username" ;$password = "password" ; $conn = mysqli_connect ($servername , $username , $password ); if (!$conn ) { die ("Connection failed: " . mysqli_connect_error ()); } echo "连接成功" ;?>
PDO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php $servername = "localhost" ;$username = "username" ;$password = "password" ; try { $conn = new PDO ("mysql:host=$servername ;" , $username , $password ); echo "连接成功" ; } catch (PDOException $e ){ echo $e ->getMessage (); } ?>
PDO为什么可以防止注入(扩展) 预处理(Prepared Statements)是数据库操作中的一种安全机制,用于将SQL代码 和用户输入的数据 分离,从而避免SQL注入攻击,同时提高执行效率。以下是其核心原理和流程:
1. 预处理的核心思想
代码与数据分离 : SQL语句的结构 (如表名、字段、条件逻辑)提前定义并固定,用户输入的数据仅作为参数传入,无法修改SQL原有的逻辑结构。
类比 : 类似于“填空题”——SQL语句是固定的模板,用户输入的数据只是填入模板中的“空白处”,无法改变题目本身。
2. 预处理的工作流程 步骤1:定义SQL模板(预编译) 开发者编写一个包含占位符的SQL语句模板,例如:
1 2 3 SELECT * FROM users WHERE username = ? AND password = ?; -- 或者使用命名占位符: SELECT * FROM users WHERE username = :username AND password = :password;
此时,数据库会解析并编译 这个SQL模板,确定其语法和结构(例如检查表名、字段是否存在)。
步骤2:绑定参数 将用户输入的数据(如表单提交的$_POST['username']
)绑定到占位符。例如:
1 2 3 4 // 使用PDO绑定参数 $stmt = $pdo->prepare($sql); $stmt->bindParam(':username', $username); $stmt->bindParam(':password', $password);
数据库明确知道这些参数是数据 而非代码,会对其中的特殊字符(如'
、"
、;
)进行安全处理(例如转义或类型校验)。
步骤3:执行查询 执行时,数据库将预编译的SQL结构和绑定的参数合并,确保参数不会破坏原有逻辑:
3. 预处理如何防止SQL注入?
结构固定 : SQL模板在预编译后,逻辑已确定(例如查询的字段、表名、条件顺序)。后续传入的参数无法改变SQL结构 ,即使包含恶意代码(如' OR 1=1 --
),也只会被视为普通字符串。
自动转义 : 数据库会根据参数类型(如字符串、整数)自动处理特殊字符。例如,字符串参数中的单引号'
会被转义为\'
,使其失去破坏SQL结构的能力。
绕过手动转义的缺陷 : 传统拼接SQL时,依赖开发者手动调用addslashes()
等函数,容易遗漏或处理不当。预处理由数据库底层自动处理,更安全可靠。
4. 预处理的额外优势
性能提升 :
同一SQL模板多次执行时(如批量插入),数据库只需编译一次,后续直接复用,减少解析开销。
例如:循环插入100条数据,预处理效率远高于拼接100条独立SQL语句。
类型安全 :
绑定参数时可指定数据类型(如PDO::PARAM_INT
),避免因类型错误导致的意外行为。
5. 示例对比:预处理 vs 普通查询 不安全写法(直接拼接SQL) 1 2 3 4 $username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; // 若用户输入 username = ' OR 1=1 --,会导致SQL注入!
安全写法(预处理) 1 2 3 4 5 6 7 $sql = "SELECT * FROM users WHERE username = :username AND password = :password"; $stmt = $pdo->prepare($sql); $stmt->execute([ ':username' => $_POST['username'], ':password' => $_POST['password'] ]); // 即使用户输入恶意字符,也会被正确处理为普通字符串。
6. 注意事项
总结 预处理通过分离代码与数据 ,从根本上消除了SQL注入的可能性,是数据库操作中最关键的安全实践。正确使用预处理,配合参数绑定,能有效保护应用免受恶意输入攻击。
php连接数据库,进行账号密码的判断 为了减少代码重复,一般连接数据库的操作都会封装在一个文件,其他文件想要调用,直接使用include
包含即可:
config.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $servername = "localhost" ;$username = "root" ;$password = "root" ;$database = 'myblog' ; $conn = new mysqli ($servername , $username , $password ,$database ); if ($conn ->connect_error) { die ("连接失败: " . $conn ->connect_error); }else { echo "连接成功" ; } ?>
login.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php header ('Content-type:text/html; charset=utf-8' );include './db/config.php' ; $username = $_POST ['username' ]; $password = $_POST ['password' ]; $sql = "select * from users where username='$username ' and password='$password '" ; $result =$conn ->query ($sql ); $row =$result ->fetch_row (); if ($result ->num_rows > 0 ) { echo "登录成功" ; } else { echo "用户名或密码错误" ; } ?>
这样就可以简单判断数据库的账号密码是否正确,这里需要手动添加这个数据库中的值
如果想要更改上面这种未授权,那么第一种就是加上else,或者在if判断里面,判断错误之后直接使用die
函数进行强制退出。
1 2 3 4 5 小知识点: 一段语句一般最外名使用的是双引号进行包裹,然后里面的变量都是使用单引号进行包裹 下面这个是一个js的语句,用来跳转到某个页面: window.location.href='login.html'
cookie和session的使用 cookie和session的区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 cookie:保存在客户端 session:保存在服务端 通俗解释: Cookie:像超市的储物柜小票(你保存小票,凭它取物)。 例子:记住用户名,下次自动填充。 Session:像银行的保险柜(银行保管物品,给你一把钥匙)。 例子:用户登录后,服务端记录登录状态,浏览器只保存钥匙(Session ID)。 应用场景: 用Cookie:不敏感数据 + 需长期保存(如“记住我”功能)。 用Session:敏感数据或临时交互(如购物车、支付流程)。 安全风险: 不管是cookie还是session,当他们的值可以被预判的时候,那么不管你是是保存在服务端还是客户端都是没有用的,只要能被预测下一个值,或者被劫持,那么这两个都有安全风险。 token: 具有唯一性,就是说,每次登录,然后你再次登录的时候token的值就会发生变化,这个时候并不是说过你不退出登录值就不会更改,意思是每次触发登录这个操作的时候,token就会发生改变。
cookie
上面这种就是数据包中带有cookie,这里的cookie等于username
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 setcookie("username",$username,time()+3600); 设置一个超长时间的cookie: setcookie("username",$username,time()+(365*86400)); setcookie("username",$username,time()+(365*86400)); 如果像这里这样进行设置,那么这里在admin.php的判断页面,就要判断两个值,不能像下面那样只对username进行判断。 第一个字段的意思:是cookie参数的名字 第二个字段的意思:是第一个字段的值 第三个字段的意思:是过期时间,这里用时间戳进行表示。 cookie的格式 setcookie(name,value,expire,path,domain,secure) name是cookie的名称 value是cookie的值 expire是失效时间 path是cookie的生效路径 domain是cookie的作用域名范围 secure用于指定是否开启https连接来传输cookie
这里你可以尝试将cookie这个值设置60秒失效,然后观察一下过期之后cookie的字段。
cookie的写法,在后台页面直接判断cookie中有没有我们想要的值(这个值可以伪造)。如果有这个值,那么就直接访问后台页面,如果没有就跳转到登录页面
login.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php header ('Content-type:text/html; charset=utf-8' );include './db/config.php' ; $username = $_POST ['username' ]; $password = $_POST ['password' ]; $sql = "select * from users where username='$username ' and password='$password '" ; $result =$conn ->query ($sql ); $row =$result ->fetch_row (); var_dump ($row ); if ($result ->num_rows > 0 ) { setcookie ("username" ,$username ,time ()+360 ); echo '<script>alert("用户:' .$username . '登录成功");window.location.href="./admin.php"</script>' ; }else { echo "<script>alert('用户名或密码错误');</script>" ; } ?>
admin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>后台管理</title> </head> <body> <h1>这是一个后台页面</h1> </body> </html> <?php if (empty ($_COOKIE ['username' ])){ echo '<script>alert("没有cookie这个值");window.location.href="./login.html"</script>' ; }else { echo "欢迎来到yatming的后台" ; echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>' ; } ?>
session login.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?php session_start ();header ('Content-type:text/html; charset=utf-8' );include './db/config.php' ; $username = $_POST ['username' ]; $password = $_POST ['password' ]; $sql = "select * from users where username='$username ' and password='$password '" ; $result =$conn ->query ($sql ); $row =$result ->fetch_row (); var_dump ($row ); if ($result ->num_rows > 0 ) { $_SESSION ['username' ] = $username ; $_SESSION ['password' ] = $password ; echo '<script>alert("用户:' .$username . '登录成功");window.location.href="./admin.php"</script>' ; }else { echo "<script>alert('用户名或密码错误');window.location.href='./login.html'</script>" ; } ?>
admin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>后台管理</title> </head> <body> <h1>这是一个后台页面</h1> </body> </html> <?php session_start (); if (empty ($_SESSION ['username' ])){ echo '<script>alert("没有session");window.location.href="./login.html"</script>' ; }else { echo "欢迎来到yatming的后台" ; echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>' ; } ?>
这里你会发现session的设置方法和cookie差不多。只不过session是保存在服务端的。
这里我用的phpstudy,这个是我的保存目录。
注册的功能实现 register.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?php include './db/config.php' ;$username = $_POST ['username' ];$password = $_POST ['password' ];$emali = $_POST ['emali' ];$password2 = $_POST ['password2' ];$stmt = $conn ->query ("SELECT * FROM users WHERE username = '$username ' OR mail = '$emali ';" );if (!($password == $password2 )){ echo '<script>alert("两次密码输入不一致");window.location.href="./login.html"</script>' ; }elseif ($stmt ->num_rows > 0 ){ echo "邮箱或者用户名已经存在" ; }else { $sql = "INSERT INTO users(username,mail,password) values('$username ','$emali ','$password ');" ; $data =$conn ->query ($sql ); echo '<script>alert("注册成功")</script>' ; $_SESSION ['username' ] = $username ; $_SESSION ['password' ] = $password ; echo '<script>alert("用户:' .$username . '登录成功");window.location.href="./admin.php"</script>' ; } ?>
admin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>后台管理</title> </head> <body> <h1>这是一个后台页面</h1> </body> </html> <?php include './db/config.php' ; if (empty ($_SESSION ['username' ])){ echo '<script>alert("没有session");window.location.href="./login.html"</script>' ; }else { echo "欢迎来到yatming的后台" ; echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>' ; } ?>
修改已有账户的密码 1 下面这种写法有一个漏洞就是,你登录之后,可以修改任意用户的账号密码
update 表名 set 列名 = 数据 where 列名 = 条件
edit.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <html> <h1>修改页面</h1> <form action="" method="POST" > <div> <input type="text" name="username" placeholder="用户名" required> </div> <div> <input type="password" name="password" placeholder="密码" required> </div> <input type="submit" value="修改密码" > </form> <td><a href="login.html" >回到登录页面</a></td> </html> <?php include './db/config.php' ;include 'xhsession.php' ;$username =$_POST ['username' ];$password =$_POST ['password' ];if (empty ($_SESSION ['username' ]) && empty ($_SESSION ['password' ])){ echo '<script>alert("没有session");window.location.href="./login.html"</script>' ; } else { if (isset ($_POST ['username' ]) && isset ($_POST ['password' ])) { $sql = "update users set password = '$password ' where username='$username '; " ; $result =$conn ->query ($sql ); session_destroy (); echo '<script>alert("密码更新成功,请重新登录");</script>' ; } } ?>
xhsession.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php function secureSessionDestroy ( ) { $_SESSION = array (); if (ini_get ("session.use_cookies" )) { $params = session_get_cookie_params (); setcookie (session_name (), '' , time () - 42000 , $params ["path" ], $params ["domain" ], $params ["secure" ], $params ["httponly" ] ); } session_destroy (); session_regenerate_id (true ); } ?>
admin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>后台管理</title> </head> <body> <h1>欢迎来到yatming的后台</h1> </body> </html> <?php include './db/config.php' ; if (empty ($_SESSION ['username' ]) && empty ($_SESSION ['password' ])){ echo '<script>alert("没有session");window.location.href="./login.html"</script>' ; }else { echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>' ; echo "<td><a href='./edit.php'>修改密码</a></td>" ; } ?>
第二种方式实现注册,登录, 修改密码 解析include,empty函数
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 <?php include_once ("./config/db.php" );$type = $_GET ['type' ];if (empty ($_GET ['type' ])){ $type ='login' ; } switch ($type ){ case 'login' : include_once ('./template/login.html' ); break ; case 'login_api' : $username = $_POST ['username' ]; $password = $_POST ['password' ]; $sql = "select * from user where username='$username ' and password = '$password ';" ; $result =$db ->query ($sql ); if ($result ->num_rows > 0 ){ echo "<script>alert('$username 登录成功,欢迎你 ');window.location.href='./index.php?type=admin'</script>" ; $_SESSION ['username' ] = $username ; $_SESSION ['password' ] = $password ; }else { echo '<script>alert("登录失败,账号或者密码错误");window.location.href="./index.php?type=login"</script>' ; } break ; case 'reregister' : if ($_SERVER ['REQUEST_METHOD' ] === 'POST' ) { $username = trim ($_POST ['username' ] ?? '' ); $password = trim ($_POST ['password' ] ?? '' ); $password2 = trim ($_POST ['password2' ] ?? '' ); if (empty ($username ) || empty ($password )) { die ('<script>alert("用户名和密码不能为空");history.back()</script>' ); } if ($password !== $password2 ) { die ('<script>alert("两次密码输入不一致");history.back()</script>' ); } $check = $db ->prepare ("SELECT id FROM user WHERE username = ?" ); $check ->bind_param ("s" , $username ); $check ->execute (); if ($check ->get_result ()->num_rows > 0 ) { die ('<script>alert("用户名已存在");history.back()</script>' ); } $hashed_pw = password_hash ($password , PASSWORD_DEFAULT); $stmt = $db ->prepare ("INSERT INTO user (username, password) VALUES (?, ?)" ); $stmt ->bind_param ("ss" , $username , $hashed_pw ); if ($stmt ->execute () && $stmt ->affected_rows > 0 ) { $_SESSION ['username' ] = $username ; $_SESSION ['password' ] = $password ; echo '<script>alert("注册成功,请登录");location.href="./index.php?type=admin"</script>' ; exit ; } else { die ('<script>alert("注册失败,请重试");history.back()</script>' ); } } else { include_once ('./template/reergister.html' ); } break ; case 'admin' : if (empty ($_SESSION ['username' ])){ echo '<script>alert("你尚未登录到网站之中!");window.location.href="./index.php?type=login"</script>' ; }else { echo '<script>alert("登录到后台中");window.location.href="admin.php"</script>' ; } echo 'hello yatming' ; break ; default : echo 'this is default' ; break ; } ?>
admin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>后台管理</title> </head> <body> <h1>欢迎来到yatming的后台</h1> <br/> <br/> <br/> <br/> <br/> <h5><a href="login.html">退出登录</a></h5> </body> </html> <?php include './config/db.php'; if(empty($_SESSION['username']) ){ echo '<script>alert("没有session");window.location.href="./login.html"</script>'; }else{ echo '<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=1555837685&bvid=BV141421r7Hq&cid=1590430720&p=1" scrolling="no" width="100%" height="500px" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>'; //session_destroy(); //update 表名 set 列名 = 数据 where 列名 = 条件 //echo "<td><a href='./edit.php'>修改密码</a></td>"; } ?>
最终版本——PDO 1 2 3 4 5 预编译(PDO)语句的几个步骤 1、准备(Prepare): 创建一个预处理语句对象,SQL语句中的参数使用占位符(如?或命名占位符如“ :name ”)表示 2、绑定参数(Bind):将变量绑定到占位符(这一步有时在execute中直接传递参数数组代替) 3、执行(Execute):执行预处理语句,将绑定的值插入到SQL语句中并执行 4、获取结果(Fetch):根据需要获取结果
目录结构:
数据库配置文件:db.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php session_start (); $dbhost = '127.0.0.1' ; $username = 'root' ; $password = 'root' ; $dbname = 'code' ; try { $pdo = new PDO ("mysql:host=$dbhost ;dbname=$dbname ;charset=utf8" , $username , $password ); $pdo ->setAttribute (PDO::ATTR_ERRMODE , PDO::ERRMODE_EXCEPTION ); $pdo ->setAttribute (PDO::ATTR_EMULATE_PREPARES , false ); } catch (PDOException $e ) { die ('数据库连接失败: ' . $e ->getMessage ()); } ?>
函数文件:js.php
1 2 3 4 5 6 7 <?php function alert_localtion ($msg ,$url ) { echo '<script>alert("' .$msg .'");window.location.href="' .$url .'"</script>' ; die ; } ?>
修改密码前端文件:edit_pass.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!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 > <form action ="./index.php?type=edit_pass_api" method ="post" > <p > 用户名:<input type ="text" name ="username" /> </p > <p > 旧密码:<input type ="password" name ="old_password" /> </p > <p > 新密码:<input type ="password" name ="new_password" /> </p > <input type ="submit" value ="修改密码" /> </form > </body > </html >
登录前端页面:login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 <!DOCTYPE html > <html lang ="zh-CN" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 登录 - 简约苹果风</title > <link rel ="stylesheet" href ="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" > <style > * { margin : 0 ; padding : 0 ; box-sizing : border-box; font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans' , 'Helvetica Neue' , sans-serif; } body { background : linear-gradient (135deg , #f5f7fa 0% , #e4e8f0 100% ); min-height : 100vh ; display : flex; justify-content : center; align-items : center; padding : 20px ; } .container { width : 100% ; max-width : 400px ; } .apple-logo { display : block; margin : 0 auto 30px ; width : 80px ; height : 80px ; background : linear-gradient (135deg , #0071e3 , #8e44ad ); border-radius : 50% ; display : flex; justify-content : center; align-items : center; box-shadow : 0 5px 15px rgba (0 , 0 , 0 , 0.1 ); } .apple-logo i { color : white; font-size : 40px ; } .form-container { background : white; border-radius : 20px ; box-shadow : 0 10px 30px rgba (0 , 0 , 0 , 0.08 ); padding : 40px 30px ; } .form-title { font-size : 1.8rem ; font-weight : 600 ; margin-bottom : 30px ; color : #333 ; text-align : center; } .input-group { margin-bottom : 25px ; } .input-group label { display : block; margin-bottom : 8px ; font-weight : 500 ; color : #555 ; font-size : 0.95rem ; } .input-group input { width : 100% ; padding : 15px 18px ; border : 1px solid #e0e0e0 ; border-radius : 12px ; font-size : 1rem ; transition : all 0.3s ; background : #f9f9f9 ; } .input-group input :focus { outline : none; border-color : #0071e3 ; box-shadow : 0 0 0 3px rgba (0 , 113 , 227 , 0.1 ); background : white; } .input-group .error { color : #e74c3c ; font-size : 0.85rem ; margin-top : 6px ; display : none; } .input-group .has-error input { border-color : #e74c3c ; } .input-group .has-error .error { display : block; } .btn { width : 100% ; padding : 15px ; border : none; border-radius : 12px ; background : #0071e3 ; color : white; font-size : 1.1rem ; font-weight : 500 ; cursor : pointer; transition : all 0.3s ; margin-top : 15px ; box-shadow : 0 4px 10px rgba (0 , 113 , 227 , 0.2 ); } .btn :hover { background : #005bb8 ; transform : translateY (-2px ); box-shadow : 0 6px 15px rgba (0 , 113 , 227 , 0.3 ); } .switch-text { text-align : center; margin-top : 25px ; color : #666 ; font-size : 0.95rem ; } .switch-text a { color : #0071e3 ; text-decoration : none; font-weight : 500 ; } .switch-text a :hover { text-decoration : underline; } .security-info { background : #f8f9fa ; border-radius : 12px ; padding : 15px ; margin-top : 20px ; border : 1px solid #eee ; font-size : 0.85rem ; color : #666 ; text-align : center; } .security-info i { color : #0071e3 ; margin-right : 8px ; } .footer { text-align : center; margin-top : 30px ; color : #777 ; font-size : 0.9rem ; } </style > </head > <body > <div class ="container" > <div class ="apple-logo" > <i class ="fas fa-lock" > </i > </div > <div class ="form-container" > <h2 class ="form-title" > 登录您的账户</h2 > <form id ="loginForm" action ="./index.php?type=login_api" method ="post" > <div class ="input-group" > <label for ="username" > 用户名</label > <input type ="text" id ="username" name ="username" required > <div class ="error" id ="usernameError" > 输入包含不安全字符</div > </div > <div class ="input-group" > <label for ="password" > 密码</label > <input type ="password" id ="password" name ="password" required > <div class ="error" id ="passwordError" > 输入包含不安全字符</div > </div > <button type ="submit" class ="btn" > 登录</button > </form > <div class ="security-info" > <p > <i class ="fas fa-shield-alt" > </i > 安全系统检测SQL注入关键词</p > </div > <div class ="switch-text" > 还没有账户? <a href ="register.html" > 立即注册</a > </div > </div > <div class ="footer" > <p > © 2023 简约账户系统 | 苹果风格设计</p > </div > </div > <script > document .addEventListener ('DOMContentLoaded' , function ( ) { const sqlKeywords = [ 'select' , 'insert' , 'update' , 'delete' , 'drop' , 'alter' , 'create' , 'truncate' , 'union' , 'join' , 'or' , 'and' , ';' , '--' , '/*' , '*/' , 'xp_' , 'exec' ]; const loginForm = document .getElementById ('loginForm' ); const usernameInput = document .getElementById ('username' ); const passwordInput = document .getElementById ('password' ); loginForm.addEventListener ('submit' , function (e ) { usernameInput.parentElement .classList .remove ('has-error' ); passwordInput.parentElement .classList .remove ('has-error' ); let hasSqlInjection = false ; if (containsSqlKeywords (usernameInput.value )) { usernameInput.parentElement .classList .add ('has-error' ); hasSqlInjection = true ; } if (containsSqlKeywords (passwordInput.value )) { passwordInput.parentElement .classList .add ('has-error' ); hasSqlInjection = true ; } if (hasSqlInjection) { e.preventDefault (); } }); function containsSqlKeywords (value ) { const lowerValue = value.toLowerCase (); for (const keyword of sqlKeywords) { if (lowerValue.includes (keyword)) { return true ; } } return false ; } }); </script > </body > </html >
处理所有功能的php文件:index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 <?php include_once './config/db.php' ;include_once './function/js.php' ;$type = !empty ($_GET ['type' ]) ? $_GET ['type' ] : 'login' ;$No_Check_Controller_array = array ('login' ,'login_api' ,'register' ,'register_api' );if (!(in_array ($type ,$No_Check_Controller_array ))){ if (empty ($_SESSION ['username' ])){ alert_localtion ("尚未登录,正在跳转到登录页面.........." ,"./index.php?type=login" ); } } switch ($type ){ case 'login' : include_once ('./login.html' ); session_destroy (); break ; case 'login_api' : $username = $_POST ['username' ]; $password = $_POST ['password' ]; if (!empty ($username ) && !empty ($password )){ try { $sql = "SELECT * FROM blog WHERE username = :username AND password = :password" ; $stmt = $pdo ->prepare ($sql ); $stmt ->bindParam (':username' , $username , PDO::PARAM_STR ); $stmt ->bindParam (':password' , $password , PDO::PARAM_STR ); $stmt ->execute (); if ($stmt ->rowCount () > 0 ){ $row = $stmt ->fetch (PDO::FETCH_ASSOC ); if ($row ['status' ] == '0' ){ alert_localtion ("账号封禁" ,"./index.php?type=login" ); } $_SESSION ['username' ] = $username ; $_SESSION ['password' ] = $password ; alert_localtion ("Hello $username 登录成功" ,"./index.php?type=index" ); } else { alert_localtion ("登录失败,账号或者密码错误!" ,"./index.php?type=login" ); } } catch (PDOException $e ) { alert_localtion ("数据库错误: " .$e ->getMessage (),"./index.php?type=login" ); } } break ; case 'register' : include_once ('./register.html' ); break ; case 'register_api' : $username = $_POST ['username' ]; $password = $_POST ['password' ]; $password2 = $_POST ['password2' ]; if (empty ($password ) || empty ($username ) || empty ($password2 )){ echo "请填写完整" ; } else { if ($password === $password2 ){ try { $sql = "SELECT * FROM blog WHERE username = :username" ; $stmt = $pdo ->prepare ($sql ); $stmt ->bindParam (':username' , $username , PDO::PARAM_STR ); $stmt ->execute (); if ($stmt ->rowCount () > 0 ){ alert_localtion ("该用户已经存在" ,"./index.php?type=login" ); } else { $sql = "INSERT INTO blog (username, password, status) VALUES (:username, :password, '1')" ; $stmt = $pdo ->prepare ($sql ); $stmt ->bindParam (':username' , $username , PDO::PARAM_STR ); $stmt ->bindParam (':password' , $password , PDO::PARAM_STR ); $stmt ->execute (); $_SESSION ['username' ] = $username ; $_SESSION ['password' ] = $password ; alert_localtion ("注册成功,请登录" ,"./index.php?type=login" ); } } catch (PDOException $e ) { alert_localtion ("数据库错误: " .$e ->getMessage (),"./index.php?type=register" ); } } else { echo "密码不一致" ; } } break ; case 'edit_pass' : include_once ('./edit_pass.html' ); break ; case 'edit_pass_api' : $username = $_POST ['username' ]; $old_password = $_POST ['old_password' ]; $new_password = $_POST ['new_password' ]; if (empty ($old_password ) || empty ($username ) || empty ($new_password )){ echo "请填写完整" ; } else { try { $sql = "SELECT * FROM blog WHERE username = :username AND password = :old_password" ; $stmt = $pdo ->prepare ($sql ); $stmt ->bindParam (':username' , $username , PDO::PARAM_STR ); $stmt ->bindParam (':old_password' , $old_password , PDO::PARAM_STR ); $stmt ->execute (); if ($stmt ->rowCount () > 0 ){ $sql = "UPDATE blog SET password = :new_password WHERE username = :username" ; $stmt = $pdo ->prepare ($sql ); $stmt ->bindParam (':new_password' , $new_password , PDO::PARAM_STR ); $stmt ->bindParam (':username' , $username , PDO::PARAM_STR ); $stmt ->execute (); session_destroy (); alert_localtion ("密码修改成功!" ,"./index.php?type=login" ); } else { alert_localtion ("账号不存在,或者密码不对!" ,"./index.php?type=edit_pass" ); } } catch (PDOException $e ) { alert_localtion ("数据库错误: " .$e ->getMessage (),"./index.php?type=edit_pass" ); } } break ; case 'index' : echo 'hello ' .$_SESSION ['username' ].' 欢迎登录到个人中心' ; echo '<br/>' ; echo '<br/>' ; echo '<a href="./index.php?type=edit_pass">修改密码</a><br/><br/>' ; echo '<a href="http://10.20.0.198/ly/index.php">留言板</a>' ; break ; default : echo '默认页' ; break ; } ?>
注册的前端文件:register.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 <!DOCTYPE html > <html lang ="zh-CN" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 注册 - 简约苹果风</title > <link rel ="stylesheet" href ="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" > <style > * { margin : 0 ; padding : 0 ; box-sizing : border-box; font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans' , 'Helvetica Neue' , sans-serif; } body { background : linear-gradient (135deg , #f5f7fa 0% , #e4e8f0 100% ); min-height : 100vh ; display : flex; justify-content : center; align-items : center; padding : 20px ; } .container { width : 100% ; max-width : 400px ; } .apple-logo { display : block; margin : 0 auto 30px ; width : 80px ; height : 80px ; background : linear-gradient (135deg , #0071e3 , #8e44ad ); border-radius : 50% ; display : flex; justify-content : center; align-items : center; box-shadow : 0 5px 15px rgba (0 , 0 , 0 , 0.1 ); } .apple-logo i { color : white; font-size : 40px ; } .form-container { background : white; border-radius : 20px ; box-shadow : 0 10px 30px rgba (0 , 0 , 0 , 0.08 ); padding : 40px 30px ; } .form-title { font-size : 1.8rem ; font-weight : 600 ; margin-bottom : 30px ; color : #333 ; text-align : center; } .input-group { margin-bottom : 25px ; } .input-group label { display : block; margin-bottom : 8px ; font-weight : 500 ; color : #555 ; font-size : 0.95rem ; } .input-group input { width : 100% ; padding : 15px 18px ; border : 1px solid #e0e0e0 ; border-radius : 12px ; font-size : 1rem ; transition : all 0.3s ; background : #f9f9f9 ; } .input-group input :focus { outline : none; border-color : #0071e3 ; box-shadow : 0 0 0 3px rgba (0 , 113 , 227 , 0.1 ); background : white; } .input-group .error { color : #e74c3c ; font-size : 0.85rem ; margin-top : 6px ; display : none; } .input-group .has-error input { border-color : #e74c3c ; } .input-group .has-error .error { display : block; } .btn { width : 100% ; padding : 15px ; border : none; border-radius : 12px ; background : #0071e3 ; color : white; font-size : 1.1rem ; font-weight : 500 ; cursor : pointer; transition : all 0.3s ; margin-top : 15px ; box-shadow : 0 4px 10px rgba (0 , 113 , 227 , 0.2 ); } .btn :hover { background : #005bb8 ; transform : translateY (-2px ); box-shadow : 0 6px 15px rgba (0 , 113 , 227 , 0.3 ); } .switch-text { text-align : center; margin-top : 25px ; color : #666 ; font-size : 0.95rem ; } .switch-text a { color : #0071e3 ; text-decoration : none; font-weight : 500 ; } .switch-text a :hover { text-decoration : underline; } .security-info { background : #f8f9fa ; border-radius : 12px ; padding : 15px ; margin-top : 20px ; border : 1px solid #eee ; font-size : 0.85rem ; color : #666 ; text-align : center; } .security-info i { color : #0071e3 ; margin-right : 8px ; } .footer { text-align : center; margin-top : 30px ; color : #777 ; font-size : 0.9rem ; } </style > </head > <body > <div class ="container" > <div class ="apple-logo" > <i class ="fas fa-user-plus" > </i > </div > <div class ="form-container" > <h2 class ="form-title" > 创建新账户</h2 > <form id ="registerForm" action ="./index.php?type=register_api" method ="post" > <div class ="input-group" > <label for ="regUsername" > 用户名</label > <input type ="text" id ="regUsername" name ="username" required > <div class ="error" id ="regUsernameError" > 输入包含不安全字符</div > </div > <div class ="input-group" > <label for ="regPassword" > 密码</label > <input type ="password" id ="regPassword" name ="password" required > <div class ="error" id ="regPasswordError" > 输入包含不安全字符</div > </div > <div class ="input-group" > <label for ="regPassword2" > 确认密码</label > <input type ="password" id ="regPassword2" name ="password2" required > <div class ="error" id ="regPassword2Error" > 输入包含不安全字符</div > </div > <button type ="submit" class ="btn" > 注册</button > </form > <div class ="security-info" > <p > <i class ="fas fa-shield-alt" > </i > 安全系统检测SQL注入关键词</p > <p > <i class ="fas fa-exclamation-triangle" > </i > 检测关键词: select, insert, update, delete, drop, or, and, ;, -- 等</p > </div > <div class ="switch-text" > 已有账户? <a href ="login.html" > 返回登录</a > </div > </div > <div class ="footer" > <p > © 2023 简约账户系统 | 苹果风格设计</p > </div > </div > <script > document .addEventListener ('DOMContentLoaded' , function ( ) { const sqlKeywords = [ 'select' , 'insert' , 'update' , 'delete' , 'drop' , 'alter' , 'create' , 'truncate' , 'union' , 'join' , 'or' , 'and' , ';' , '--' , '/*' , '*/' , 'xp_' , 'exec' ]; const registerForm = document .getElementById ('registerForm' ); const usernameInput = document .getElementById ('regUsername' ); const passwordInput = document .getElementById ('regPassword' ); const password2Input = document .getElementById ('regPassword2' ); registerForm.addEventListener ('submit' , function (e ) { document .querySelectorAll ('.input-group' ).forEach (group => { group.classList .remove ('has-error' ); }); let hasSqlInjection = false ; const inputs = [ {element : usernameInput, errorId : 'regUsernameError' }, {element : passwordInput, errorId : 'regPasswordError' }, {element : password2Input, errorId : 'regPassword2Error' } ]; inputs.forEach (input => { if (containsSqlKeywords (input.element .value )) { input.element .parentElement .classList .add ('has-error' ); hasSqlInjection = true ; } }); if (hasSqlInjection) { e.preventDefault (); } }); function containsSqlKeywords (value ) { const lowerValue = value.toLowerCase (); for (const keyword of sqlKeywords) { if (lowerValue.includes (keyword)) { return true ; } } return false ; } }); </script > </body > </html >
留言板实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <?php $db_host = 'localhost' ;$db_user = 'root' ;$db_pass = 'root' ;$db_name = 'message_board' ;$pdo = new PDO ("mysql:host=$db_host ;dbname=$db_name ;charset=utf8mb4" , $db_user , $db_pass );$pdo ->setAttribute (PDO::ATTR_ERRMODE , PDO::ERRMODE_EXCEPTION );if (isset ($_GET ['content' ])) { $content = $_GET ['content' ]; $ua = $_SERVER ['HTTP_USER_AGENT' ] ?? 'Unknown UA' ; $stmt = $pdo ->prepare ("INSERT INTO `messages` (`content`, `ua`) VALUES (:content, :ua)" ); $stmt ->execute ([ ':content' => $content , ':ua' => $ua ]); header ('Location: ' . $_SERVER ['PHP_SELF' ]); exit ; } $stmt = $pdo ->query ("SELECT * FROM `messages` ORDER BY `create_time` DESC" );$messages = $stmt ->fetchAll (PDO::FETCH_ASSOC );?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8" > <title>留言板</title> <style> body { max-width: 600 px; margin: 0 auto; padding: 20 px; } .message { border: 1 px solid .ua-info { color: </style> </head> <body> <h1>留言板</h1> <form method="get" > <textarea name="content" rows="5" cols="50" placeholder="输入留言内容..." required></textarea><br> <button type="submit" >提交</button> </form> <h2>最新留言</h2> <?php foreach ($messages as $msg ): ?> <div class ="message "> <?php echo $msg ['content ']; ?> <div class ="ua -info ">UA : <?php echo htmlspecialchars ($msg ['ua ']); ?></div > <!-- UA 头进行了转义 --> </div > <?php endforeach ; ?> </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 个人认为的缺陷: 1、首先是只要可以注册成功就可以登录后台, 应该写一个后台管理员界面和普通用户后台,然后分开进行鉴权。 2、这里虽然进行了预编译,但是只有对登录和注册进行了预编译,没有对后台进行进一步的判断,这里也是由于第一个的原因导致所有注册的用户都可以登录到后台,只要我在注册的时候写一条xss语句注册成用户,然后在登录到后台的时候由于后台会显示登录者的用户名,没有对这个用户名做任何的过滤,所以就会执行xss。 payload:")</script><script>alert(1)// 3、这个留言板也没有对xss有任何的过滤,我是在后端部署了一个安全狗,但是安全狗只对特殊的标签进行过滤,如果我使用如下payload,依旧可以绕过安全狗。 payload: <p><input onclick="alert(2)">2</p> <button onclick="alert(1)">1</button> <a href="https://www.yatming.top" onclick="alert(document.cookie)">跳转</a> <div onclick="alert(1)">1</div> <div onmouseover="alert(1)">11231</div> <iframe onload=alert("xss");></iframe> <svg onload=alert(1)> <body onload="alert(1)"> <body onscroll=alert("xss");><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><input autofocus> <details open ontoggle="alert('xss');">
留言板删除的思路 1 删除的思路,首先你要对每个数据有一个唯一标识符号,比如说id,或者内容,但是如果选择内容的话,一个用户有多个相同的内容该怎么判断(个人理解:如果根据内容进行筛选一个人的数据,那么如果出现一个人有多条相同数据的时候可以只删除一条,或者增加时间字段,但是这样过于麻烦)。还是建议有一个唯一字段,拥有了唯一字段之后,你可以使用`a`标签创建一个链接,这个链接可以将唯一的字段在后端使用get或者post的方式进行接收,当接收到这个值的时候,执行delele语句,条件就是传输过来的唯一字段,然后执行sql语句就行了。当然这里同样要进行鉴权,因为如果不进行鉴权,所有人都可以删除任何人的数据。所以这里在sql判断的时候还要加上username字段,比如:`delete from xiaodi where username =xxx and id = xx`
原生的AJAX 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 小知识:一般的js语句都是在代码的底部引入,css文件都是在html文件的头部引入。这样可以减少网站的加载时间(并不是物理上的减少,而是感官上的) ajax和from表单提交的区别: 1、提交的方式 form表单通常是通过在html定义的cation,method和submit来进行表单提交,另外也可以通过在js中调用submit函数进行表单提交 2、页面的刷新 form提交,更新数据完成后,需要跳转到一个空白的页面,然后子在对原网页进行处理,哪怕是提交给自己本身的页面,也是需要刷新的,因此局限性很大。 ajax,$http都可以实现页面的局部刷新,整个页面不会进行刷新。 3、由谁来提交 from提交是浏览器完成的,无论是浏览器是否开启js,都可以提交表单 ajax,$http是通过js来提交请求,请求和响应均由js引擎来处理,因此不启用js的浏览器,无法完成该操作。
原生ajax创建的五个步骤
1、使用ajax发送数据的步骤
第一步:创建异步对象
1 var xhr = new XMLHttpRequest ();
第二步:设置请求行open(请求方式,请求url):
1 2 3 xhr.open ("post" ,"validate.php" );
第三步:设置请求(GET方式忽略此步骤)头:setRequestHeader()
1 2 3 xhr.setRequestHeader ('Content-Type' , 'application/json' );
第四步:定义onload
监听器
1 2 3 xhr.onload = function ( ) { }; xhr.onerror = function ( ) { }; xhr.ontimeout = function ( ) { };
第五步:发送请求体send()
完整流程示意图:
1 2 3 4 5 6 7 8 9 10 11 创建XHR对象 ↓ open()配置 ↓ (可选)设置请求头 ↓ 定义事件监听器 ←─────┐ ↓ │ send() │ │ │ └──→ 等待响应 → 触发监听器
错误风险 :若先调用send()
再设置onload
监听器,可能导致事件丢失
1 2 3 4 5 6 7 8 9 10 <script type = "text/javascript" > var xhr = new XMLHttpRequest (); xhr.open ("post" ,"vaildate.php" ); xhr.onreadystatechange = function ( ){ if (xhr.status == 200 && xhr.readyState == 4 ){ console .log (xhr.responseText ); } } xhr.send ("username=admin" +"&password=123456" ); </script>
可以在浏览器中使用F12进行调试:
这里报错是因为请求不到这个文件,这个文件不存在,然后可以在网络,如上图,这里的载荷可以看到上图按照我们写的代理写的请求体去发送了请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script type = "text/javascript" > function login ( ){ var xhr = new XMLHttpRequest (); xhr.open ("post" ,"vaildate.php" ); xhr.onreadystatechange = function ( ){ if (xhr.status == 200 && xhr.readyState == 4 ){ console .log (xhr.responseText ); } } xhr.send ("username=admin" +"&password=123456" ); } </script>
将其封装一个函数,然后在F12中调用他:
js获取数据的两种方式 1 2 3 4 5 6 7 8 9 getElementById ()方法:通过id取得HTML 元素。getElementsByName ()方法:通过name取得元素,是一个数组。还有就是 document .getElementsByName ("usernaem" )[0 ].value document .getElementsByName ("usernaem" )[0 ].innerHTML
通过name获取页面中的值:
1 document .getElementsByName ("username" )[0 ].value
通过ID获取页面中的值
1 document.getElementById("usertext" ).value
如果你要通过id
获取页面中的值,那么相对应的html中也要有这个值。
1 2 3 小知识点: js中console .log 是用来打印变量的。跟printf类似的作用。 js中使用的是 + 进行字符串连接,类似于之前的 . 连接符
那么可以获取到值之后,如何让用户点击登录的时候自动执行这个函数呢?使用onclick=""
点击事件
原生态ajax:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <script src="./jquery-3.7.1.js" ></script> <script type = "text/javascript" > function login ( ){ var username = document .getElementsByName ("username" )[0 ].value ; var password = document .getElementsByName ("password" )[0 ].value ; var post_data = "username=" + username + "&password=" + password var xhr = new XMLHttpRequest (); xhr.open ("post" ,"index.php?type=login_api" ); xhr.send (post_data); xhr.onreadystatechange = function ( ){ if (xhr.status == 200 && xhr.readyState == 4 ){ console .log (xhr.responseText ); } } } </script >
jquery_AJAX 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <script src="./jquery-3.7.1.js" ></script> <script type = "text/javascript" > function login ( ) { var username = document .getElementsByName ("username" )[0 ].value ; var password = document .getElementsByName ("password" )[0 ].value ; data = { "username" :username, "password" :password } $.ajax ( { url : "./index.php?type=login_api" , type : 'POST' , data : data, async : true , success :(function (result ){ console .log (result) }) } ) } </script >
这里就可以从上图看到点击按钮之后触发login函数。
总结:jquery_ajax的方式更加好用。且代码更容易看懂。
1 小知识点:如果你想要form表单停止跳转的话,那么只需要在form表单中加入:action="javascript:;"
上面这里使用ajax是不能使页面进行跳转的,现在实现这个登录成功就可以跳转的功能(这里只对原理进行研究,不提供源代码了)
json数据格式 1. 可读性强
结构清晰 :使用键值对(key: value
)和层级缩进,直观展示数据关系,适合人类阅读和调试。
2. 跨语言兼容
广泛支持 :几乎所有编程语言(如 Python、Java、C#、PHP 等)都有内置或第三方库解析和生成 JSON。
数据交换通用 :不同系统或服务间传递数据时,JSON 作为“中间语言”减少兼容问题。
3. 轻量高效
体积小 :相比 XML,JSON 没有冗余标签,节省带宽,加快网络传输速度。
解析速度快 :语法简单,序列化(生成)和反序列化(解析)效率高。
这里是一条跳转语句,就是登录之后跳转的地址,但是这里没有明确写出具体的链接地址(他是由后端传输到前端,而不是直接写到前端的),如果这里明确写出来的话对于攻击者来说就可以直接获取到后台的地址。
MVC形式架构 MVC(Model-View-Controller) 是一种经典的软件设计架构模式 ,用于将应用程序的逻辑、数据和用户界面分离,提升代码的可维护性、可扩展性和复用性。它将应用程序划分为三个核心组件:
1. Model(模型)
职责: 处理数据和业务逻辑。
包含:
数据结构(如数据库表、对象)。
数据访问操作(如增删改查)。
业务规则和验证逻辑(如用户注册验证、订单计算)。
特点: 不关心用户界面如何显示,也不直接处理用户输入。只负责管理应用程序的核心数据和功能。
2. View(视图)
职责: 展示数据(给用户看)和接收用户输入。
包含: 用户界面元素(UI),如网页(HTML/CSS)、图形界面(GUI)、命令行输出等。
特点:
从Model 获取需要显示的数据(通常通过Controller)。
将用户的操作(点击按钮、输入文本)传递给Controller 。
通常不包含复杂的业务逻辑,主要负责展示 。
3. Controller(控制器)
职责: 作为Model 和View 之间的协调者 ,处理用户输入并更新模型和视图。
包含:
接收来自View 的用户输入(如HTTP请求、点击事件)。
根据输入调用相应的Model 进行数据处理或状态变更(如保存数据、计算结果)。
选择并更新合适的View 来响应用户(如渲染新的页面、刷新部分界面)。
特点: 是应用程序的“交通警察”,决定用户请求的流向。
小迪篇 这个是目录结构:
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <!-- 配置文件 --> <!-- <scripttype="text/javascript"src="./ueditor/ueditor.config.js"></script> --> <!-- 编辑器源码文件 --> <!-- <scripttype="text/javascript"src="./ueditor/ueditor.all.js"></script> --> <script src="./ueditor/ueditor.config.js">/*引入配置文件*/</script> <script src="./ueditor/ueditor.all.js">/*引入源码文件*/</script> </head> <body> <script src="/ueditor/ueditor.config.js">/*引入配置文件*/</script> <script src="/ueditor/ueditor.all.js">/*引入源码文件*/</script> <form action="" method="post"> <br/> <input type="text" placeholder="用户名" name="username" /> <br/> <br/> <textarea id="content" placeholder="请输入留言..." name="content" rows="6" cols="50" ></textarea> <br/> <br/> <script type="text/javascript"> UE.getEditor("content"); //实例化编辑器传参,id为将要被替换的容器。 </script> <button type="submit">提交</button> </form> <br/><br/><br/> <a href="./login.html">登录后台</a> </body> </html> <?php include "./config/db.php"; function getClientIP() { $ip = ''; if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; // 共享互联网IP } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; // 代理服务器转发链 } elseif (!empty($_SERVER['REMOTE_ADDR'])) { $ip = $_SERVER['REMOTE_ADDR']; // 直接连接IP } // 处理多个IP的情况(取第一个) if (strpos($ip, ',') !== false) { $ipList = explode(',', $ip); $ip = trim($ipList[0]); } return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : 'unknown'; } function add_liuyan($pdo) { $username = $_POST['username']; $content = $_POST['content']; $ip_addr = getClientIP(); $ua_gent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; //插入留言 if(!empty($content) && !empty($username) ){ $sql = "INSERT INTO xiaodi (username, content, ip_addr,ua_gent) VALUES (:username, :content, :ip_addr,:ua_gent)"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':username', $username, PDO::PARAM_STR); $stmt->bindParam(':content', $content, PDO::PARAM_STR); $stmt->bindParam(':ip_addr', $ip_addr, PDO::PARAM_STR); $stmt->bindParam(':ua_gent', $ua_gent, PDO::PARAM_STR); $stmt->execute(); }else{ echo "<script>alert('用户或者内容未输入')</script>"; } } function show_liuyan($pdo) { //查询留言 $sql = "SELECT * FROM xiaodi;"; $stmt = $pdo->prepare($sql); $stmt->execute(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { echo "<hr/>"; echo "<br/>"; echo "<br/>"; echo "用户: " . $row['username'] . "<br>"; echo "内容: " . $row['content'] . "<br>"; echo "ip地址: " . $row['ip_addr'] . "<br>"; echo "UA头: " . $row['ua_gent'] . "<br>"; } } show_liuyan($pdo); add_liuyan($pdo); /* 预编译(PDO)语句的几个步骤 1、准备(Prepare): 创建一个预处理语句对象,SQL语句中的参数使用占位符(如?或命名占位符如“ :name ”)表示 2、绑定参数(Bind):将变量绑定到占位符(这一步有时在execute中直接传递参数数组代替) 3、执行(Execute):执行预处理语句,将绑定的值插入到SQL语句中并执行 4、获取结果(Fetch):根据需要获取结果 */ ?>
admin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php include "../config/db.php" ;include "../login.php" ;if (empty ($_SESSION ['username' ])){ echo '你的权限不够!' ; die ; } $del = $_GET ['del' ];$sql = "delete from xiaodi where content = '$del ';" ;$stmt = $pdo ->prepare ($sql );$stmt ->execute ();$sql = "SELECT * FROM xiaodi;" ;$stmt = $pdo ->prepare ($sql );$stmt ->execute ();while ($row = $stmt ->fetch (PDO::FETCH_ASSOC )) { echo "<hr/>" ; echo "<br/>" ; echo "<br/>" ; echo "用户: " . $row ['username' ] . "<br>" ; echo "内容: " . $row ['content' ] . "<br>" ; echo "ip地址: " . $row ['ip_addr' ] . "<br>" ; echo "UA头: " . $row ['ua_gent' ] . "<br>" ; echo "<a href='admin.php?del=" . $row ['content' ] . "'>删除</a>" ; } ?>
login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 <!DOCTYPE html > <html lang ="zh-CN" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 用户登录</title > <style > body { font-family : Arial, sans-serif; background-color : #f0f2f5 ; display : flex; justify-content : center; align-items : center; height : 100vh ; margin : 0 ; } .login-container { background-color : white; padding : 2rem ; border-radius : 8px ; box-shadow : 0 2px 10px rgba (0 ,0 ,0 ,0.1 ); width : 320px ; } h2 { color : #1877f2 ; text-align : center; margin-bottom : 1.5rem ; } .form-group { margin-bottom : 1rem ; } input { width : 100% ; padding : 12px ; border : 1px solid #dddfe2 ; border-radius : 6px ; margin-top : 0.5rem ; } button { background-color : #1877f2 ; color : white; width : 100% ; padding : 12px ; border : none; border-radius : 6px ; font-weight : bold; cursor : pointer; } button :hover { background-color : #166fe5 ; } .links { text-align : center; margin-top : 1rem ; } a { color : #1877f2 ; text-decoration : none; } </style > </head > <body > <div class ="login-container" > <h2 > 用户登录</h2 > <form id ="loginForm" action ="./login.php" method ="post" > <div class ="form-group" > <input type ="text" placeholder ="用户名" name ="username" /> </div > <div class ="form-group" > <input type ="password" placeholder ="密码" name ="password" /> </div > <button type ="submit" > 登录</button > </form > <div class ="links" > <a href ="./index.php?type=register" > 注册新账号</a > </div > </div > </body > </html >
db.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php session_start (); $dbhost = '127.0.0.1' ; $username = 'root' ; $password = 'root' ; $dbname = 'xiaodi' ; try { $pdo = new PDO ("mysql:host=$dbhost ;dbname=$dbname ;charset=utf8" , $username , $password ); $pdo ->setAttribute (PDO::ATTR_ERRMODE , PDO::ERRMODE_EXCEPTION ); $pdo ->setAttribute (PDO::ATTR_EMULATE_PREPARES , false ); } catch (PDOException $e ) { die ('数据库连接失败: ' . $e ->getMessage ()); } ?>
login.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php include "./config/db.php" ;include "./js.php" ;$username =$_POST ['username' ];$password =$_POST ['password' ];if (!empty ($username ) && !empty ($password )){ try { $sql = "SELECT * FROM xiaodi WHERE username = :username AND password = :password" ; $stmt = $pdo ->prepare ($sql ); $stmt ->bindParam (':username' , $username , PDO::PARAM_STR ); $stmt ->bindParam (':password' , $password , PDO::PARAM_STR ); $stmt ->execute (); if ($stmt ->rowCount () > 0 ){ $_SESSION ['username' ] = $username ; $_SESSION ['password' ] = $password ; alert_localtion ("Hello $username 登录成功" ,"./admin/admin.php" ); } else { alert_localtion ("登录失败,账号或者密码错误!" ,"./index.php" ); } } catch (PDOException $e ) { alert_localtion ("数据库错误: " .$e ->getMessage (),"./index.php" ); } } ?>
个人认为的缺陷
1 2 3 4 1、第一个就是只有admin用户可以删除数据(懒的在一步一步的写单独的注册页面和对每个用户进行判断了) 2、就是下面我提到的,我这里对唯一字段的判断就是内容,当一个用户有多条相同内容的时候,显然这样判断是有问题的。 3、安全问题:没有对用户的输入进行验证,可以实现xss攻击。同时如果实现了单个用户删除自身的留言,但是过滤不严谨,就会导致越权。 4、如果要增加功能的话,无非就是注册,修改密码,以及留言板本身的更新,修改。删除功能我已经实现。
界面:
上面已经使用session了,如果使用cookie也是同理,下面写入token的验证方式
token验证 token.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 <?php session_start ();$_SESSION ['token' ] = bin2hex (random_bytes (16 ));?> <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <title>后台登录</title> <style> body { background-color: } .login { width: 400 px; margin: 100 px auto; background-color: border-radius: 5 px; box-shadow: 0 0 10 px rgba (0 ,0 ,0 ,0.3 ); padding: 30 px; } .login h2 { text-align: center; font-size: 2 em; margin-bottom: 30 px; } .login label { display: block; margin-bottom: 20 px; font-size: 1.2 em; } .login input[type="text" ], .login input[type="password" ] { width: 100 %; padding: 10 px; border: 1 px solid border-radius: 5 px; font-size: 1.2 em; margin-bottom: 20 px; } .login input[type="submit" ] { background-color: color: border: none; padding: 10 px 20 px; border-radius: 5 px; font-size: 1.2 em; cursor: pointer; } .login input[type="submit" ]:hover { background-color: } </style> </head> <body> <div class ="login "> <h2 >后台登录</h2 > <form action ="token_check .php " method ="post "> <input type ="hidden " name ="token " value ="<?php echo $_SESSION ['token '] ; ?>"> <label for ="username ">用户名:</label > <input type ="text " name ="username " id ="username " required > <label for ="password ">密码:</label > <input type ="password " name ="password " id ="password " required > <input type ="submit " value ="登录"> </form > </div > </body > </html >
token_check.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php session_start ();$token = $_POST ['token' ] ?? '' ;if ($token !== $_SESSION ['token' ]) { header ('HTTP/1.1 403 Forbidden' ); $_SESSION ['token' ] = bin2hex (random_bytes (16 )); echo 'Access denied' ; exit ; }else { $_SESSION ['token' ] = bin2hex (random_bytes (16 )); if ($_POST ['username' ]=='admin' && $_POST ['password' ]=='123456' ){ echo '登录成功!' ; echo '你是管理员可以访问文件管理页面!' ; }else { echo '登录失败!' ; } }
代码解析:
1 首先token.php就是在php的表单中输出了$_SESSION ['token' ] 使其name可以获取到这个值,然后就可以在后面的token_check.php文件中对他进行赋值。这样操作的话,if 判断中的else 条件是无论如何都会成立的。也就是说每次提交数据都会进行进行账号和密码的判断,然后重新获取一次token值,如果你使用暴力破解,那么就会出现if 中的第一个条件不满足。从而触发Access denied。除非你的第一次暴力破解的第一个数据包就成功了。
文件上传 文件上传的前端页面:upload.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 文件上传页面</title > <style > body { font-family : Arial, sans-serif; background-color : #f2f2f2 ; padding : 20px ; } h1 { text-align : center; margin-top : 50px ; } form { background-color : #fff ; border-radius : 10px ; padding : 20px ; margin-top : 30px ; max-width : 600px ; margin : 0 auto; } input [type="file" ] { margin-top : 20px ; margin-bottom : 20px ; } button { background-color : #4CAF50 ; color : #fff ; padding : 10px 20px ; border : none; border-radius : 5px ; cursor : pointer; } button :hover { background-color : #3e8e41 ; } </style > </head > <body > <h1 > 文件上传</h1 > <form action ="./upload.php" method ="POST" enctype ="multipart/form-data" > <label for ="file" > 选择文件:</label > <br > <input type ="file" id ="file" name ="f" > <br > <button type ="submit" > 上传文件</button > </form > </body > </html >
这里文件上传就不得不提$_FILES
这个全局变量。他有就几个核心的参数,如下:
参数
描述
示例值
name
上传文件的原始文件名(含扩展名)
"my_photo.jpg"
type
文件的 MIME 类型(由浏览器提供,不可靠)
"image/jpeg"
tmp_name
文件在服务器上的临时存储路径
"/tmp/php/php7h4j1o"
error
上传状态码(整数,0 表示成功)
0
(UPLOAD_ERR_OK)
size
文件大小(字节)
102400
(100KB)
简单运行输出一下,各种信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $name =$_FILES ['file' ]['name' ];$type =$_FILES ['file' ]['type' ];$size =$_FILES ['file' ]['size' ];$tmp_name =$_FILES ['file' ]['tmp_name' ];$error =$_FILES ['file' ]['error' ];echo $name ."<br>" ;echo $type ."<br>" ;echo $size ."<br>" ;echo $tmp_name ."<br>" ;echo $error ."<br>" ;if (move_uploaded_file ($tmp_name ,'upload/' .$name )){ echo "文件上传成功!" ; } ?>
这里虽然显示了临时tmp的目录,但是当你去这个目录找这个文件的时候发现已经不再,而是在当前的upload目录里面。这里我上传的是一个txt的文本文档,所以这里mime的类型是:text/pain
,下面的那个0表示没有错误信息
黑名单上传 这里如果上传了两个同名的文件,会将原先存在的文件进行一个替换的操作,解决办法就是对每个上传的文件使用时间戳重命名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php $name =$_FILES ['file' ]['name' ];$type =$_FILES ['file' ]['type' ];$size =$_FILES ['file' ]['size' ];$tmp_name =$_FILES ['file' ]['tmp_name' ];$error =$_FILES ['file' ]['error' ];$black_ext =array ('php' ,'asp' ,'jsp' ,'aspx' );$fenge = explode ('.' ,$name );$exts = end ($fenge );if (in_array ($exts ,$black_ext )){ echo '非法后缀文件' .$exts ; }else { move_uploaded_file ($tmp_name ,'upload/' .$name ); echo '<script>alert("上传成功")</script>' ; } ?>
上传php文件:
上传txt文件:
注意:如果对方使用的黑名单验证,然后在没有对php5这样的后缀进行验证,但是你访问这个文件的时候,是触发的下载,而不是解析,那么这种情况是根据你的环境(一般来说需要修改apache的配置文件)来说的,并不是你上传的问题。
白名单上传 1 2 3 4 5 6 7 8 9 10 $allow_ext =array ('png' ,'jpg' ,'gif' ,'jpeg' );$fenge = explode ('.' ,$name );$exts = end ($fenge );if (!in_array ($exts ,$allow_ext )){ echo '非法后缀文件' .$exts ; }else { move_uploaded_file ($tmp_name ,'upload/' .$name ); echo '<script>alert("上传成功")</script>' ; }
这里跟黑名单的区别就是在in_array
这里进行一个取反的操作。然后这个名单里面填写可以上传的后缀就可以了。
MIME类型上传控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?php $name =$_FILES ['file' ]['name' ];$type =$_FILES ['file' ]['type' ];$size =$_FILES ['file' ]['size' ];$tmp_name =$_FILES ['file' ]['tmp_name' ];$error =$_FILES ['file' ]['error' ];MIME文件类型过滤 $allow_type =array ('image/png' ,'image/jpg' ,'image/jpeg' ,'image/gif' );if (!in_array ($type ,$allow_type )){ echo '非法后缀文件' ; }else { move_uploaded_file ($tmp_name ,'upload/' .$name ); echo '<script>alert("上传成功")</script>' ; } ?>
常见 MIME 类型示例
类型
典型扩展名
用途说明
text/plain
.txt
纯文本文件
text/html
.html, .htm
HTML 网页文件
text/css
.css
样式表文件
image/jpeg
.jpg, .jpeg
JPEG 图像
image/png
.png
PNG 透明背景图像
image/gif
.gif
GIF 动图
application/json
.json
JSON 数据格式
application/pdf
.pdf
PDF 文档
application/zip
.zip
压缩文件
audio/mpeg
.mp3
MP3 音频
video/mp4
.mp4
MP4 视频
MIME类型就是上图数据包中的红框标识
文件管理 有些时候如果有目录穿越的话,有的可以访问有的不可以访问,这个是根据环境来判断的,有的中间件可以让你的访问局限在网站的根目录,linux上也可以通过selinux的安全上下文来限制访问的目录。
小迪的源码有问题,比如说我点击下级目录,这样在去看的时候全部都是文件了,本身存在的是文件夹,访问的时候都变成文件了,访问不了。
了解源码中的函数
__DIR__
PHP 魔术常量,表示当前脚本所在的绝对路径 (例如:/var/www/html/your_folder
)
realpath()
将路径转换为标准化的绝对路径
移除多余的../
或./
解析符号链接
检查路径是否存在 (例如:/var/www/html/your_folder/../other
→ /var/www/html/other
)
最终结果 BASE_DIR
被定义为当前 PHP 文件所在目录的完整绝对路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 <?php define ('BASE_DIR' , realpath (__DIR__ ));$dir = isset ($_GET ['dir' ]) ? $_GET ['dir' ] : '' ;$current_path = realpath (BASE_DIR . DIRECTORY_SEPARATOR . $dir );if ($current_path === false || strpos ($current_path , BASE_DIR) !== 0 ) { $current_path = BASE_DIR; } $parent_dir = dirname (str_replace (BASE_DIR, '' , $current_path ));?> <!DOCTYPE html> <html> <head> <title>文件浏览器</title> <style> body { font-family: Arial, sans-serif; margin: 20 px; } .file-item, .folder-item { padding: 5 px; } .folder-item { font-weight: bold; } a { text-decoration: none; color: a:hover { text-decoration: underline; } .current-path { background: </style> </head> <body> <div class ="current -path "> 当前路径: <?= htmlspecialchars (str_replace (BASE_DIR , '/', $current_path )) ?: '/' ?> <!-- 这里的意思就是将根目录替换为/,如果还是上面的config /data 那么就是得到 /config /data --> </div > <?php if ($current_path !== BASE_DIR ): ?> <div class ="folder -item "> <a href ="?dir =<?= urlencode ($parent_dir ) ?>">[返回上级目录]</a > <!-- 返回上一级目录就调用前面用到的函数,然后进行url 的编码,因为路径中可能会有中文 --> </div > <?php endif ; ?> <!-- 结束语句 --> <?php // 读取目录内容 $items = scandir ($current_path ); //scandir () 函数返回一个数组,其中包含指定路径中的文件和目录。 //$current_path 是安全的根目录然后拼接你输入的那个文件夹或者文件的名字 if ($items === false ) { die ('无法读取目录内容' ); } foreach ($items as $item ) { if ($item === '.' || $item === '..' ) continue ; $full_path = $current_path . DIRECTORY_SEPARATOR . $item ; $relative_path = str_replace (BASE_DIR . DIRECTORY_SEPARATOR, '' , $full_path ); if (is_dir ($full_path )) { echo '<div class="folder-item">' ; echo '<a href="?dir=' . urlencode ($relative_path ) . '">📁 ' . htmlspecialchars ($item ) . '</a>' ; echo '</div>' ; } else { echo '<div class="file-item">' ; echo '📄 ' . htmlspecialchars ($item ); echo '</div>' ; } } ?> </body> </html>
代码分块解释1 1 2 3 if ($current_path === false || strpos($current_path, BASE_DIR) !== 0) { $current_path = BASE_DIR; }
这段代码的作用,用于防止用户访问超出允许范围的目录。
1 2 3 if ($current_path === false || strpos($current_path, BASE_DIR) !== 0) { $current_path = BASE_DIR; }
1. 条件分解 - 双重安全验证 第一部分:$current_path === false
含义 :检查路径解析是否失败
为什么需要 :
1 $current_path = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $dir);
realpath()
函数在路径不存在或无效时会返回 false
例如用户提交了无效路径:?dir=non_existent_folder
作用 :确保我们处理的路径是真实存在的物理路径
第二部分:strpos($current_path, BASE_DIR) !== 0
含义 :检查请求路径是否在允许的安全范围内
详细解析 :
strpos($a, $b)
:查找 $b
在 $a
中的位置(这里的意思是如果匹配到了一定是0(是从0,1,2这样开始算的),因为路径是绝对路径,如果输入的绝对路径可以匹配到说明没有问题)
!== 0
:严格检查结果不是 0(即不在开头)
相当于问:”$current_path
是否不以 BASE_DIR
开头?”
1 相当于一个路径:/var/www/html这个是我的BASE_DIR路径,如果你输入的值在我这里找不到,说明不在允许的安全范围内。
安全意义 :
1 2 3 4 5 6 7 8 // 合法路径示例: BASE_DIR = "/var/www/html" $current_path = "/var/www/html/documents" // 位置0 → 允许 // 非法路径示例: BASE_DIR = "/var/www/html" $current_path = "/etc/passwd" // 位置false → 阻止 $current_path = "/var/www" // 位置0但路径更短 → 阻止
2. 逻辑关系:||
(OR 操作符)
任一条件为 true 时触发重置:
路径无效(=== false
)
路径不在安全范围内(!== 0
)
相当于:”如果路径无效或越权,则重置”
3. 重置操作:$current_path = BASE_DIR
作用 :当检测到不安全路径时,强制将当前路径重置为安全根目录
安全意义 :
阻止目录遍历攻击(如 ?dir=../../etc
)
防止访问系统敏感文件
确保用户只能在沙箱内操作
情景1:正常访问子目录
1 2 3 4 5 6 7 8 9 10 11 用户请求: ?dir=documents ↓ 拼接路径: /var/www/html/documents ↓ realpath() 返回: /var/www/html/documents (有效路径) ↓ 检查: 1. 不是false → 通过 2. strpos("/var/www/html/documents", "/var/www/html") → 返回0 → 通过 ↓ 显示 documents 目录内容
情景2:恶意路径遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 用户请求: ?dir=../../../etc/passwd ↓ 拼接路径: /var/www/html/../../../etc/passwd ↓ realpath() 返回: /etc/passwd (有效但危险!) ↓ 检查: 1. 不是false → 通过 2. strpos("/etc/passwd", "/var/www/html") → 返回false → 触发重置 ↓ 重置为: /var/www/html ↓ 显示安全目录内容
情景3:无效路径
1 2 3 4 5 6 7 8 9 10 11 12 用户请求: ?dir=invalid_folder ↓ 拼接路径: /var/www/html/invalid_folder ↓ realpath() 返回: false (目录不存在) ↓ 检查: 1. === false → 触发重置 ↓ 重置为: /var/www/html ↓ 显示安全目录内容
代码分开解释2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?php $items = scandir ($current_path ); if ($items === false ) { die ('无法读取目录内容' ); } foreach ($items as $item ) { if ($item === '.' || $item === '..' ) continue ; $full_path = $current_path . DIRECTORY_SEPARATOR . $item ; $relative_path = str_replace (BASE_DIR . DIRECTORY_SEPARATOR, '' , $full_path ); if (is_dir ($full_path )) { echo '<div class="folder-item">' ; echo '<a href="?dir=' . urlencode ($relative_path ) . '">📁 ' . htmlspecialchars ($item ) . '</a>' ; echo '</div>' ; } else { echo '<div class="file-item">' ; echo '📄 ' . htmlspecialchars ($item ); echo '</div>' ; } }
这块代码负责读取并显示当前目录的内容 ,并实现文件夹的导航功能。下面我将逐行详细解析:
1 2 3 4 5 6 <?php // 读取目录内容 $items = scandir($current_path); if ($items === false) { die('无法读取目录内容'); }
1. 读取目录内容
scandir($current_path)
:
PHP 内置函数,扫描指定目录
返回包含文件和子目录名称的数组
数组包含特殊条目:.
(当前目录)和..
(上级目录)
错误处理 :
1 2 foreach ($items as $item) { if ($item === '.' || $item === '..') continue;
2. 过滤特殊目录项
跳过.
和..
:
避免显示当前目录和上级目录链接
因为已经单独实现了”返回上级目录”功能
保持界面简洁
1 2 $full_path = $current_path . DIRECTORY_SEPARATOR . $item; $relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path);
3. 路径处理
$full_path
:
构建文件/目录的完整物理路径
使用DIRECTORY_SEPARATOR
确保跨平台兼容性(Windows用\
,Linux用/
)
$relative_path
:
计算相对于安全根目录的路径
用于生成URL参数
示例:
1 2 3 BASE_DIR = "/var/www/html" $full_path = "/var/www/html/images/cats.jpg" $relative_path = "images/cats.jpg"
1 2 3 4 5 if (is_dir($full_path)) { // 显示可点击的文件夹 echo '<div class="folder-item">'; echo '<a href="?dir=' . urlencode($relative_path) . '">📁 ' . htmlspecialchars($item) . '</a>'; echo '</div>';
4. 文件夹处理
is_dir()
检测 :判断是否为目录
生成可点击链接 :
使用文件夹图标📁
href="?dir=..."
:点击后URL参数更新为相对路径
安全措施 :
urlencode()
:确保路径中的特殊字符正确传输
htmlspecialchars()
:防止XSS攻击,安全显示文件名
CSS样式 :folder-item
类用于样式控制
1 2 3 4 5 6 7 8 } else { // 显示文件 echo '<div class="file-item">'; echo '📄 ' . htmlspecialchars($item); echo '</div>'; } } ?>
5. 文件处理
非目录则视为文件
直接显示 :
安全措施 :
htmlspecialchars()
:防止恶意文件名执行脚本
完整工作流程示例: 假设目录结构:
1 2 3 4 5 6 7 /var/www/html (BASE_DIR) ├── documents/ │ └── report.pdf ├── images/ │ ├── cat.jpg │ └── dog.png └── index.php
首次访问 :
显示:
1 2 3 [文件夹] documents → 链接到 ?dir=documents [文件夹] images → 链接到 ?dir=images [文件] index.php
点击”images” :
关键安全特性:
路径隔离 :
始终基于$current_path
(已验证在安全范围内)
使用相对路径生成链接
输出过滤 :
文件名双重安全处理:
1 2 urlencode() // URL安全 htmlspecialchars() // HTML安全
目录遍历防护 :
仅允许访问BASE_DIR及其子目录
自动过滤.
和..
错误处理 :
文件包含 1 2 3 4 include() 在错误发生后脚本继续执行 require() 在错误发生后脚本停止执行 include_once() 如果已经包含,则不再执行 require_once() 如果已经包含,则不再执行
正常的写法都是:
1 2 include "1.php" ;这样的意思是包含了1 .php这个文件
但是如果这里是一个可以接收变量的传参
1 2 3 4 5 6 include $_GET ['page' ];这样写的话,那么如果我可以控制这个变量,那么就可以让他包含任意文件。 当然上面那种写法并不代表完全安全,因为如果1 .php还包含了其他文件,如果1 .php中的包含是通过变量进行接收的值,那么同样会出现任意文件包含。 文件包含可以配合文件上传进行一个组合拳,因为文件上传一般限制的很死,那么这个时候如果有文件包含你可以上传要给图片马,然后通过文件包含去包含这个文件,被包含的文件会被当做当前脚本语言去执行。也可以是txt或者其他格式。
文件管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 <?php // 安全设置:限制访问的根目录(当前目录) define('BASE_DIR', realpath(__DIR__)); define('ALLOWED_EDIT_EXTENSIONS', ['txt', 'php', 'html', 'css', 'js', 'json', 'xml', 'md', 'log', 'ini', 'conf', 'env', 'htaccess']); //定义了一个允许编辑的文件扩展名列表。这个列表用于限制用户只能编辑特定类型的文件(通常是文本文件),以防止用户编辑二进制文件(如图片、可执行文件等)可能导致的错误或安全问题。 // 获取要浏览的目录(进行安全过滤) $dir = isset($_GET['dir']) ? $_GET['dir'] : ''; $current_path = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $dir); // 验证路径是否在允许范围内 if ($current_path === false || strpos($current_path, BASE_DIR) !== 0) { $current_path = BASE_DIR; } // 处理返回上一级的链接 $parent_dir = dirname(str_replace(BASE_DIR, '', $current_path)); // 处理文件操作 $message = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 文件编辑保存 if (isset($_POST['save_file'])) { //edit_file:根目录/绝对路径/文件名 $edit_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_POST['file_path']); // 验证文件路径 if ($edit_file && strpos($edit_file, BASE_DIR) === 0) { $content = $_POST['file_content']; if (file_put_contents($edit_file, $content)) { //file_put_contents() 函数把一个字符串写入文件中 $message = '<div class="success">文件保存成功!</div>'; } else { $message = '<div class="error">文件保存失败!</div>'; } } else { $message = '<div class="error">无效的文件路径!</div>'; } } } else if (isset($_GET['action'])) { // 文件删除 if ($_GET['action'] === 'delete') { $target = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']); // 验证路径 if ($target && strpos($target, BASE_DIR) === 0) { if (is_dir($target)) { // 删除目录(递归删除) $deleted = deleteDirectory($target); $message = $deleted ? '<div class="success">目录删除成功!</div>' : '<div class="error">目录删除失败!</div>'; } else { // 删除文件 if (unlink($target)) { $message = '<div class="success">文件删除成功!</div>'; } else { $message = '<div class="error">文件删除失败!</div>'; } } } else { $message = '<div class="error">无效的文件路径!</div>'; } } // 文件下载 else if ($_GET['action'] === 'download') { $download_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']); // 验证文件路径 if ($download_file && strpos($download_file, BASE_DIR) === 0 && is_file($download_file)) { header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . basename($download_file) . '"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($download_file)); flush(); readfile($download_file); exit; } else { $message = '<div class="error">文件下载失败!无效的文件路径。</div>'; } } } // 递归删除目录函数 function deleteDirectory($dir) { if (!file_exists($dir)) { //file_exists() 函数检查文件或目录是否存在。 return true; } if (!is_dir($dir)) { return unlink($dir); // 如果这里不是文件夹,就删除文件 } foreach (scandir($dir) as $item) { if ($item == '.' || $item == '..') { continue; } if (!deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) { return false; } } return rmdir($dir); } // 检查文件是否可编辑 function isEditable($file) { $ext = pathinfo($file, PATHINFO_EXTENSION);//返回文件名的后缀 /* pathinfo函数以数组的形式返回关于文件路径的信息。 返回的数组元素如下: [dirname]: 目录路径 [basename]: 文件名 [extension]: 文件后缀名 [filename]: 不包含后缀的文件名 */ return in_array(strtolower($ext), ALLOWED_EDIT_EXTENSIONS); //判断这个文件的后缀是否在最开始定义的数组常量里面 } ?> <!DOCTYPE html> <html> <head> <title>增强版文件浏览器</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f5f7fa; color: #333; } .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.1); overflow: hidden; } header { background: linear-gradient(135deg, #4b6cb7, #182848); color: white; padding: 20px; text-align: center; } h1 { margin: 0; font-size: 28px; } .operations { background: #f0f5ff; padding: 15px; display: flex; justify-content: space-between; border-bottom: 1px solid #d0d8e8; } .btn { background: #4b6cb7; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; text-decoration: none; display: inline-block; } .btn:hover { background: #3a5795; } .btn-danger { background: #e74c3c; } .btn-danger:hover { background: #c0392b; } .btn-success { background: #2ecc71; } .btn-success:hover { background: #27ae60; } .message-container { padding: 10px 20px; } .success { background: #d4edda; color: #155724; padding: 10px; border-radius: 4px; margin: 10px 0; } .error { background: #f8d7da; color: #721c24; padding: 10px; border-radius: 4px; margin: 10px 0; } .current-path { background: #e8f0fe; padding: 15px 20px; font-size: 16px; border-bottom: 1px solid #d0d8e8; } .current-path span { font-weight: bold; color: #4b6cb7; } .file-list { padding: 0; margin: 0; list-style: none; } .list-header { display: grid; grid-template-columns: 40px 2fr 1fr 150px 200px; padding: 12px 20px; background: #f8f9fa; font-weight: bold; border-bottom: 1px solid #eee; } .file-item, .folder-item { display: grid; grid-template-columns: 40px 2fr 1fr 150px 200px; padding: 12px 20px; border-bottom: 1px solid #f0f0f0; align-items: center; transition: background 0.2s; } .file-item:hover, .folder-item:hover { background-color: #f9fbfd; } .folder-item { font-weight: 600; background-color: #f8fafd; } .item-icon { font-size: 20px; text-align: center; } .item-name { font-weight: 500; } .item-actions { display: flex; gap: 8px; } .action-icon { margin-right: 5px; } .editor-container { padding: 20px; } .editor-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } textarea { width: 100%; height: 400px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; font-family: monospace; font-size: 14px; line-height: 1.5; } .editor-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px; } .breadcrumb { display: flex; align-items: center; padding: 0 20px 15px; border-bottom: 1px solid #eee; } .breadcrumb a { color: #4b6cb7; text-decoration: none; } .breadcrumb a:hover { text-decoration: underline; } .breadcrumb span { margin: 0 8px; color: #999; } </style> </head> <body> <div class="container"> <header> <h1>文件浏览器</h1> <p>支持浏览、下载、删除、读取和编辑文件</p> </header> <div class="operations"> <div> <a href="?dir=<?= urlencode($parent_dir) ?>" class="btn"> <span class="action-icon">⬆️</span> 返回上级目录 </a> </div> <div> <a href="#" class="btn" onclick="alert('上传功能需要后端支持,请参考文档进行配置')"> <span class="action-icon">📤</span> 上传文件 </a> </div> </div> <?php if ($message): ?> <div class="message-container"> <?= $message ?> </div> <?php endif; ?> <div class="current-path"> 当前路径: <span><?= htmlspecialchars(str_replace(BASE_DIR, '/', $current_path)) ?: '/' ?></span> </div> <?php // 显示文件编辑器(如果请求了编辑) if (isset($_GET['action']) && $_GET['action'] === 'edit') { $edit_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']); // 验证文件路径 if ($edit_file && strpos($edit_file, BASE_DIR) === 0 && is_file($edit_file) && isEditable($edit_file)) { $content = htmlspecialchars(file_get_contents($edit_file)); $relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $edit_file); echo '<div class="breadcrumb"> <a href="?dir=' . urlencode(dirname($relative_path)) . '">返回文件列表</a> <span>›</span> <strong>' . htmlspecialchars(basename($edit_file)) . '</strong> </div>'; echo '<div class="editor-container"> <div class="editor-header"> <h2>编辑文件: ' . htmlspecialchars(basename($edit_file)) . '</h2> </div> <form method="POST"> <input type="hidden" name="file_path" value="' . htmlspecialchars($relative_path) . '"> <textarea name="file_content">' . $content . '</textarea> <div class="editor-actions"> <a href="?dir=' . urlencode(dirname($relative_path)) . '" class="btn">取消</a> <button type="submit" name="save_file" class="btn btn-success">保存更改</button> </div> </form> </div>'; // 不显示文件列表 exit; } } ?> <?php // 显示文件内容(如果请求了查看) if (isset($_GET['action']) && $_GET['action'] === 'view') { $view_file = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $_GET['file']); // 验证文件路径 if ($view_file && strpos($view_file, BASE_DIR) === 0 && is_file($view_file) && isEditable($view_file)) { $content = htmlspecialchars(file_get_contents($view_file)); $relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $view_file); echo '<div class="breadcrumb"> <a href="?dir=' . urlencode(dirname($relative_path)) . '">返回文件列表</a> <span>›</span> <strong>' . htmlspecialchars(basename($view_file)) . '</strong> </div>'; echo '<div class="editor-container"> <div class="editor-header"> <h2>查看文件: ' . htmlspecialchars(basename($view_file)) . '</h2> <a href="?action=edit&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn">编辑</a> </div> <pre style="background: #f8f9fa; padding: 20px; border-radius: 5px; border: 1px solid #eee; max-height: 500px; overflow: auto;">' . $content . '</pre> <div class="editor-actions"> <a href="?dir=' . urlencode(dirname($relative_path)) . '" class="btn">返回</a> </div> </div>'; // 不显示文件列表 exit; } } ?> <div class="list-header"> <div class="item-icon"></div> <div class="item-name">名称</div> <div>类型</div> <div>大小</div> <div>操作</div> </div> <ul class="file-list"> <?php // 读取目录内容 $items = scandir($current_path); if ($items === false) { die('无法读取目录内容'); } // 先显示目录 foreach ($items as $item) { if ($item === '.' || $item === '..') continue; $full_path = $current_path . DIRECTORY_SEPARATOR . $item; $relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path); if (is_dir($full_path)) { $size = '-'; $type = '文件夹'; echo '<li class="folder-item">'; echo '<div class="item-icon">📁</div>'; echo '<div class="item-name"><a href="?dir=' . urlencode($relative_path) . '">' . htmlspecialchars($item) . '</a></div>'; echo '<div>' . $type . '</div>'; echo '<div>' . $size . '</div>'; echo '<div class="item-actions">'; echo '<a href="?action=delete&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn btn-danger" onclick="return confirm(\'确定要删除这个文件夹吗?此操作不可恢复!\')">删除</a>'; echo '</div>'; echo '</li>'; } } // 再显示文件 foreach ($items as $item) { if ($item === '.' || $item === '..') continue; $full_path = $current_path . DIRECTORY_SEPARATOR . $item; $relative_path = str_replace(BASE_DIR . DIRECTORY_SEPARATOR, '', $full_path); if (!is_dir($full_path)) { $size = filesize($full_path); $type = pathinfo($item, PATHINFO_EXTENSION) . ' 文件'; // 格式化文件大小 if ($size < 1024) { $size = $size . ' B'; } elseif ($size < 1048576) { $size = round($size / 1024, 2) . ' KB'; } else { $size = round($size / 1048576, 2) . ' MB'; } echo '<li class="file-item">'; echo '<div class="item-icon">📄</div>'; echo '<div class="item-name">' . htmlspecialchars($item) . '</div>'; echo '<div>' . $type . '</div>'; echo '<div>' . $size . '</div>'; echo '<div class="item-actions">'; echo '<a href="?action=download&file=' . urlencode($relative_path) . '" class="btn">下载</a>'; echo '<a href="?action=delete&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn btn-danger" onclick="return confirm(\'确定要删除这个文件吗?此操作不可恢复!\')">删除</a>'; if (isEditable($full_path)) { echo '<a href="?action=view&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn">查看</a>'; echo '<a href="?action=edit&file=' . urlencode($relative_path) . '&dir=' . urlencode($dir) . '" class="btn">编辑</a>'; } echo '</div>'; echo '</li>'; } } ?> </ul> </div> <script> // 确认删除操作 function confirmDelete() { return confirm('确定要删除这个文件/文件夹吗?此操作不可恢复!'); } </script> </body> </html>
删除流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 A[开始删除目录] --> B{是文件?} B -->|是| C[删除文件] B -->|否| D[遍历目录内容] D --> E[获取第一项] E --> F{是 . 或 ..?} F -->|是| G[跳过] F -->|否| H[递归删除该项] H --> I{删除成功?} I -->|是| J[获取下一项] I -->|否| K[返回失败] J --> E G --> J K --> L[结束] J -->|所有项处理完| M[删除空目录] M --> N{删除成功?} N -->|是| O[返回成功] N -->|否| P[返回失败]
小迪的文件管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 <?php ini_set('open_basedir',__DIR__); $path=$_GET['path'] ?? './'; $action = isset($_GET['a'])?$_GET['a']:''; $path = isset($_GET['path'])?$_GET['path']:'.'; if(is_file($path)) { //获得文件名 $file = basename($path); //获得路径 $path = dirname($path); } //判断,不是目录 elseif(!is_dir($path)) { echo '我只会吃瓜!'; } function getlist($path){ $hd=opendir($path); while(($file_name=readdir($hd) )!== false){ if($file_name != '.' && $file_name != '..'){ $file_path = "$path/$file_name"; $file_type = filetype($file_path); } $list[$file_type][] = array( //$file_type = dir 和 file $list['dir'] 和 $list['file'] 'file_name'=>$file_name, //文件名存储键值file_name 'file_path'=>$file_path, //文件路径存储键值file_path 'file_size'=>round(filesize($file_path)/1024), //通过换算文件大小存储键值file_path 'file_time'=>date('Y/m/d H:i:s',filemtime($file_path)), //获取文件时间并存储键值file_path ); } closedir($hd); return $list; } $list=getlist($path); //接受方法 判断是怎么操作 //echo $action; switch ($action){ case 'del': unlink($file); //$cmd="del $file"; //system($cmd); //echo $cmd; break; case 'down': header("Content-Type: application/octet-stream"); header("Content-Disposition: attachment; filename=\"" . $file . "\""); header("Content-Length: " . filesize($file)); readfile($file); break; case 'edit': $content=file_get_contents($file); echo '<form name="form1" method="post" action="">'; echo "文件名:".$file."<br>"; echo "文件内容:<br>"; echo '<textarea name="code" style="resize:none;" rows="100" cols="100"">'.$content.'</textarea><br>'; echo '<input type="submit" name="submit" id="submit" value="提交">'; echo '</form>'; break; } //检测编辑后提交的事件 进入文件重新写入 if(isset($_POST['code'])){ $f=fopen("$path/$file",'w+'); fwrite($f,$_POST['code']); fclose($f); } ?> <table width="100%" style="font-size: 10px;text-align: center;"> <tr> <th>图标</th> <th>名称</th> <th>日期</th> <th>大小</th> <th>路径</th> <th>操作</th> </tr> <?php foreach ($list['dir'] as $v): ?> <tr> <td><img src="./img/list.png" width="20" height="20"></td> <td><?php echo $v['file_name']?></td> <td><?php echo $v['file_time']?></td> <td>-</td> <td><?php echo $v['file_path']?></td> <td><a href="?path=<?php echo $v['file_path']?>">打开</a></td> </tr> <?php endforeach;?> <?php foreach ($list['file'] as $v): ?> <tr> <td><img src="./img/file.png" width="20" height="20"></td> <td><?php echo $v['file_name']?></td> <td><?php echo $v['file_time']?></td> <td><?php echo $v['file_size']?></td> <td><?php echo $v['file_path']?></td> <td> <a href="?a=edit&path=<?php echo $v['file_path']?>">编辑</a> <a href="?a=down&path=<?php echo $v['file_path']?>">下载</a> <a href="?a=del&path=<?php echo $v['file_path']?>">删除</a> </td> </tr> <?php endforeach;?> </table>
思考这些代码中出现的安全问题
文件包含(包含的文件,以及被包含的文件中再次引用的包含都不能用变量进行传输)以及目录的读取,如果没有做严格的安全限制,那么就会出现目录穿越(可以通过php.ini(define(‘BASE_DIR’, realpath(DIR ));)中的安全设置,强制用户只能读取当前脚本的目录以及子目录)的问题。比如:./
,../
这样可以穿越目录。然后就可以进行文件目录的一个变量,文件包含上面也说过,如果你的include后面的值是一个可以控制的变量,那么就会出现,任何可以被读取到的文件都会被当作网站的脚本语言执行。
文件上传:可以让攻击者直接上传恶意文件。(安全的思路,首先你要取得文件的最后一个后缀,这个过程一定要严谨,然后在使用白名单,对这个后缀进行一个验证)
文件下载:可以下载任意的文件。需要对用下载的用户进行鉴权,如果没有权限下载,就不能允许下载。
文件删除:可以删除任意的文件。同文件下载,需要对用户进行鉴权。这个鉴权需要严谨。
1 同时这里使用的代码中的函数进行文件上传,文件的删除和文件的下载这样的操作,但是有些函数是系统的函数,也就是说不是通过代码实现的删除功能, 而是直接通过系统的命令进行删除,如果这里没有做严格的控制同样会出现一个命令执行的问题。