PHP博客管理系统开发全记录

包含Linux运维/数据库设计/用户权限/安全维护…

本项目是Web安全课的课程设计项目,以php语言不依托任何后端框架开发的一个博客管理系统,环境为Ubuntu 16.04TLS + Apache2 + Mysql。实现了基本的博客功能:注册、登录、发表博客、评论、管理博客、封禁用户(管理员);完善了安全防护措施:可防御SQL注入、XSS攻击、越权攻击、暴力破解;全站使用markdown 编辑器和解析器,主页显示文章分类、最新文章以及博文的预览,进行了简单的搜索引擎优化和移动端美化。

Github地址

线上演示地址(暂时开放)

本项目与@zzzskd 共同开发,且其代码比重更大。

服务端LAMP环境配置、运行与维护

  • Ubuntu 16.04 LTS
  • Apache2
  • Mysql
  • php7.0

Ubuntu(Linux运维)

查看已建立的网络连接以及对应进程

netstat -antulp | grep EST

iptables封杀某个IP或者ip段,如:123.4.5.6

iptables -I INPUT -s 123.4.5.6 -j DROP
iptables -I INPUT -s 123.4.5.1/24 -j DROP

find / *.php -perm 4777  //查找777的权限的php文件

awk -F: '{if($3==0)print $1}' /etc/passwd  //查看root权限的账号

查看页面访问排名前十的IP

cat /var/log/apache2/access.log | cut -f1 -d " " | sort | uniq -c | sort -k 1 -r | head -10

查看页面访问排名前十的URL

cat /var/log/apache2/access.log | cut -f4 -d " " | sort | uniq -c | sort -k 1 -r | head -10

备份Web目录:

tar -zcvf web.tar.gz /var/www/html/

Mysql

sudo apt-get install mysql-server

关闭Mysql外联:

UPDATE mysql.user SET HOST = "localhost" WHERE USER = "root";
FLUSH PRIVILEGES;

备份mysql数据库:

mysqldump -u 用户名 -p 密码 数据库名 > back.sql  //备份指定数据库
mysqldump --all-databases > back.sql    //备份所有数据库

还原mysql数据库:

mysql -u 用户名 -p 密码 数据库名 < back.sql

避免使用弱密码登录。

Apache2

sudo apt-get install apache2

关闭列目录:

配置/etc/apache2/apache2.conf

<Directory /var/www/>
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

去除Indexes即关闭列目录。

要善于用Apache日志(/var/log/apache2/error.log)解决网站报错、发现用户恶意行为。

如图有人在恶意扫描我们的Web目录:

php7

sudo apt-get install php7.0

安装php7 module进行解析

sudo apt-get install libapache2-mod-php7.0

数据库的设计与操作

数据库的建立

CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;# 设置字符集,避免乱码

表的建立

用户表

CREATE TABLE users(
    user_id INT(10) UNSIGNED AUTO_INCREMENT,
    user_name VARCHAR(50) NOT NULL UNIQUE,              # user_name 不能重复
    user_pass VARCHAR(50) NOT NULL,
    user_email VARCHAR(50) NOT NULL UNIQUE,
    user_telno VARCHAR(20) UNIQUE,
    user_sex ENUM('男','女')  DEFAULT '男',
    user_birthday DATE NOT NULL DEFAULT '0000-01-01',
    user_register_datetime DATETIME NOT NULL,
    user_image_url VARCHAR(255) NOT NULL DEFAULT '',    # 用户头像地址 只有注册成功后才能设置
    user_description VARCHAR(255) NOT NULL DEFAULT '',  # 用户自我描述
    user_type TINYINT(3) NOT NULL DEFAULT '0',          # 0 为普通用户,1 为管理员
    user_lock TINYINT(3) NOT NULL DEFAULT '0',          # 用户违规,则锁定
    PRIMARY KEY (user_id),
    INDEX users_namepass(user_name, user_pass, user_lock)            # 建立索引,加快搜索速度
)ENGINE=InnoDB DEFAULT CHARSET=utf8 ;                   # 设置引擎和字符集

