目录
前言
希望能通过写一个简单的CMS系统(php+mysql)来理解一些cms架构,并对于一些功能点能进行安全加固。
该文可能会有一些长,我是边编写,边写文章的,并且这部分还没有系统的学习过。所以之后在使用时发现的问题可能在最后改过来了,但我可能忘记提及,请见谅。也感谢各位师傅能给我提一些宝贵的意见。
一、基本内容
希望能实现的功能有:主要是一个文章管理系统。里面的内容包括:
1.内容管理
主要是文章上传,自己能看到属于自己上传的文章内容,也可以看到属于他人上传的文章内容
2.用户管理
用户的注册与登录、用户的基本信息(包括账号,密码,文章,头像上传等)
3.评论管理
用户可以在他人和自己的文章页面发表评论
使用到的工具:phpstudy phpmyadmin phpstorm
二、mysql部分
本来这部分的内容我想在PHP部分之后写的,但是在写完PHP部分之后我觉得还是有必要将这部分先插到前面来,因为在PHP部分中三个表之间的逻辑联系感觉需要先通过这部分给弄明白才好去学
前期准备
phpMyadmin用来管理数据库
打开phpstudy,打开web服务后在软件管理页面为专门的网页下载phpMyadmin
然后由于该phpMyadmin已经绑定了相应的网站,直接点击打开,输入账号密码登录进去就行了
1.users表
首先考虑有几个字段,username password avatar(头像,这里我打算存头像的路径) id(唯一标识)
created_at 注册时间
CREATE table users (
id INT AUTO_INCREMENT PRIMARY KEY, # AUTO_INCREMENT PRIMARY KEY 唯一标识用户的ID,自动递增
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
avatar VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
2.articles表
其字段应该有:article_id(主键,用于作为文章的唯一标识ID)user_id(外键,与users表中的id相挂钩) titile content author created_at
CREATE TABLE articles(
article_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
3.comments表
其字段应该有:comment_id user_id article_id content created_at
CREATE TABLE comments(
comment_id INT AUTO_INCREMENT PRIMARY KEY,
article_id INT NOT NULL,
user_id INT NOT NULL,
content text NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (article_id) REFERENCES articles(id), # 这里的foreign key约束用于维护数据库中的数据完整性,确保一个表中的数据引用另一个表中的有效数据,即每条评论都必须属于一篇有效的文章
FOREIGN KEY (user_id) REFERENCES users(id)
);
不过在正常情况下每个表还是用一个主键 id 更为妥当,而不是用article_id comment_id 作为主键,不然到后面会出现很多bug(像我一样)
三、PHP部分
这部分只包含PHP代码,但是写到后面我发现有一个点我并没有搞明白,导致我在编写的过程中遇到的问题很多——PHP代码在网页中有什么作用?
之所以会发现这个问题,是因为我想把前端部分交给AI,但是一些网页功能的实现我以为需要用PHP实现,其实还是需要靠前端实现,亦或是两者都有,导致有些功能在编写PHP代码时不知道要不要写。
但是后面一想,这也许是我并不明确PHP代码在网页设计中发挥的作用——(1)一些逻辑的实现(2)一些参数的提供.......所以我想先在每个PHP文件前先搞清楚这个文件的逻辑作用
1.连接mysql的php文件
config.php 这里的php代码主要实现的逻辑功能有实现对数据库的交互,我这里用函数connectToDatabase来实现
# config.php
<?php
function connectToDatabase(){
$host='localhost'; //定义了数据库服务器的主机地址,这里是本地主机
$db='cms';
$username='admin';
$password='123456!';
try {
$pdo=new PDO("mysql:host=$host;dbname=$db",$username,$password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// setAttribute是对PDO对象的属性进行设置
// PDO::ATTR_ERRMODE 常量 代表错误报告模式
// PDO::ERRMODE_EXCEPTION 常量 当执行SQL语句出现错误时,PDO会抛出一个PDOException异常
}catch(PDOException $e){
error_log("Connected failed: ",$e->getMessage());
echo "Sorry, there was an error connecting to the database. Please try again later.";
http_response_code(500);
// 当错误时,回应500状态码
exit;
}
return $pdo;
}
2.用户注册的php文件
register.php 先来想象一下这里代码的逻辑功能:
(1)首先判断表单是否提交,如果username和password字段都提交成功,才能进入注册用户的函数
if($_SERVER["REQUEST_METHOD"]=="POST"){
$username=$_POST['username'];
$password=$_POST['password'];
registerUser($username,$password);
}
但这里再细想一下的话,我们也该对表单是否填写完整进行一个判断,我想着这可以通过前端实现,但前端验证是可以被绕过的!后端验证的话,虽然稍微安全,但是需要等待服务器响应后才能得到反馈。。。最好的就是前后端一起验证,但是我想写的是一个最简单的cms,我先把这个简单的cms写出来,再进行安全加固。
(2)然后既然要注册一个新用户,肯定要和数据库进行交互
function registerUser($username, $password){
$pdo=connectToDatabase();
try{
$hashPassword=password_hash($password, PASSWORD_DEFAULT);
// PASSWORD 表示使用PHP当前版本默认的密码哈希算法
$stmt=$pdo->prepare("INSERT INTO users (username, password) VALUES (?,?)");
$stmt->execute([$username, $hashPassword]);
echo "注册成功!";
header('Location: login.php');
exit;
}catch(PDOException $e){
error_log("Register failed: ".$e->getMessage());
echo "Sorry, there was an error during registration. Please try again later.";
http_response_code(500);
}
}
然后就是整个的register.php代码
<?php
require 'config.php';
function registerUser($username, $password){
$pdo=connectToDatabase();
try{
$hashPassword=password_hash($password, PASSWORD_DEFAULT);
// PASSWORD 表示使用PHP当前版本默认的密码哈希算法
$stmt=$pdo->prepare("INSERT INTO users (username, password) VALUES (?,?)");
$stmt->execute([$username, $hashPassword]);
echo "注册成功!";
header('Location: login.php');
exit;
}catch(PDOException $e){
error_log("Register failed: ".$e->getMessage());
echo "Sorry, there was an error during registration. Please try again later.";
http_response_code(500);
}
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST['username'];
$password = $_POST['password'];
registerUser($username, $password);
}
?>
3.用户登录的php文件
login.php 同样的,这里也需要填写一个POST表单,并且,既然是登录的话,其逻辑功能应该有:(1)判断账号密码哈希是否正确(2)输入正确后能导向主页部分(3)既然是登录进去,之后需要一个能验证本人身份的“令牌”,这里我用$_SESSION['user_id']来存储用户的id作为令牌
整个的login.php代码
<?php
require 'config.php';
session_start();
function loginUser($username,$password){
$pdo=connectToDatabase();
try{
$stmt=$pdo->prepare("SELECT id,password from users where username=?");
$stmt->execute([$username]);
$result=$stmt->fetch(PDO::FETCH_ASSOC);
// PDO::FETCH_ASSOC 从结果集中取出一行数据
if($result && password_verify($password,$result['password'])){
// 使用password_verify匹配
$_SESSION['user_id']=$result['id'];
// 将用户ID存储在会话中
header('Location: index.php');
echo '登录成功';
exit();
}else{
echo '用户名或密码错误';
}
}catch(PDOException $e){
error_log("login error: ".$e->getMessage());
echo "Sorry, there was an error during login. Please try again later.";
http_response_code(500);
}
}
if($_SERVER["REQUEST_METHOD"]=="POST"){
$username=$_POST["username"];
$password=$_POST["password"];
loginUser($username,$password);
}
4.网页主页面的php文件
index.php 前期的登录和注册都做完了,现在用户在登录进去之后首先可以看到(1)搜索文章的搜索框,然后搜索到了就点进去(2)同时在首页还可以点进去编辑自己的个人信息(user_info.php),(3)并且点击退出登录可以摧毁会话
逻辑功能:(1)首先查看此时的user_id身份标识是否被设置$_SESSION['user_id'](这在我们之前的login.php已经被设置 (2) 通过这个身份标识来获取用户的信息(我想用点击的方式进入用户信息编辑页面,所以这里应该利用前端代码来实现,没关系,只是一个跳转到其他文件的功能,有$_SESSION['user_id']就很好(3)实现搜索请求,这里应该是将参数传给article.php让它来执行
<?php
session_start();
require 'config.php';
# 检查用户是否已经登录
if(!isset($_SESSION['user_id'])){
header("Location: login.php");
exit();
}
$user_id=$_SESSION['user_id'];
# 处理搜索请求
if($_SERVER["REQUEST_METHOD"]=="POST" && isset($_POST['query'])) {
$query = $_POST['query'];
#存储此时的搜索值
$_SESSION['search_query'] = $query;
header("Location: articles.php");
exit();
}
5.搜索的文章的php文件
article.php 还记得之前传入的搜索参数,既然传入了搜索参数的话,就得在数据库中进行搜索了,需要用到sql语句,然后如果搜得到的话就显示出来,搜不到就显示没有结果。
逻辑功能:通过参数在articles表中查询与其相对应的标题字段,同时,会出现整篇文章的内容,我们可以通过query和title进行关联找到此时的articles的基本信息,还应该包含什么信息呢?评论部分应该也要显示出来,而评论与谁有关也得连结一下。
简单来说:就是要实现users表、articles表和comments表的连接。users表和articles表进行连接的应该是作者的id;articles表和comments表进行连接的应该是文章的id;comments表和users表进行连接的应该是评论者的id,如果一个人进行了多条评论,借助user_id以及评论的唯一标识id,通过where来选择,就可以将一条条评论列出来了。
现在来写一下:
首先,借助$_SESSION['search_query']这个输入的搜索参数来搜索文章,然后得到文章的全部内容以及作者的名字
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
$search_query = $_SESSION['search_query'] ?? '';
if (empty($search_query)) {
die("搜索查询未提供");
}
# 获取匹配的文章的信息
$stmt = $pdo->prepare("SELECT articles.*
FROM articles
JOIN users ON articles.user_id = users.id
WHERE articles.title LIKE ?");
$stmt->execute(['%' . $search_query . '%']);
$article = $stmt->fetch();
if (!$article) {
die("没有找到相关结果");
}
articles表和users表的连接已经完成,接下来是获取与该文章有关的评论,即articles表与comments表的连结:
# 获取文章评论
$article_id = $article['article_id'];
$_SESSION['article_id'] = $article_id;
$stmt = $pdo->prepare("SELECT comments.*, users.username AS commenter
FROM comments
JOIN users ON comments.user_id = users.id
WHERE comments.article_id = ?");
$stmt->execute([$article_id]);
$comments = $stmt->fetchAll();
?>
与之同时完成的,还有comments表和users表的连结。按过程来说就是:
(1)首先以文章的标题为媒介进行匹配,然后通过articles表中的user_id字段,让其与users表中的id字段再进行匹配,找到相应的文章和作者信息,完成连结
(2)通过上一步已经有的article_id,然后以comments表中的article_id为媒介进行匹配,从而实现comments表和articles表的连结
(3)最后让comments表中的user_id字段去匹配users表中的id字段,从而找到对应的评论和评论者
之后还需要判断此时是否有评论(先不写了),这里我还想加一个链接跳转到写评论的页面comment.php去,可以使用前端点击跳转,下面是总的article.php代码
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
$search_query = $_SESSION['search_query'] ?? '';
if (empty($search_query)) {
die("搜索查询未提供");
}
# 获取匹配的文章的信息
$stmt = $pdo->prepare("SELECT articles.*
FROM articles
JOIN users ON articles.user_id = users.id
WHERE articles.title LIKE ?");
$stmt->execute(['%' . $search_query . '%']);
$article = $stmt->fetch();
if (!$article) {
die("没有找到相关结果");
}
# 获取文章评论
$article_id = $article['article_id'];
$_SESSION['article_id'] = $article_id;
$stmt = $pdo->prepare("SELECT comments.*, users.username AS commenter
FROM comments
JOIN users ON comments.user_id = users.id
WHERE comments.article_id = ?");
$stmt->execute([$article_id]);
$comments = $stmt->fetchAll();
?>
6.用户评论的php文件
comment.php
通过用户的id来给这篇文章评论,并将评论的内容存到comments表中
逻辑作用:这里的article_id让我想起来上面如果搜到了一篇文章的时候,要给其进行评论,可以在点击前端链接的时候插入一段PHP代码,令此时的SESSION['article_id']为这篇文章的id就行了
<?php
require 'config.php';
session_start();
$pdo = connectToDatabase();
$article_id =$_GET['article_id']?? null;
if ($article_id == null) {
die("文章ID未提供");
}
# 检查用户是否已经登录
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$user_id = $_SESSION['user_id'];
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$content = $_POST["content"] ?? null;
if (empty($content)) {
die("评论内容不能为空");
}
# 插入数据
$stmt = $pdo->prepare("INSERT INTO comments (article_id, user_id, content, created_at) VALUES (?, ?, ?, NOW())");
$stmt->execute([$article_id, $user_id, $content]);
# 此时再回到文章页面
header("Location: article.php?article_id=" . $article_id . "&query=" . urlencode($_SESSION['search_query']));
exit();
}
?>
7.用户基本信息的php文件
user_info.php
现在回到index.php首页,我们需要一个链接点击进入我们现在的用户资料展示页中,现在需要的参数只有一个,就是user_id,然后要实现与其相关的articles表的连结——看自己有几篇文章
逻辑功能:先通过user_id来引入users表中的账号以及头像,头像怎么存?我想着用文件系统来存,其实就是利用php,但是每个用户对应的头像怎么得到呢?可以在users表中加一个字段avatar来存储文章目录。然后是文章的显示,通过user_id与articles表中的user_id来连结,再加上添加文章,编辑文章,删除文章的功能(但这里不体现,因为可以直接通过链接跳转到add_article.php等页面)。最后就是个人信息的修改,也是同理,在user_change.php页面
<?php
session_start();
require 'config.php';
$pdo=connectToDatabase();
# 检查用户是否已经登录
if(!isset($_SESSION['user_id'])){
die("请先登录");
}
$user_id=$_SESSION['user_id'];
# 获取用户的信息
$stmt=$pdo->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([$user_id]);
$user_info=$stmt->fetch();
if(!$user_info){
die("未找到用户信息");
}
然后到了上传文件块,这里我用另一个文件upload_avator来进行操作,这里可以先用一个表单来存文件(在前端中弄一下就行),到时候转到upload_avator中来进行处理就行
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
# 检查用户是否已经登录
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$user_id = $_SESSION['user_id'];
# 获取用户的信息
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user_info = $stmt->fetch();
if (!$user_info) {
die("未找到用户信息");
}
# 获取用户的文章信息
$stmt = $pdo->prepare("SELECT * FROM articles WHERE user_id = ?");
$stmt->execute([$user_id]);
$articles = $stmt->fetchAll();
# 将绝对路径转换为相对路径
$avatar_path=str_replace('D:/phpstudy_pro/WWW/cms/', '', $user_info['avatar']);
?>
8.头像上传的php文件
然后是头像文件上传的处理
upload_avatar.php
逻辑功能:
(1)因为users表中的avatar字段用来存储头像文件存储的路径,所以先要得到user_id
(2)然后检查是否提交表单,并且是否提交成功,提交成功后,可以用一些参数记下此时头像文件的一些参数,需要对文件后缀规定白名单
(3)最后将图片重命名存进去(这里直接进行安全加固了,将图片名以不为人知的方法进行重命名可以有效防止文件上传漏洞)然后将头像路径进行更新
<?php
session_start();
require 'config.php';
$pdo=connectToDatabase();
# 检查用户是否已经登录
if(!isset($_SESSION['user_id'])){
die("请先登录");
}
$user_id = $_SESSION['user_id'];
if($_SERVER['REQUEST_METHOD']=='POST'){
if(isset($_FILES['avatar']) && $_FILES['avatar']['error']==UPLOAD_ERR_OK){
$file_tmp_path = $_FILES['avatar']['tmp_name'];
$file_name = $_FILES['avatar']['name'];
$file_size = $_FILES['avatar']['size'];
$file_type = $_FILES['avatar']['type'];
$file_name_cmps = explode(".", $file_name);# 将文件名按 '.' 切割成数组
$file_ext=strtolower(end($file_name_cmps));# 获取文件拓展名
# 白名单
$allowed=array('jpg','jpeg','png','gif');
if(in_array($file_ext,$allowed)){
# 重命名文件 确保唯一性
$file_new_name=md5(time().$file_name).'.'.$file_ext;
$upload_file_dir='D:/phpstudy_pro/WWW/cms/upload/';
$dest_path=$upload_file_dir.$file_new_name; # 完整路径
if(move_uploaded_file($file_tmp_path,$dest_path)){ # 将临时文件移动到目标目录,如果移动成功
# 更新用户头像路径
$stmt=$pdo->prepare("UPDATE users SET avatar=? WHERE id=?");
$stmt->execute([$dest_path,$user_id]);
$_SESSION['message']='文件上传成功。';
}else {
$_SESSION['message'] = '移动文件出错';
}
}else{
$_SESSION['message']='上传失败,文件类型不允许';
}
}else{
$_SESSION['message']='上传失败,错误代码: '.$_FILES['avatar']['error'];
}
header('location: user_info.php');
exit();
}
除此之外,需要通过$_SESSION['message']在user_info.php中反应结果,所以user_info.php中还需要加上一段,显示上传头像的反馈信息。这段代码加在前端中
if (isset($_SESSION['message'])) {
echo '<p>' . htmlspecialchars($_SESSION['message']) . '</p>';
unset($_SESSION['message']);
}
9.修改基本信息的php文件
user_info_change.php
通过user_info.php点击链接进去进行修改,主要修改的是账号和密码
逻辑功能:(1)得到当前用户的所有信息,更新账号 (2)密码需要填写原本密码进行验证之后菜允许更新
<?php
session_start();
require 'config.php';
$pdo=connectToDatabase();
# 检查用户是否已经登录
if(!isset($_SESSION['user_id'])){
die("请先登录!");
}
$user_id = $_SESSION['user_id'];
$stmt=$pdo->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([$user_id]);
$user_info=$stmt->fetch();
if(!$user_info){
die("未找到用户信息");
}
if($_SERVER["REQUEST_METHOD"] == "POST"){
$username = $_POST["username"] ?? '';
$old_password = $_POST["old_password"] ?? '';
$new_password=$_POST["new_password"] ?? '';
if(empty($username)){
echo "账号不能为空";
}else{
if(!empty($old_password)&&!empty($new_password)){
if(password_verify($old_password, $user_info['password'])){
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
$stmt=$pdo->prepare("UPDATE users SET username=?,password=? WHERE id=?");
$stmt->execute([$username, $hashed_password, $user_id]);
echo "信息更新成功!";
}else{
echo "原密码不正确!";
}
}else{
$stmt=$pdo->prepare("UPDATE users SET username=? WHERE id=?");
$stmt->execute([$username, $user_id]);
echo "账号信息更新成功";
}
}
}
10.添加文章的php文件
add_article.php
在user_info.php页面点击进去添加文章,需要有文章的作者,标题,内容,以及id
<?php
session_start();
require 'config.php';
$pdo=connectToDatabase();
if(!isset($_SESSION['user_id'])){
die("请先登录");
}
$user_id = $_SESSION['user_id'];
$message = '';
if($_SERVER['REQUEST_METHOD']=='POST'){
$title = $_POST['title'] ?? '';
$content = $_POST['content'] ?? '';
$author = $_POST['author'] ?? '';
if(empty($title) || empty($content) || empty($author)){
$message = "请填写完整信息";
} else {
try {
$stmt = $pdo->prepare("INSERT INTO articles (author, title, content, user_id, created_at) VALUES (?, ?, ?, ?, NOW())");
$stmt->execute([$author, $title, $content, $user_id]);
$message = "文章添加成功";
}catch (PDOException $e) {
error_log("error adding article: ".$e->getMessage());
$_SESSION['message']='添加文章时出错,请稍后再试';
}
header('Location: user_info.php');
exit();
}
}
?>
11.编辑文章的php文件
edit_article.php
此时需要先获得文章的id,而并不是user_id,文章id作为文章的唯一标识id,点击编辑文章时,会获得此文章的id
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$user_id = $_SESSION['user_id'];
$article_id = $_GET['article_id'] ?? null;
if (!$article_id) {
die("未找到文章ID");
}
$stmt = $pdo->prepare("SELECT * FROM articles WHERE article_id = ? AND user_id = ?");
$stmt->execute([$article_id, $user_id]);
$article = $stmt->fetch();
if (!$article) {
die("未找到该文章信息");
}
$message = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$title = $_POST['title'] ?? '';
$content = $_POST['content'] ?? '';
$author = $_POST['author'] ?? '';
if (empty($title) || empty($content) || empty($author)) {
$message = "请填写完整信息";
} else {
$stmt = $pdo->prepare("UPDATE articles SET title = ?, content = ?, author = ? WHERE article_id = ? AND user_id = ?");
$stmt->execute([$title, $content, $author, $article_id, $user_id]);
$message = "文章编辑成功";
header('Location: user_info.php');
exit();
}
}
?>
12.删除文章的php文件
delete_article.php
比上面麻烦的是,这里需要关联三个表,因为评论部分也需要删除
<?php
session_start();
require 'config.php';
$pdo=connectToDatabase();
if(!isset($_SESSION['user_id'])){
die("请先登录");
}
$usre_id=$_SESSION['user_id'];
$article_id=$_GET['article_id'] ?? null;
if(!$article_id){
die("未找到文章ID");
}
# 先看看能不能找到该文章
$stmt=$pdo->prepare("SELECT * FROM articles WHERE article_id=? AND user_id=?");
$stmt->execute([$article_id,$usre_id]);
$article=$stmt->fetch();
if(!$article){
die("未找到文章信息");
}
if($_SERVER['REQUEST_METHOD']=='POST'){
# 删除文章
$stmt=$pdo->prepare("DELETE FROM articles WHERE article_id=? AND user_id=?");
$stmt->execute([$article_id,$usre_id]);
# 删除评论
$stmt=$pdo->prepare("DELETE FROM comments WHERE article_id=? ");
$stmt->execute([$article_id]);
echo "文章及相关评论删除成功";
header('Location:user_info.php');
exit();
}
13.查看自己文章的php代码
view_article.php 在user_info.php点击文章标题从而跳往查看文章内容的php文件
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
# 获取文章ID
$article_id = $_GET['article_id'] ?? null;
if (!$article_id) {
die("未找到文章ID");
}
# 获取文章信息
$stmt = $pdo->prepare("SELECT * FROM articles WHERE article_id = ?");
$stmt->execute([$article_id]);
$article = $stmt->fetch();
if (!$article) {
die("未找到文章信息");
}
?>
14.首页退出登录的php代码
<?php
session_start();
session_destroy(); # 销毁会话
header("Location: login.php"); # 重定向到登录页面
exit();
四、PHP+前端部分代码
1.register.php
这里在前端加一个表单完整性验证就行了,用js进行判断
<?php
require 'config.php';
function registerUser($username, $password) {
$pdo = connectToDatabase();
try {
$hashPassword = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD 表示使用PHP当前版本默认的密码哈希算法
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?,?)");
$stmt->execute([$username, $hashPassword]);
echo "注册成功!";
header('Location: login.php');
exit;
} catch(PDOException $e) {
error_log("Register failed: " . $e->getMessage());
echo "Sorry, there was an error during registration. Please try again later.";
http_response_code(500);
}
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST['username'];
$password = $_POST['password'];
registerUser($username, $password);
}
?>
<!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: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.error {
color: red;
margin-bottom: 10px;
}
</style>
<script>
function validateForm() {
var username = document.forms["registerForm"]["username"].value;
var password = document.forms["registerForm"]["password"].value;
var confirmPassword = document.forms["registerForm"]["confirm_password"].value;
var errorElement = document.getElementById("error");
if (username == "" || password == "" || confirmPassword == "") {
errorElement.textContent = "所有字段均为必填项。";
return false;
}
if (password != confirmPassword) {
errorElement.textContent = "两次输入的密码不一致。";
return false;
}
return true;
}
</script>
</head>
<body>
<div class="container">
<h1>用户注册</h1>
<form name="registerForm" action="register.php" method="post" onsubmit="return validateForm()">
<div id="error" class="error"></div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<label for="password">密码:</label>
<input type="password" id="password" name="password">
<label for="confirm_password">确认密码:</label>
<input type="password" id="confirm_password" name="confirm_password">
<input type="submit" value="注册">
</form>
</div>
</body>
</html>
2.login.php
<?php
require 'config.php';
session_start();
function loginUser($username, $password) {
$pdo = connectToDatabase();
try {
$stmt = $pdo->prepare("SELECT id, password FROM users WHERE username = :username");
$stmt->bindParam(':username', $username);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result && password_verify($password, $result['password'])) {
$_SESSION['user_id'] = $result['id'];
header('Location: index.php');
exit();
} else {
echo '用户名或密码错误';
}
} catch (PDOException $e) {
error_log("login error: " . $e->getMessage());
echo "Sorry, there was an error during login. Please try again later.";
http_response_code(500);
}
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST["username"];
$password = $_POST["password"];
loginUser($username, $password);
}
?>
<!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: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.error {
color: red;
margin-bottom: 10px;
}
.register-link {
text-align: center;
margin-top: 20px;
}
.register-link a {
color: #4CAF50;
text-decoration: none;
}
.register-link a:hover {
text-decoration: underline;
}
</style>
<script>
function validateForm() {
var username = document.forms["loginForm"]["username"].value;
var password = document.forms["loginForm"]["password"].value;
var errorElement = document.getElementById("error");
if (username == "" || password == "") {
errorElement.textContent = "用户名和密码必须填写。";
return false;
}
return true;
}
</script>
</head>
<body>
<div class="container">
<h1>用户登录</h1>
<form name="loginForm" action="login.php" method="post" onsubmit="return validateForm()">
<div id="error" class="error"></div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<label for="password">密码:</label>
<input type="password" id="password" name="password">
<input type="submit" value="登录">
</form>
<div class="register-link">
<a href="register.php">还没有账号?点击这里注册</a>
</div>
</div>
</body>
</html>
3.index.php
<?php
session_start();
require 'config.php';
# 检查用户是否已经登录
if(!isset($_SESSION['user_id'])){
header("Location: login.php");
exit();
}
$user_id=$_SESSION['user_id'];
# 处理搜索请求
if($_SERVER["REQUEST_METHOD"]=="POST" && isset($_POST['query'])){
$query=$_POST['query'];
# 存储此时的搜索值
$_SESSION['search_query']=$query;
header("Location: article.php");
exit();
}
?>
<!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: #f2f2f2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
text-align: center;
}
h1 {
margin-bottom: 20px;
}
input[type="text"] {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.profile-link, .logout-link {
margin-top: 20px;
display: block;
color: #4CAF50;
text-decoration: none;
}
.profile-link:hover, .logout-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>首页</h1>
<form action="index.php" method="post">
<input type="text" name="query" placeholder="搜索文章..." required>
<input type="submit" value="开始搜索">
</form>
<a href="user_info.php" class="profile-link">查看个人资料</a>
<a href="logout.php" class="logout-link">退出登录</a>
</div>
</body>
</html>
4.article.php
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
$search_query = $_SESSION['search_query'] ?? '';
if (empty($search_query)) {
die("搜索查询未提供");
}
# 获取匹配的文章的信息
$stmt = $pdo->prepare("SELECT articles.*, users.username AS author
FROM articles
JOIN users ON articles.user_id = users.id
WHERE articles.title LIKE ?");
$stmt->execute(['%' . $search_query . '%']);
$article = $stmt->fetch();
if (!$article) {
die("没有找到相关结果");
}
# 获取文章评论
$article_id = $article['article_id'];
$_SESSION['article_id'] = $article_id;
$stmt = $pdo->prepare("SELECT comments.*, users.username AS commenter
FROM comments
JOIN users ON comments.user_id = users.id
WHERE comments.article_id = ?");
$stmt->execute([$article_id]);
$comments = $stmt->fetchAll();
?>
<!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: #f2f2f2;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 10px;
}
.author {
color: #888;
margin-bottom: 20px;
}
.content {
margin-bottom: 20px;
}
.comments-section {
margin-top: 40px;
}
.comment {
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
margin-bottom: 10px;
}
.comment:last-child {
border-bottom: none;
}
.commenter {
color: #555;
font-weight: bold;
}
.comment-content {
margin-top: 5px;
}
.comment-button {
display: inline-block;
padding: 10px 20px;
background-color: #4CAF50;
color: #fff;
text-decoration: none;
border-radius: 5px;
margin-top: 20px;
}
.comment-button:hover {
background-color: #45a049;
}
.back-link {
display: block;
margin-top: 20px;
text-align: center;
color: #007BFF;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1><?php echo htmlspecialchars($article['title']); ?></h1>
<div class="author">作者: <?php echo htmlspecialchars($article['author']); ?></div>
<div class="content"><?php echo nl2br(htmlspecialchars($article['content'])); ?></div>
<div class="comments-section">
<h2>评论</h2>
<?php if (count($comments) > 0): ?>
<?php foreach ($comments as $comment): ?>
<div class="comment">
<div class="commenter"><?php echo htmlspecialchars($comment['commenter']); ?></div>
<div class="comment-content"><?php echo nl2br(htmlspecialchars($comment['content'])); ?></div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p>暂无评论</p>
<?php endif; ?>
</div>
<a href="comment.php?article_id=<?php echo $article_id;?>" class="comment-button">给该文章评论</a>
<a href="index.php" class="back-link">返回首页</a>
</div>
</body>
</html>
5.comment.php
<?php
require 'config.php';
session_start();
$pdo = connectToDatabase();
$article_id =$_GET['article_id']?? null;
if ($article_id == null) {
die("文章ID未提供");
}
# 检查用户是否已经登录
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$user_id = $_SESSION['user_id'];
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$content = $_POST["content"] ?? null;
if (empty($content)) {
die("评论内容不能为空");
}
# 插入数据
$stmt = $pdo->prepare("INSERT INTO comments (article_id, user_id, content, created_at) VALUES (?, ?, ?, NOW())");
$stmt->execute([$article_id, $user_id, $content]);
# 此时再回到文章页面
header("Location: article.php?article_id=" . $article_id . "&query=" . urlencode($_SESSION['search_query']));
exit();
}
?>
<!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: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 400px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
textarea {
width: 100%;
height: 100px;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="container">
<h1>发表评论</h1>
<form action="comment.php?article_id=<?php echo $article_id;?>" method="post">
<label for="content">评论内容:</label>
<textarea id="content" name="content" required></textarea>
<input type="submit" value="提交评论">
</form>
</div>
</body>
</html>
6.user_info.php
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
# 检查用户是否已经登录
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$user_id = $_SESSION['user_id'];
# 获取用户的信息
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user_info = $stmt->fetch();
if (!$user_info) {
die("未找到用户信息");
}
# 获取用户的文章信息
$stmt = $pdo->prepare("SELECT * FROM articles WHERE user_id = ?");
$stmt->execute([$user_id]);
$articles = $stmt->fetchAll();
# 将绝对路径转换为相对路径
$avatar_path = str_replace('D:/phpstudy_pro/WWW/cms/', '', $user_info['avatar']);
?>
<!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: #f2f2f2;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 10px;
}
.user-info {
margin-bottom: 20px;
}
.user-info p {
margin: 5px 0;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 20px;
}
.articles-section {
margin-top: 30px;
}
.article {
border-bottom: 1px solid #ddd;
padding: 10px 0;
margin-bottom: 10px;
}
.article:last-child {
border-bottom: none;
}
.article-title {
font-size: 18px;
color: #333;
}
.article-title a {
color: #333;
text-decoration: none;
}
.article-title a:hover {
text-decoration: underline;
}
.article-date {
color: #888;
font-size: 14px;
}
.article-actions {
margin-top: 10px;
}
.article-actions a {
margin-right: 10px;
color: #4CAF50;
text-decoration: none;
}
.article-actions a:hover {
text-decoration: underline;
}
.add-article-link {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: #fff;
text-decoration: none;
border-radius: 5px;
}
.add-article-link:hover {
background-color: #45a049;
}
.edit-profile-link {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #FF9800;
color: #fff;
text-decoration: none;
border-radius: 5px;
}
.edit-profile-link:hover {
background-color: #FB8C00;
}
.back-link {
display: block;
margin-top: 20px;
text-align: center;
color: #007BFF;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.message {
color: red;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>个人资料</h1>
<?php if (isset($_SESSION['message'])): ?>
<p class="message"><?php echo htmlspecialchars($_SESSION['message']); unset($_SESSION['message']); ?></p>
<?php endif; ?>
<div class="user-info">
<img src="<?php echo htmlspecialchars($avatar_path); ?>" alt="头像" class="avatar">
<p><strong>账号:</strong> <?php echo htmlspecialchars($user_info['username']); ?></p>
</div>
<form action="upload_avatar.php" method="post" enctype="multipart/form-data">
<label for="avatar">上传头像:</label>
<input type="file" id="avatar" name="avatar" required>
<input type="submit" value="上传">
</form>
<div class="articles-section">
<h2>我的文章</h2>
<?php if (count($articles) > 0): ?>
<?php foreach ($articles as $article): ?>
<div class="article">
<div class="article-title"><a href="view_article.php?article_id=<?php echo $article['article_id']; ?>"><?php echo htmlspecialchars($article['title']); ?></a></div>
<div class="article-date">发布时间: <?php echo htmlspecialchars($article['created_at']); ?></div>
<div class="article-actions">
<a href="edit_article.php?article_id=<?php echo $article['article_id']; ?>">编辑文章</a>
<a href="delete_article.php?article_id=<?php echo $article['article_id']; ?>">删除文章</a>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p>暂无文章</p>
<?php endif; ?>
</div>
<a href="add_article.php" class="add-article-link">添加文章</a>
<a href="user_info_change.php" class="edit-profile-link">修改个人信息</a>
<a href="index.php" class="back-link">返回首页</a>
</div>
</body>
</html>
7.user_info_change.php
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
# 检查用户是否已经登录
if (!isset($_SESSION['user_id'])) {
die("请先登录!");
}
$user_id = $_SESSION['user_id'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user_info = $stmt->fetch();
if (!$user_info) {
die("未找到用户信息");
}
$message = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST["username"] ?? '';
$old_password = $_POST["old_password"] ?? '';
$new_password = $_POST["new_password"] ?? '';
if (empty($username)) {
$message = "账号不能为空";
} else {
if (!empty($old_password) && !empty($new_password)) {
if (password_verify($old_password, $user_info['password'])) {
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET username = ?, password = ? WHERE id = ?");
$stmt->execute([$username, $hashed_password, $user_id]);
$message = "信息更新成功!";
} else {
$message = "原密码不正确!";
}
} else {
$stmt = $pdo->prepare("UPDATE users SET username = ? WHERE id = ?");
$stmt->execute([$username, $user_id]);
$message = "账号信息更新成功";
}
}
}
?>
<!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: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 400px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.message {
text-align: center;
margin-bottom: 20px;
color: red;
}
.back-link {
display: block;
text-align: center;
margin-top: 20px;
color: #007BFF;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>修改个人信息</h1>
<?php if (!empty($message)): ?>
<div class="message"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<form action="user_info_change.php" method="post">
<div class="form-group">
<label for="username">账号:</label>
<input type="text" id="username" name="username" value="<?php echo htmlspecialchars($user_info['username']); ?>" required>
</div>
<div class="form-group">
<label for="old_password">原密码:</label>
<input type="password" id="old_password" name="old_password">
</div>
<div class="form-group">
<label for="new_password">新密码:</label>
<input type="password" id="new_password" name="new_password">
</div>
<input type="submit" value="更新信息">
</form>
<a href="user_info.php" class="back-link">返回个人信息</a>
</div>
</body>
</html>
8.add_article.php
<?php
session_start();
require 'config.php';
$pdo=connectToDatabase();
if(!isset($_SESSION['user_id'])){
die("请先登录");
}
$user_id = $_SESSION['user_id'];
$message = '';
if($_SERVER['REQUEST_METHOD']=='POST'){
$title = $_POST['title'] ?? '';
$content = $_POST['content'] ?? '';
$author = $_POST['author'] ?? '';
if(empty($title) || empty($content) || empty($author)){
$message = "请填写完整信息";
} else {
try {
$stmt = $pdo->prepare("INSERT INTO articles (author, title, content, user_id, created_at) VALUES (?, ?, ?, ?, NOW())");
$stmt->execute([$author, $title, $content, $user_id]);
$message = "文章添加成功";
}catch (PDOException $e) {
error_log("error adding article: ".$e->getMessage());
$_SESSION['message']='添加文章时出错,请稍后再试';
}
header('Location: user_info.php');
exit();
}
}
?>
<!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: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 500px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
textarea {
height: 150px;
resize: vertical;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.message {
text-align: center;
margin-bottom: 20px;
color: red;
}
</style>
</head>
<body>
<div class="container">
<h1>添加文章</h1>
<?php if (!empty($message)): ?>
<div class="message"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<form action="add_article.php" method="post">
<div class="form-group">
<label for="author">作者:</label>
<input type="text" id="author" name="author" required>
</div>
<div class="form-group">
<label for="title">标题:</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="content">内容:</label>
<textarea id="content" name="content" required></textarea>
</div>
<input type="submit" value="添加文章">
</form>
</div>
</body>
</html>
9.edit_article.php
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$user_id = $_SESSION['user_id'];
$article_id = $_GET['article_id'] ?? null;
if (!$article_id) {
die("未找到文章ID");
}
$stmt = $pdo->prepare("SELECT * FROM articles WHERE article_id = ? AND user_id = ?");
$stmt->execute([$article_id, $user_id]);
$article = $stmt->fetch();
if (!$article) {
die("未找到该文章信息");
}
$message = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$title = $_POST['title'] ?? '';
$content = $_POST['content'] ?? '';
$author = $_POST['author'] ?? '';
if (empty($title) || empty($content) || empty($author)) {
$message = "请填写完整信息";
} else {
$stmt = $pdo->prepare("UPDATE articles SET title = ?, content = ?, author = ? WHERE article_id = ? AND user_id = ?");
$stmt->execute([$title, $content, $author, $article_id, $user_id]);
$message = "文章编辑成功";
header('Location: user_info.php');
exit();
}
}
?>
<!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: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 500px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
textarea {
height: 150px;
resize: vertical;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.message {
text-align: center;
margin-bottom: 20px;
color: red;
}
</style>
</head>
<body>
<div class="container">
<h1>编辑文章</h1>
<?php if (!empty($message)): ?>
<div class="message"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<form action="edit_article.php?article_id=<?php echo htmlspecialchars($article_id); ?>" method="post">
<div class="form-group">
<label for="author">作者:</label>
<input type="text" id="author" name="author" value="<?php echo htmlspecialchars($article['author']); ?>" required>
</div>
<div class="form-group">
<label for="title">标题:</label>
<input type="text" id="title" name="title" value="<?php echo htmlspecialchars($article['title']); ?>" required>
</div>
<div class="form-group">
<label for="content">内容:</label>
<textarea id="content" name="content" required><?php echo htmlspecialchars($article['content']); ?></textarea>
</div>
<input type="submit" value="更新文章">
</form>
</div>
</body>
</html>
10.view_article.php
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
# 获取文章ID
$article_id = $_GET['article_id'] ?? null;
if (!$article_id) {
die("未找到文章ID");
}
# 获取文章信息
$stmt = $pdo->prepare("SELECT * FROM articles WHERE article_id = ?");
$stmt->execute([$article_id]);
$article = $stmt->fetch();
if (!$article) {
die("未找到文章信息");
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($article['title']); ?></title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 10px;
}
.article-info {
margin-bottom: 20px;
}
.article-info p {
margin: 5px 0;
color: #888;
}
.article-content {
margin-top: 30px;
}
</style>
</head>
<body>
<div class="container">
<h1><?php echo htmlspecialchars($article['title']); ?></h1>
<div class="article-info">
<p><strong>作者:</strong> <?php echo htmlspecialchars($article['author']); ?></p>
<p><strong>发布时间:</strong> <?php echo htmlspecialchars($article['created_at']); ?></p>
</div>
<div class="article-content">
<?php echo nl2br(htmlspecialchars($article['content'])); ?>
</div>
</div>
</body>
</html>
11.delete_article.php
<?php
session_start();
require 'config.php';
$pdo = connectToDatabase();
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$user_id = $_SESSION['user_id'];
$article_id = $_GET['article_id'] ?? null;
if (!$article_id) {
die("未找到文章ID");
}
# 先看看能不能找到该文章
$stmt = $pdo->prepare("SELECT * FROM articles WHERE article_id = ? AND user_id = ?");
$stmt->execute([$article_id, $user_id]);
$article = $stmt->fetch();
if (!$article) {
die("未找到文章信息");
}
$message = '';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
# 删除文章
$stmt = $pdo->prepare("DELETE FROM articles WHERE article_id = ? AND user_id = ?");
$stmt->execute([$article_id, $user_id]);
# 删除评论
$stmt = $pdo->prepare("DELETE FROM comments WHERE article_id = ?");
$stmt->execute([$article_id]);
$message = "文章及相关评论删除成功";
header('Location: user_info.php');
exit();
}
?>
<!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: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 500px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.message {
text-align: center;
margin-bottom: 20px;
color: red;
}
</style>
</head>
<body>
<div class="container">
<h1>删除文章</h1>
<?php if (!empty($message)): ?>
<div class="message"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<form action="delete_article.php?article_id=<?php echo htmlspecialchars($article_id); ?>" method="post">
<div class="form-group">
<p>确定要删除这篇文章及其相关评论吗?</p>
</div>
<input type="submit" value="确认删除">
</form>
</div>
</body>
</html>
五、安全加固部分
这部分的内容主要是根据我之前打过的一些靶场以及之前在编写过程中遇到的问题来进行的,也可以作为一个还未完成的总结——有一些加固已经用上了。
1.防止sql注入
(1)首先就是预处理语句,参数化查询
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?,?)");
$stmt->execute([$username, $hashPassword]);
预编译可以解决绝大部分的sql注入问题,但并不是绝对安全的。
(2)防范存储过程中的注入。
我们这里并不存在存储过程,自然也不存在存储过程的注入。但不免其他的cms中会有存储过程,存储过程中的注入就和基本的sql注入差不多,如果是拼接的,也需要用参数化查询
2.防范XSS注入
上面的代码也只有htmlspecialchars一种防止xss注入的手段
(1)htmlspecialchars :将一些特殊字符转化为HTML实体从而防止XSS注入
(2)防止伪协议。在一些使用a标签href进行链接跳转时我发现可以通过构造伪协议从而造成xss注入:
这里我用waf.php作为加固部分,使用其中的防护函数
既然是伪协议要过滤掉,那就只允许用http以及https协议就行了
<?php
# XSS注入部分
function sanitizeInput($input){
return htmlspecialchars($input, ENT_QUOTES,'UTF-8');
}
function sanitizeUrl($url)
{
$allowedSchemes = ['http', 'https'];
$parsedUrl=parse_url($url);
# 首先检查到底有没有协议,register.php文件就没有协议
if(!isset($parsedUrl['scheme'])){
return htmlspecialchars($url, ENT_QUOTES,'UTF-8');
}
if(in_array($parsedUrl['scheme'], $allowedSchemes)){
return htmlspecialchars($url, ENT_QUOTES,'UTF-8');
}
return '#';
}
然后在a标签处,将a标签换为:
<a href="<?php echo sanitizeUrl('register.php');?>">还没有账号?点击这里注册</a>
<a href="<?php echo sanitizeUrl('comment.php?article_id='.$article_id);?>" class="comment-button">给该文章评论</a>
3.文件上传漏洞
主要在upload_avatar.php和waf.php中修改
(1)白名单设置
# 允许设置的文件类型
$allowed_ext=array('jpg','jpeg','png','gif');
$allowed_mime=array(
'image/jpeg'=>'jpg',
'image/png'=>'png',
'image/gif'=>'gif'
);
但是,即使通过了MIME类型和拓展名的双重验证,攻击者仍可以上传一个精心构造的图片文件(比如shell.php.jpg)来绕过检查,如果其中嵌入了PHP代码,且服务器可能存在某些配置漏洞的话,该文件还是可能被攻击者利用然后将此文件当做PHP执行,对此,有以下解决方法
(2)强制覆盖拓展名+进行二次渲染
我们允许上传的图片类型为jpeg,png,gif 但我们可以统一这些头像文件的后缀,假设jpg是绝对安全的,不会被服务器解析PHP代码的,那我只需要一个最终的“白名单”jpg就行了,其他的头像文件全部解释为jpg文件
# 保存为JPG格式
imagejpeg($image,$dest_path,100); # 保存为jpg格式
imagedestroy($image);
对于文件内容进行二次渲染
switch($file_type){
# 强制将文件内容解析为JPEG\PNG\GIF图片
case 'image/jpeg':
$image=imagecreatefromjpeg($file_tmp_path);
break;
case 'image/png':
$image=imagecreatefrompng($file_tmp_path);
break;
case 'image/gif':
$image=imagecreatefromgif($file_tmp_path);
break;
default:
throw new Exception('非法文件类型');
}
上面两块代码加起来才为二次渲染,先从文件中仅提取图像元素(破坏PHP代码),再生成新的jpg格式文件
(3)重命名文件名
可以将文件名以非常随机的方式命名,大大减少攻击者getshell的可能
$file_new_name=bin2hex(random_bytes(16)).'.'.$new_ext;
(4)限制文件大小类型,防止DOS攻击
# 文件大小限制
$max_size=2*1024*1024;
if($file_size>$max_size){
$_SESSION['message']='文件上传超过2MB限制';
header('Location:user_info.php');
}
(5)使用相对路径存储
如果直接将绝对路径存入数据库,攻击者可能会通过精心构造一个文件名,比如../../../etc/passwd,拼接到我们的绝对路径后从而访问到系统的敏感文件
$upload_file_dir=__DIR__.'/upload/';# __DIR__就是此时的脚本文件目录
(6) 隐藏敏感错误信息,使攻击者难以猜到服务器内部信息
自定义错误消息
(7) 进行日志记录
# 记录日志
$log_msg=sprintf(
"[%s] 用户ID:%d 上传文件:%s IP:%s",
date('Y-m-d H:i:s'),
$user_id,
$file_new_name,
$_SERVER['REMOTE_ADDR']
);
file_put_contents(__DIR__.'/upload_log.txt',$log_msg.PHP_EOL,FILE_APPEND);
4.会话劫持
(1) 设置会话cookie参数
安全的cookie参数可以增强Web应用的安全性和控制会话的行为
# 设置会话 Cookie 参数
session_set_cookie_params([
'lifetime' => 600,
'path' => '/', # 表示会话cookie在整个网站的所有路径下都有效
'domain' => $_SERVER['HTTP_HOST'],# 会话cookie只在当前访问的域名下有效
'secure' => true,# 只通过HTTPS协议传输
'httponly' => true,# js代码无法读取或修改该会话cookie。从而防止XSS通过js来窃取用户的会话ID
'samesite' => 'Lax'
]);
session_start();
同时,由于该操作是在waf.php中进行的,为了代码的通用性,将session_start()加在这里,其他文件就不需要加session_start()了,但是每个开启了会话的文件前都得加require 'waf.php';
(2)会话超时管理
当用户不在活跃状态时,设置会话超时。防止攻击者在用户暂时离开电脑但未退出登录的情况下,直接使用用户的会话进行恶意操作,并可以有效对抗暴力破解攻击,减少数据暴露时间
# 会话超时管理
$sessionTimeout=600;
if(isset($_SESSION['last_activity'])){
$elapsed_time=time()-$_SESSION['last_activity'];
if($elapsed_time>$sessionTimeout){
session_unset();
session_destroy();
die("会话已超时,请重新登录.");
}
}
$_SESSION['last_activity']=time();
(3)用户端指纹验证
如果攻击者窃取到了会话ID登录机制,此时生成的指纹会和被窃取的用户不同
# 生成当前请求的客户端指纹
$current_fingerprint=hash('sha256',$_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR']);
# 用户端指纹验证机制
if(isset($_SESSION['client_fingerprint'])){
if($_SESSION['client_fingerprint']!=$current_fingerprint){
session_unset();
session_destroy();
die("会话异常,请重新登录");
}
}else{
# 如果是首次登录或者会话中还没有存储指纹,则存储当前指纹
$_SESSION['client_fingerprint']=$current_fingerprint;
}
(4)防止会话固定攻击
代码中当检测到用户登录成功(isset($_SESSION['user_id'])
)后,调用 session_regenerate_id(true)
重新生成一个全新的会话 ID。这样一来,攻击者预先知道的那个旧会话 ID 就会失效,无法再用于控制用户的会话,从而有效抵御了会话固定攻击。
这个一般用在登录这个关键节点上:
if(isset($_SESSION['user_id'])){
session_regenerate_id(true);
# 检测到用户已经登录,重新生成会话ID并删除会话文件
}
5. CSRF
(1)CSRF令牌
通过生成一个CSRF令牌来防止攻击者的跨站请求伪造
# CSRF令牌生成
if(empty($_SESSION['csrf_token'])){
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token=$_SESSION['csrf_token'];
# 验证CSRF令牌
if($_SERVER['REQUEST_METHOD'] == 'POST'){
if(!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $csrf_token){
http_response_code(403);
die("CSRF 验证失败,请重新提交请求。");
}
}
然后需要在表单提交中验证CSRF令牌
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'];?>">
这个地方要特别注意,该语句要在submit之前,不然会引起逻辑错误
waf.php
<?php
//——————————XSS注入部分——————————
function sanitizeInput($input){
return htmlspecialchars($input, ENT_QUOTES,'UTF-8');
}
function sanitizeUrl($url)
{
$allowedSchemes = ['http', 'https'];
$parsedUrl=parse_url($url);
# 首先检查到底有没有协议,register.php文件就没有协议
if(!isset($parsedUrl['scheme'])){
return sanitizeInput($url);
}
if(in_array($parsedUrl['scheme'], $allowedSchemes)){
return sanitizeInput($url);
}
return '#';
}
//——————————会话劫持————————————————
# 设置会话 Cookie 参数
session_set_cookie_params([
'lifetime' => 600,
'path' => '/', # 表示会话cookie在整个网站的所有路径下都有效
'domain' => $_SERVER['HTTP_HOST'],# 会话cookie只在当前访问的域名下有效
'httponly' => true,# js代码无法读取或修改该会话cookie。从而防止XSS通过js来窃取用户的会话ID
'secure' => 0,
'samesite' => 'Lax'
]);
session_start();
# 生成当前请求的客户端指纹
$current_fingerprint=hash('sha256',$_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR']);
# 会话超时管理
$sessionTimeout=600;
if(isset($_SESSION['last_activity'])){
$elapsed_time=time()-$_SESSION['last_activity'];
if($elapsed_time>$sessionTimeout){
session_unset();
session_destroy();
die("会话已超时,请重新登录.");
}
}
$_SESSION['last_activity']=time();
# 用户端指纹验证机制
if(isset($_SESSION['client_fingerprint'])){
if($_SESSION['client_fingerprint']!=$current_fingerprint){
session_unset();
session_destroy();
die("会话异常,请重新登录");
}
}else{
# 如果是首次登录或者会话中还没有存储指纹,则存储当前指纹
$_SESSION['client_fingerprint']=$current_fingerprint;
}
//——————————————CSRF——————————————
# CSRF令牌生成
if(empty($_SESSION['csrf_token'])){
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token=$_SESSION['csrf_token'];
# 验证CSRF令牌
if($_SERVER['REQUEST_METHOD'] == 'POST'){
if(!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $csrf_token){
http_response_code(403);
die("CSRF 验证失败,请重新提交请求。");
}
}
upload_avatar.php
<?php
require 'config.php';
session_start();
$pdo=connectToDatabase();
# 检查用户是否已经登录
if(!isset($_SESSION['user_id'])){
die("请先登录");
}
$user_id = $_SESSION['user_id'];
if($_SERVER['REQUEST_METHOD']=='POST'){
if(isset($_FILES['avatar']) && $_FILES['avatar']['error']==UPLOAD_ERR_OK){
$file_tmp_path = $_FILES['avatar']['tmp_name'];
$file_name = $_FILES['avatar']['name'];
$file_size = $_FILES['avatar']['size'];
# 文件大小限制
$max_size=2*1024*1024;
if($file_size>$max_size){
$_SESSION['message']='文件上传超过2MB限制';
header('Location:user_info.php');
}
# 使用更加可靠的MIME检测
$finfo=finfo_open(FILEINFO_MIME_TYPE);
# FILEINFO_MIME_TYPE 表明要获取文章的MIME类型
$file_type=finfo_file($finfo,$file_tmp_path);
finfo_close($finfo);
$file_name_cmps=explode(".",$file_name);
$file_ext=strtolower(end($file_name_cmps));
# 允许设置的文件类型
$allowed_ext=array('jpg','jpeg','png','gif');
$allowed_mime=array(
'image/jpeg'=>'jpg',
'image/png'=>'png',
'image/gif'=>'gif'
);
# 双重验证,拓展名和MIME类型
if(in_array($file_ext,$allowed_ext) && isset($allowed_mime[$file_type])){
# 强制覆盖拓展名为jpg
$new_ext='jpg';
# 二次渲染防御
try{
switch($file_type){
# 强制将文件内容解析为JPEG\PNG\GIF图片
case 'image/jpeg':
$image=imagecreatefromjpeg($file_tmp_path);
break;
case 'image/png':
$image=imagecreatefrompng($file_tmp_path);
break;
case 'image/gif':
$image=imagecreatefromgif($file_tmp_path);
break;
default:
throw new Exception('非法文件类型');
}
# 生成新文件名
$file_new_name=bin2hex(random_bytes(16)).'.'.$new_ext;
$upload_file_dir=__DIR__.'/upload/';# __DIR__就是此时的脚本文件目录
$dest_path=$upload_file_dir.$file_new_name;
# 保存为JPG格式
imagejpeg($image,$dest_path,100);# “图像资源 路径 质量”
imagedestroy($image);
# 记录日志
$log_msg=sprintf(
"[%s] 用户ID:%d 上传文件:%s IP:%s",
date('Y-m-d H:i:s'),
$user_id,
$file_new_name,
$_SERVER['REMOTE_ADDR']
);
file_put_contents(__DIR__.'/upload_log.txt',$log_msg.PHP_EOL,FILE_APPEND);
# 更新数据库
$web_path='upload/'.$file_new_name;
$stmt=$pdo->prepare("UPDATE users SET avatar=? WHERE id=?");
$stmt->execute([$web_path,$user_id]);
$_SESSION['message']='头像更新成功';
}catch(Exception $e){
$_SESSION['message']='文件处理失败,请稍后再试';
}
}else{
$_SESSION['message']='上传失败,文件类型不允许';
}
}else{
$error_code=$_FILES['avatar']['error'] ?? '未知错误';
$_SESSION['message']='上传失败,错误代码: '.$error_code;
}
header('Location:user_info.php');
exit();
}
六、总结
(1)这篇对自己的提升是很大的,譬如在安全加固那一块,以前不太清楚的点(会话劫持、CSRF)也搞懂了一些。同时,对于PHP和MYSQL板块也清晰了很多,了解了一些逻辑关系的实现,比如$_SESSION会话,$pdo连接数据库等(主要是也有点想不起来了,拉得有点长)
(2)然后来看一看网页的实现: