最近闲得蛋疼,突然想搞个同学录网站怀旧一下。结果发现网上那些现成的要么收费,要么丑得像我高中班主任的地中海发型。得,自己动手丰衣足食,抄起键盘就开始撸代码。现在把这段充满bug的旅程记录下来,给后来者当个反面教材也好。
先说下技术选型这个坑。本来想用Python装个逼,结果发现虚拟主机只支持PHP。行,PHP就PHP,反正这东西写起来跟写作文似的,就是容易写出"屎山"。数据库用的MySQL,别问为什么不用MongoDB,问就是穷。
先来看用户表结构,这个设计让我掉了不少头发:
CREATE TABLE users
(
id
int(11) NOT NULL AUTO_INCREMENT,
username
varchar(50) NOT NULL,
password
char(60) NOT NULL COMMENT '用password_hash加密',
real_name
varchar(20) DEFAULT NULL,
class_id
int(11) DEFAULT NULL,
avatar
varchar(255) DEFAULT 'default.jpg',
graduate_year
year(4) DEFAULT NULL,
created_at
timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (id
),
UNIQUE KEY username
(username
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里有个坑,密码字段长度一定要设60,因为password_hash出来的字符串固定60个字符。别问我怎么知道的,我当初设了50,结果用户注册时数据库默默把密码截断了,登录时永远提示密码错误,debug到怀疑人生。
登录验证这块我用了最朴素的session方案:
session_start();
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = trim($_POST['username']);
$password = $_POST['password'];
$stmt = $pdo->prepare("SELECT id, password FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
header('Location: /home.php');
exit;
} else {
$error = "用户名或密码错误";
}
}
注意那个trim(),有用户反映用户名后面加空格登录不了。这让我想起当年暗恋的班花,她名字后面也总是跟着一群追求者,像极了字符串后面的空格。
同学录的核心功能当然是留言板。这个看似简单的功能让我踩了三个大坑:
1. XSS攻击:刚开始直接echo用户输入,结果有个调皮的同学写了段js代码,把页面搞得跟迪厅似的闪个不停
2. SQL注入:最早用字符串拼接SQL,直到有人留言内容里带了个单引号,整个站就嗝屁了
3. 表情符号支持:00后学弟学妹们不用emoji会死,MySQL的utf8居然存不了,得用utf8mb4
修正后的留言插入代码长这样:
$content = htmlspecialchars($_POST['content'], ENT_QUOTES);
$stmt = $pdo->prepare("INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, NOW())");
$stmt->execute([$_SESSION['user_id'], $content]);
照片上传功能堪称血泪史的高潮部分。我天真地以为move_uploaded_file()就完事了,结果:
有同学上传了10MB的毕业照,直接把服务器撑爆
有人传了.php文件,差点把我服务器变成肉鸡
图片重名直接覆盖,引发多起"你凭什么删我照片"的战争
最终的上传代码臃肿得像过年回家的行李箱:
$allowed_types = ['image/jpeg', 'image/png'];
$max_size = 2 1024 1024; // 2MB
if (in_array($_FILES['photo']['type'], $allowed_types) &&
$_FILES['photo']['size'] <= $max_size) {
$ext = pathinfo($_FILES['photo']['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $ext;
$path = 'uploads/' . $filename;
if (move_uploaded_file($_FILES['photo']['tmp_name'], $path)) {
// 更新用户头像
$stmt = $pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?");
$stmt->execute([$filename, $_SESSION['user_id']]);
}
分页功能让我深刻理解了什么叫"理想很丰满,现实很骨感"。第一版分页我写了50行代码,后来发现用SQL的LIMIT和OFFSET就能解决:
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$per_page = 10;
$offset = ($page - 1) $per_page;
$stmt = $pdo->prepare("SELECT FROM messages ORDER BY id DESC LIMIT ? OFFSET ?");
$stmt->execute([$per_page, $offset]);
但是!这里有个巨坑:MySQL的预处理语句对LIMIT参数的处理很奇葩,得改成这样:
$stmt->bindValue(1, $offset, PDO::PARAM_INT);
$stmt->bindValue(2, $per_page, PDO::PARAM_INT);
$stmt->execute();
搜索功能本来想用LIKE糊弄过去,直到有人搜"张",结果把所有带"张"字的留言都搜出来了,包括"紧张"、"开张"、"嚣张"。最后被迫上了全文索引:
ALTER TABLE messages ADD FULLTEXT INDEX ft_content
(content
);
$keyword = $_GET['q'];
$stmt = $pdo->prepare("SELECT * FROM messages WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)");
$stmt->execute([$keyword]);
部署时遇到的坑够写本《悲惨世界》续集:
1. 文件权限问题:上传目录要755,我设成777后被警告存在安全风险
2. PHP版本问题:本地用7.4开发,服务器是5.6,password_hash函数直接罢工
3. 时区问题:所有时间显示"现在",原来是忘了设date_default_timezone_set
最后的最后,给后来者几个忠告:
永远不要相信用户输入,过滤、验证、转义三件套不能少
错误日志要开,但别把错误信息直接展示给用户
数据库操作一定要用预处理语句
上传文件要限制类型和大小
PHP版本最好用7.0以上,别学我用5.6自虐
这个同学录网站现在跑得还算稳当,虽然界面丑得像90年代的QQ空间。但有什么关系,就像我们班当年的毕业照,再丑也是青春。代码虽然糙,但好歹是自己一砖一瓦垒起来的,比那些花里胡哨的框架更有温度。