文章类别表 系统默认 用户不能设置

CREATE TABLE categories(
    category_id INT(10) UNSIGNED AUTO_INCREMENT,
    category_name VARCHAR(50) NOT NULL, 
    PRIMARY KEY (category_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

文章表

CREATE TABLE articles(
    article_id INT(10) UNSIGNED AUTO_INCREMENT,
    article_user_id INT(10) UNSIGNED NOT NULL,
    article_title VARCHAR(50) NOT NULL,
    article_details TEXT NOT NULL,
    article_post_datetime DATETIME NOT NULL,
    article_modify_datetime DATETIME NOT NULL,
    article_category_id INT(10) UNSIGNED NOT NULL,
    article_excerpt VARCHAR(70) NOT NULL,
    article_click INT(10) NOT NULL DEFAULT '0',
    PRIMARY KEY (article_id),
    FOREIGN KEY (article_user_id) REFERENCES users(user_id), # 外键 
    FOREIGN KEY (article_category_id) REFERENCES categories(category_id) # 外键
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

评论功能数据库设计

CREATE TABLE comments(
    comment_id INT(10) UNSIGNED AUTO_INCREMENT,
    comment_user_id INT(10) UNSIGNED NOT NULL,
    comment_article_id INT(10) UNSIGNED NOT NULL,
    comment_details TEXT NOT NULL,
    comment_post_datetime DATETIME NOT NULL,
    PRIMARY KEY (comment_id),
    FOREIGN KEY (comment_user_id) REFERENCES users(user_id), # 外键 
    FOREIGN KEY (comment_article_id) REFERENCES articles(article_id) # 外键
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

文章与评论是一对多的关系。

表中的插入数据范例

用户表中插入数据

INSERT INTO users (user_name, user_pass, user_email, user_regiter_datetime) VALUES ('zzz', 'zzz', '196413@qq.com', now());

now() => YYYY-MM-DD HH:MM:SS

向类别表中插入数据

INSERT INTO categories (category_name) VALUES ('程序人生');

向文章表中插入数据

INSERT INTO articles (article_user_id, article_title, article_details, article_post_datetime, article_modify_datetime, article_category_id, article_excerpt) VALUES ('1', 'test', 'this is a test', now(), now(), '1', 'this is an excerpt');

ALTER TABLE articles ADD article_click INT(10) NOT NULL DEFAULT '0' AFTER article_excerpt; # 修改文章表,添加属性 浏览数目(在article_excerpt之后添加)

修改详情:

1 只有 InnoDB 引擎支持外键限制,而 MyISMA 引擎不能支持外键限制,因此将三个表的引擎换为 InnoDB(ALTER TABLE users ENGINE=innodb;)
2 由于外键要求两表中的类型必须一致,因此将 article_category_id INT(10) UNSIGNED NOT NULLarticle_user_id INT(10) UNSIGNED NOT NULL 添加 UNSIGNED 限定

数据库的连接

connect.php组件,代码复用:

$mysqli = new mysqli("localhost", "root", "root", "blog");
    $mysqli -> set_charset('utf8'); # 设定字符集
    if($mysqli -> connect_errno){
        die('连接失败:'.$mysqli -> connect_error);

数据库的插入

  • 在注册页面中(./register.php)有用户表的插入,其插入命令为
INSERT INTO users (user_name, user_pass, user_email, user_register_datetime) VALUES ('$username',md5('$password'),'$email', now())";
        $sql = "INSERT INTO users (user_name, user_pass, user_email)VALUES(?,?,?)
  • 在文章发表页面(./mdeditor.php)的文章表的插入,其插入命令为
INSERT INTO articles (article_user_id, article_title, article_details, article_category_id, article_post_datetime, article_modify_datetime,article_excerpt
) VALUES ('$userid','$articletitle','$articledetails', '$articlecategoryid', now(), now(),'$articleexcerpt')
  • 类别表的插入,类别表只有超级管理员(网站所有者)才能插入,命令为 INSERT INTO categories (category_name) VALUES ('category_name');

数据库的查询

查询命令是最多的,有:

  • 首页(./index.php) 中 文章粗略的查询命令,按时间倒序排序:

$query = "SELECT user_id, user_name, article_id, article_title, article_post_datetime, article_category_id, category_name, article_excerpt, article_click FROM users, articles, categories WHERE article_user_id=user_id AND article_category_id=category_id ORDER BY article_post_datetime DESC";

  • ./detail.php 中,文章详细的查询…

  • ./delete.php 中,按照 联合 session 中user_id的查询

$sql_statement = "SELECT article_user_id, article_id, article_title, article_post_datetime, category_name, article_excerpt, article_click FROM articles, categories WHERE article_id='" . $_GET['articleid'] ."'". " AND category_id=article_category_id";

  • ./auther.php、./category.php 中按作者、类别的查询…

  • ./login.php 中的查询…

  • ./mdeditor.php 中,查询文章类别…

  • ./search.php 中,模糊查询命令:

$query = "SELECT user_id, user_name, article_id, article_title, article_post_datetime, article_category_id, category_name, article_excerpt, article_click FROM users, articles, categories WHERE article_user_id=user_id AND article_category_id=category_id AND (article_title LIKE '%$q%' OR article_excerpt LIKE '%$q%' OR article_details LIKE '%$q%') ORDER BY article_post_datetime DESC";

数据库的更新(修改)

数据库更新有以下几部分:

  • 文章详情页中(./detail.php)中,文章点击数更新:

$sql_statement = "UPDATE articles SET article_click=article_click+'1' WHERE article_id = '" . $_GET['articleid'] ."'";

  • 管理员封禁、解封用户页面(./includes/ban.php, ./includes/restore.php)中,user_lock 字段更新:

$sql_statement = "UPDATE users SET user_lock='1' WHERE user_id= '" . $_GET['userid'] ."'";

$sql_statement = "UPDATE users SET user_lock='0' WHERE user_id= '" . $_GET['userid'] ."'";

数据库的删除

./includes/deleteresult.php

$sql_statement = "DELETE FROM articles WHERE article_id='" . $_GET['articleid'] ."'";

服务端运维安全

包含了

  • Mysql关闭外联,备份与恢复,避免弱密码
  • Apache关闭列目录,关注Apache日志
  • Linux进程 端口占用 网络连接 iptables防火墙
  • Web目录备份

以上见《服务端LAMP环境配置、运行与维护》

代码安全

主要防止SQL注入和XSS漏洞,防止了简单的越权漏洞,防止暴力破解攻击。遵循一个原则:不相信用户的任何输入。

确认用户session防止越权

大量使用了 $_SESSION 中数据,确定是否为管理员在进行操作,主要有 ./manageuser.php、 ./delete.php、 ./requires/ban.php、 ./requires/restore.php

防SQL注入

主要分为确认参数安全,大体分为查询时的安全和插入时的安全,提交参数主要分为两类:

参数为 int 型

主要出现在查看文章的URL处
http://47.101.56.247/detail.php?articleid=16
此时参数应该为int整形,所以没必要使用复杂的过滤,直接使用 is_numeric() 函数判断即可,如果数据不符合或未提交完成参数, PHP页面返回错误,不进行数据库的操作。

# 如果未带参数跳转则返回首页 
if(!isset($_GET['articleid']) || !$_GET['articleid'] || !is_numeric($_GET['articleid'])){
    header("Location:index.php");
}

参数为 字符串 型

这里分为两种,注册、登录、修改密码时参数 和 文章内容参数的安全:

注册、登录、修改密码时参数:

  • 去除空白符(trim()函数);
  • 在登录和注册页增加SQL过滤函数:

  • 最后使用 htmlspecialchars(addslashes($yourworld),ENT_QUOTES,'UTF-8')处理后存入数据库;
  • 文章内容 和 搜索页面(search.php) 的参数处理 ,直接使用 htmlspecialchars(addslashes($yourworld),ENT_QUOTES,'UTF-8') 参数进行处理。

防止(一般姿势的)XSS攻击

对尖括号<>使用htmlspecialchars()进行转义后存储。

$articletitle = htmlspecialchars(addslashes($articletitle),ENT_QUOTES,'UTF-8');
$articledetails = htmlspecialchars(addslashes($articledetails),ENT_QUOTES,'UTF-8');
字符 替换后
< (小于) &lt;
/> (大于) &gt;

效果验证:

文章标题及详情处不存在XSS;

评论处不存在XSS。

登录验证码

小时候我一直以为,验证码是网络的另一端有人在肉眼比对,现在我明白不用了,因为它本来就是由我生成的:

$str = Array(); 
$string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for($i = 0;$i < 4;$i++){
   $str[$i] = $string[rand(0,35)];
   $vcode .= $str[$i];
}

之后就是图像处理的部分了,这一部分剽窃了他人的成果。

if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['verify'])){
      if($_POST['verify'] == $_SESSION['vcode']){

传递变量,校验即可。

防止弱口令登录

在注册处添加一句正则判断:

前端加placeholder提示用户,增强体验:

杜绝密码明文存储

单次md5的加密现在已经比较脆弱了,可以考虑加salt或其它加密算法。

网站备份

利用Linux的例行性排程(crontab)定时备份数据库文件与网站:

.sh 脚本编写

#!/bin/bash
# /backup/backupday.sh
# =========================================================
# 备份数据的目录
basedir=/backup/daily/
# =========================================================
PATH=/bin:/usr/bin:/sbin:/usr/sbin; export PATH
export LANG=C
basefile1=$basedir/mysql.$(date +%Y-%m-%d).tar.bz2
basefile2=$basedir/html.$(date +%Y-%m-%d).tar.bz2
[ ! -d "$basedir" ] && mkdir $basedir
# 为了防止mysql读写错误(备份的时候有人写入数据库,备份前关闭服务)
systemctl stop mysql.service
systemctl stop apache2.service
# 1. MysQL (数据库目录在 /var/lib/mysql)
cd /var/lib
 tar -jpc -f $basefile1 mysql
# 2. 
cd /var/www
 tar -jpc -f $basefile2 html
# 恢复 
systemctl start mysql.service
systemctl start apache2.service

设置权限脚本权限

chmod 700 /backup/backupwk.sh

添加到例行性排程中

每天2:30 进行备份

vim /etc/crontab
# 加入两行
30 2 * * * root /backup/backupday.sh

权限管理

用户表在创建时区分了管理员和普通用户,区分了被封禁与未被封禁的用户:

user_type TINYINT(3) NOT NULL DEFAULT '0',  # 0 为普通用户,1 为管理员
user_lock TINYINT(3) NOT NULL DEFAULT '0',  # 用户违规,则锁定

普通用户在登录状态下可以发表文章及删除自己发表的文章,管理员在登录状态下可以删除任意文章以及封禁用户

进行发表/删除文章/用户时始终先判断登录状态和当前权限:

session相关

session创建

session在login.php中创建,设置的变量有用户名、登录状态、用户类型和用户id。同时将验证码$vcode的值也写进了session中进行比对。

$_SESSION["username"] = $_POST["username"];
$_SESSION["isLogin"] = 1;
$_SESSION["usertype"] = $usertype;
$_SESSION["userid"] = $id;

session回收

session在logout.php中进行销毁session_destroy(),因为print_r($_COOKIE)发现cookie中有session的相关信息,所以在注销时将cookie一并清理

<?php 
session_start();
$_SESSION = array();
if(isset($_COOKIE[session_name()])){
    setcookie(session_name(),'', time()-42000, '/');
}
session_destroy();
echo '注销成功';
header('refresh:1;url=./index.php');

session存放位置

经过序列化后的session数据存放在/var/lib/php/sessions

线上调试、其他特性优化

返回HTTP错误状态码(5xx)

5xx的HTTP状态码代表服务器错误,此时要学会利用Apache日志(/var/log/apache2/error.log)进行分析排错,这种情况大多是PHP语法错误:

网页显示错乱/不全

大多情况下是css js HTML的 编写错误,如引用缺失等。使用F12开发者工具可以很清楚地定位到问题,或者查看源代码。

一般排错

类似于编程中的“单步调试法”,一步一步跟踪程序执行的流程,根据变量的值,找到错误的原因。在Web系统的调试过程中,将COOKIE、SESSION或各种变量用echo,print_r打印出来,确定值的变化进行排错。

另:Session值存在/var/lib/php/session中,必要时可以直接查看session文件内容。


搜索引擎优化(SEO)

进行META标签优化,在detail.php中添加

<meta name="description" content="<?php echo $articleexcerpt; ?>">

代码复用

为了减少冗余的代码,方便管理,/incluede下的header.php、articleheader.php、connect.php等“组件”都是一次写好多处使用。如connect.php负责与数据库的连接操作,写好这一个组件,之后与数据库连接时只需要添加

require './includes/connect.php';

即可;header.php中包含了Web页面顶部的LOGO、用户登入登出、搜索框的代码实现,在其它页面php中也仅需要包含过去。类似的,每个页面右侧显示的最新文章、文章分类依赖于pageright.php;页面移动端的显示效果依赖于mobile.php。

美化(ico、移动端)

  • 网站图标:

<link rel="shortcut icon" href="./images/favicon.ico">

  • 添加了移动端模板,适配手机端页面,登录注册搜索等功能都隐藏在了弹出式的js中,便于使用。

其他细节

  • 在detail.php和search.php对网页title进行设置:

<title><?p echo $key; ?></title>

进行了优化,提升用户体验。

  • 顶部栏(header.php)、侧边栏(pageright.php)使用户能方便跳转页面

  • 阅读数量显示,每点击一次阅读数+1
if($result -> num_rows == 1){
        # 文章存在则浏览数加一
        $result -> close();
        $sql_statement = "UPDATE articles SET article_click=article_click+'1' WHERE article_id = '" . $_GET['articleid'] ."'";
        $result = $mysqli -> prepare($sql_statement);
        $result -> execute();
        $result -> close();
    }
文章目录
  1. 1. 服务端LAMP环境配置、运行与维护
    1. 1.1. Ubuntu(Linux运维)
    2. 1.2. Mysql
    3. 1.3. Apache2
    4. 1.4. php7
  2. 2. 数据库的设计与操作
    1. 2.1. 数据库的建立
    2. 2.2. 表的建立
      1. 2.2.1. 用户表
      2. 2.2.2. 文章类别表 系统默认 用户不能设置
      3. 2.2.3. 文章表
      4. 2.2.4. 评论功能数据库设计
    3. 2.3. 表中的插入数据范例
      1. 2.3.1. 用户表中插入数据
      2. 2.3.2. 向类别表中插入数据
      3. 2.3.3. 向文章表中插入数据
    4. 2.4. 数据库的连接
    5. 2.5. 数据库的插入
    6. 2.6. 数据库的查询
    7. 2.7. 数据库的更新(修改)
    8. 2.8. 数据库的删除
  3. 3. 服务端运维安全
  4. 4. 代码安全
    1. 4.1. 确认用户session防止越权
    2. 4.2. 防SQL注入
      1. 4.2.1. 参数为 int 型
      2. 4.2.2. 参数为 字符串 型
    3. 4.3. 防止(一般姿势的)XSS攻击
    4. 4.4. 登录验证码
    5. 4.5. 防止弱口令登录
    6. 4.6. 杜绝密码明文存储
  5. 5. 网站备份
    1. 5.0.1. .sh 脚本编写
    2. 5.0.2. 设置权限脚本权限
    3. 5.0.3. 添加到例行性排程中
  • 6. 权限管理
  • 7. session相关
    1. 7.1. session创建
    2. 7.2. session回收
    3. 7.3. session存放位置
  • 8. 线上调试、其他特性优化
    1. 8.1. 返回HTTP错误状态码(5xx)
    2. 8.2. 网页显示错乱/不全
    3. 8.3. 一般排错
    4. 8.4. 搜索引擎优化(SEO)
    5. 8.5. 代码复用
    6. 8.6. 美化(ico、移动端)
    7. 8.7. 其他细节
  • |