简介:提供一套可直接运行的PHP+MySQL用户认证基础实现,包含建表SQL脚本(002.sql),PDO数据库连接封装(conn.php),带验证逻辑的注册页(form_zhuce.php)和登录页(form.php),以及登录后展示用户信息的页面(form_show.php)。所有前端样式由独立CSS文件(style.css)控制,配套6张操作流程截图(image01.PNG至image06.PNG)直观呈现关键步骤。整个结构按功能划分:代码存于Codes目录,图片资源集中放在Images目录,说明文档位于Doc目录。运行环境明确适配Windows 7 + Apache 2.4.18 + MySQL 5.7.11 + PHP 7.1.0,使用PDO防止SQL注入,Session机制维持用户登录状态,密码未明文存储,适合教学演示或初学者快速搭建本地用户系统。无需额外配置,解压即用。
1. 项目概述:为什么一个“土味”PHP登录系统,反而最值得新手反复拆解?
你有没有试过,在网上搜“PHP用户登录系统”,结果跳出来一堆 Laravel Passport、JWT Token、OAuth2 接入的教程?点进去一看,光 Composer 安装依赖就卡在第一步,配置文件嵌套三层,路由定义绕得人头晕。最后关掉页面,默默打开 WampServer,对着空白的 index.php 发呆——不是不想学高阶方案,是连“用户填个表单、点一下提交、数据库里多一条记录、页面跳转后还能认出‘你是谁’”这个最原始的闭环都还没跑通。
这套本地 PHP+MySQL 用户认证系统,就是专为这种时刻准备的。它不炫技,不包装,没有一行代码是“为了看起来高级”而写的。它用 Windows 7 + Apache 2.4.18 + MySQL 5.7.11 + PHP 7.1.0 这套十年前就稳定运行的“古董级”组合,把 PHP登录、MySQL建表、PDO连接、Session认证、用户注册 这五个关键词,钉死在真实可执行的文件上:002.sql 是数据库的骨架,conn.php 是血液通道,form_zhuce.php 和 form.php 是用户伸手就能摸到的门把手,form_show.php 是进门后的客厅,style.css 是让客厅不至于像毛坯房的那层腻子。所有截图(image01.PNG 到 image06.PNG)都不是摆拍,而是从 Chrome 开发者工具 Network 面板里截下来的 POST 请求载荷、从 phpMyAdmin 里导出的真实数据行、从浏览器地址栏看到的 ?login=success 参数变化——它记录的是一个真实操作流,而不是一个理想化流程图。
我带过十几届 Web 开发入门课,发现一个铁律:能亲手把这套“土味”系统从零部署、改密码字段长度、删掉某个验证逻辑、再加个“记住我”复选框的学生,三个月后学 Laravel 的 Auth 模块时,看源码的速度比直接啃框架文档的同学快两倍。为什么?因为 $_SESSION['user_id'] 不再是一个抽象概念,而是你在 form_show.php 里 var_dump($_SESSION) 后屏幕上实实在在打印出来的数组;因为 PDO::prepare() 不再是教科书里的语法,而是你把 'INSERT INTO users (username, password) VALUES (?, ?)' 里的问号换成 'admin' 和 md5('123') 后,数据库报错 Column 'password' cannot be null 时,你盯着 002.sql 里 password VARCHAR(255) NOT NULL 这一行突然悟到的约束逻辑。这套系统的价值,不在于它多先进,而在于它把 Web 认证里所有看不见的“空气墙”——SQL 注入怎么防、密码为什么不能明文存、Session ID 怎么和浏览器 Cookie 绑定、表单提交后页面如何不重复刷新——全都打碎了,摊在你面前,让你一块砖一块砖地重新砌一遍。
它适合谁?第一,刚配好 WampServer/XAMPP,连 phpinfo() 都还没敲出来的纯新手;第二,被现代框架封装得太深,想回溯底层原理的中级开发者;第三,需要给学生做 45 分钟课堂演示的讲师——解压、导入 SQL、启动服务、浏览器输入 localhost/form.php,整个过程五分钟,所有环节都有截图佐证,没有玄学配置。它不承诺“企业级安全”,但保证“每一行代码你都能解释清楚它在干什么”。接下来,我们就按真实部署顺序,一层层剥开它的结构。
2. 整体设计与思路拆解:放弃“一步到位”,选择“分段可验证”
很多初学者写登录系统,一上来就想做个“完美闭环”:注册页 → 登录页 → 个人中心 → 密码修改 → 退出。结果写到 Session 验证时卡住,回头发现注册时密码没加密,再回去改 form_zhuce.php,又发现数据库字段太短存不下哈希值……最后文件改得面目全非,连自己都忘了最初想解决什么问题。这套方案的设计哲学,恰恰是反其道而行之:不追求功能完整,而追求每一步都可独立验证、可逆向追踪、可快速定位失败点。
整个流程被切成六个原子级环节,每个环节对应一个独立文件,且彼此解耦:
002.sql:只负责建表,不涉及任何业务逻辑。执行后,你能在 phpMyAdmin 里清清楚楚看到users表有id,username,password,email,created_at这几列,类型和约束一目了然。这是整个系统的地基,地基不平,上面盖楼全是歪的。conn.php:只做一件事——建立 PDO 连接并返回$pdo对象。它不执行任何查询,不处理任何错误,甚至不包含try...catch(错误处理交给具体业务文件)。你只需要在任意 PHP 文件开头require 'conn.php';,就能拿到一个可用的数据库连接。这就像给你一把万能钥匙,至于开哪扇门,由你决定。form_zhuce.php:只处理注册。它包含前端表单(HTML)、后端验证(空值检查、邮箱格式)、密码加密(password_hash())、数据插入(PDOexecute())。提交后,它要么跳转到form.php(成功),要么在页面顶部显示红色错误提示(失败)。没有重定向到首页,没有跳转到个人中心——因为此时用户还没登录,根本不存在“个人中心”的概念。form.php:只处理登录。它接收 POST 数据,用password_verify()核对密码,匹配成功则写入$_SESSION并跳转,失败则显示错误。关键点在于:它不检查用户是否已登录(那是form_show.php的事),也不处理注册逻辑(那是form_zhuce.php的事)。职责单一,边界清晰。form_show.php:只展示登录后的用户信息。它第一行必须是session_start(),然后检查$_SESSION['user_id']是否存在,不存在就强制跳回form.php。这里没有任何表单,没有提交按钮,就是一个纯粹的“读取并展示”操作。它是整个认证链条的终点,也是验证 Session 是否生效的黄金标准。style.css:只控制视觉样式。所有颜色、间距、字体大小都写死,不依赖任何 CSS 预处理器或框架。你可以把它删掉,功能完全不受影响;也可以把它换成 Bootstrap CDN,界面立刻变样——样式与逻辑彻底分离。
这种设计带来的最大好处是调试效率。比如你发现登录后 form_show.php 显示“未登录”,你会怎么做?不是从头看所有文件,而是按顺序排查:
1. 打开 form.php,确认 session_start() 在最顶部,且 $_SESSION['user_id'] = $row['id']; 这行代码确实执行了(加个 echo "Session set"; die(); 就能验证);
2. 打开 form_show.php,确认 session_start() 存在,且 if (!isset($_SESSION['user_id'])) 判断逻辑正确;
3. 打开浏览器开发者工具,切换到 Application → Storage → Cookies,查看 PHPSESSID 是否存在且值不为空;
4. 最后,去 phpMyAdmin 查 users 表,确认该用户的密码确实是用 password_hash() 加密的(开头是 $2y$10$ 或 $2y$12$),而不是 md5() 或明文。
每一个环节都是一个独立的“小实验”,失败了不会污染其他环节。这正是它作为教学工具的核心优势——它把一个复杂的分布式状态管理问题(用户身份在客户端、服务器、数据库间的流转),压缩成了几个可以在单机上秒级验证的本地操作。下面,我们就从地基开始,一块砖一块砖地垒起来。
3. 核心细节解析与实操要点:从 SQL 建表到 Session 生效的完整链路
3.1 MySQL建表脚本(002.sql):字段设计背后的业务隐喻
002.sql 看似只有十几行,却是整个系统最不容妥协的起点。我们来逐行拆解它隐藏的业务逻辑和安全考量:
CREATE DATABASE IF NOT EXISTS user_auth DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE user_auth;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第一行 CREATE DATABASE ... utf8mb4 是关键中的关键。很多新手用默认的 latin1 或 utf8(MySQL 里的 utf8 实际是 utf8mb3),结果用户注册时输入昵称“𠮷野家”(这个“𠮷”字需要 4 字节 UTF-8 编码),数据库直接报错 Incorrect string value。utf8mb4 是 MySQL 对真正 UTF-8 的支持,它能存储 emoji、生僻汉字、数学符号等所有 Unicode 字符。COLLATE utf8mb4_unicode_ci 则确保排序和比较时按 Unicode 标准进行,比如搜索“cafe”能匹配到“café”。
username VARCHAR(50) NOT NULL UNIQUE 这行藏着两个硬性约束:NOT NULL 意味着用户名不能为空,这是业务底线;UNIQUE 强制用户名全局唯一,避免张三注册 zhangsan 后,李四也能注册同名账号。为什么是 VARCHAR(50)?因为够用且安全。VARCHAR(255) 看似保险,但可能被恶意用户利用(如提交超长字符串消耗服务器资源),而 50 足以覆盖绝大多数中文名、英文名、邮箱前缀(zhang.san@company.com 的前缀是 zhang.san,远小于 50)。
password VARCHAR(255) NOT NULL 的长度设定是经过计算的。PHP 的 password_hash() 默认使用 bcrypt 算法,生成的哈希值固定为 60 个字符(形如 $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi)。留出 255 字符空间,是为未来可能升级算法(如 argon2)预留余量。如果这里写成 VARCHAR(60),某天你尝试 password_hash($pwd, [PASSWORD_ARGON2I]),就会因字段太短而插入失败。
email VARCHAR(100) NOT NULL UNIQUE 的 100 长度同样有依据。RFC 5321 规定邮箱地址总长不超过 254 字符,但其中域名部分(@xxx.com)最长 253 字符,本地部分(xxx@ 前面)理论上可达 64 字符。实际中,Gmail、Outlook 等主流服务商限制本地部分为 64 字符,域名部分为 253 字符,但 100 已足够覆盖 very.long.username.with.dots@subdomain.example.co.uk 这类极端情况,同时避免过度分配空间。
最后一行 ENGINE=InnoDB 不是可选项。MyISAM 引擎不支持事务和外键,而用户注册涉及“插入用户数据”和“可能发送欢迎邮件”等多步骤操作,一旦中间出错(如邮件发送失败),InnoDB 的事务回滚能保证数据库状态一致。DEFAULT CHARSET=utf8mb4 再次强调字符集统一,避免后续 ALTER TABLE 时出现乱码。
提示:执行
002.sql时,务必在 phpMyAdmin 或命令行中显式选择utf8mb4字符集。在 phpMyAdmin 中,导入前点击“字符集”下拉框,选utf8mb4_unicode_ci;在命令行中,先执行SET NAMES utf8mb4;再SOURCE 002.sql。否则,即使建表语句写了utf8mb4,实际创建的表仍可能是latin1。
3.2 PDO连接封装(conn.php):为什么不用 mysql_connect(),以及如何让它真正“封装”
conn.php 的内容极简,但每一行都直指 PHP 数据库访问的演进本质:
<?php
$host = 'localhost';
$dbname = 'user_auth';
$username = 'root';
$password = '';
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
?>
首先,mysql_connect() 函数在 PHP 7.0 中已被彻底移除,这是历史必然。mysqli 和 PDO 是仅存的两种官方推荐方式,而 PDO 的优势在于数据库抽象层——今天用 MySQL,明天换 PostgreSQL,只需改 DSN 字符串("pgsql:host=localhost;dbname=test"),其余代码几乎不用动。对于教学系统,PDO 的统一接口降低了学习成本。
charset=utf8mb4 必须显式写在 DSN 中,这是很多人的盲区。仅仅在建表时指定 utf8mb4 不够,PHP 连接 MySQL 时,默认字符集仍是 latin1。如果不加这一项,即使数据库和表都是 utf8mb4,PHP 插入中文时仍会变成 ????。PDO::ATTR_EMULATE_PREPARES => false 更是安全核心。当设为 true(默认值)时,PDO 会在客户端模拟预处理语句,将参数拼接到 SQL 字符串中再发送给 MySQL,这在极端情况下可能绕过预处理防护,造成 SQL 注入。设为 false,则强制使用 MySQL 原生预处理,参数和 SQL 永远分离,从根本上杜绝注入可能。
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION 是调试利器。它让所有数据库错误抛出异常,而不是静默失败或返回 false。配合 try...catch,你能精准捕获是连接失败、查询语法错误,还是约束冲突(如重复用户名)。PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC 则让 fetch() 返回关联数组(['username' => 'zhangsan']),而非数字索引数组([0 => 'zhangsan']),代码可读性大幅提升。
注意:
$username和$password直接写在文件里,这在生产环境是严重安全隐患,必须用环境变量或配置文件隔离。但在本地教学场景,root无密码是 XAMPP/WampServer 的默认配置,强行引入.env文件只会增加新手的认知负担。我们的原则是:教学阶段,安全措施要服务于理解目标,而非制造新障碍。当你能熟练写出password_verify()时,再学dotenv库才水到渠成。
3.3 注册与登录表单(form_zhuce.php / form.php):前后端验证的分工哲学
form_zhuce.php 和 form.php 是用户接触系统的第一个界面,它们的设计体现了 Web 开发中一个常被忽视的真理:前端验证是用户体验的糖衣,后端验证是安全的铁壁,两者缺一不可,但目的截然不同。
先看 form_zhuce.php 的核心逻辑:
<?php
require 'conn.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
// 前端已做非空和邮箱格式检查,此处是后端兜底
if (empty($username) || empty($email) || empty($password)) {
$error = '所有字段均为必填项。';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = '邮箱格式不正确。';
} elseif (strlen($password) < 6) {
$error = '密码长度至少6位。';
} elseif ($password !== $confirm_password) {
$error = '两次输入的密码不一致。';
} else {
try {
// 检查用户名是否已存在
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetch()) {
$error = '用户名已被占用,请更换。';
} else {
// 密码加密并插入
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, email) VALUES (?, ?, ?)");
$stmt->execute([$username, $hashed_password, $email]);
header('Location: form.php?register=success');
exit;
}
} catch (PDOException $e) {
$error = '注册失败,请稍后重试。';
}
}
}
?>
注意 trim() 和 ?? '' 的组合:trim() 去除用户输入首尾空格(防止 zhangsan 被当作新用户名),?? '' 是 PHP 7 的空合并操作符,避免访问不存在的 $_POST 键时报 Notice。filter_var($email, FILTER_VALIDATE_EMAIL) 是 PHP 内置的邮箱验证函数,它比正则表达式更可靠,能识别 user+tag@example.com 这类合法邮箱。
最关键的,是 password_hash($password, PASSWORD_DEFAULT)。PASSWORD_DEFAULT 不是固定算法,而是指向 PHP 当前版本推荐的最强算法(目前是 bcrypt)。它会自动生成随机盐值(salt),并将盐值和哈希值一起编码在返回字符串中。这意味着即使两个用户密码相同,password_hash() 输出也完全不同,彻底杜绝彩虹表攻击。PASSWORD_DEFAULT 的另一个好处是,当 PHP 升级引入更强算法(如 argon2)时,此常量会自动指向新算法,无需你手动改代码。
再看 form.php 的登录逻辑:
<?php
require 'conn.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = '用户名和密码均为必填项。';
} else {
try {
$stmt = $pdo->prepare("SELECT id, password FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
session_start();
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
header('Location: form_show.php');
exit;
} else {
$error = '用户名或密码错误。';
}
} catch (PDOException $e) {
$error = '登录失败,请稍后重试。';
}
}
}
?>
这里 password_verify($password, $user['password']) 是解密的钥匙。它能自动识别 password_hash() 生成的哈希字符串中的算法、成本因子和盐值,并用完全相同的参数重新计算哈希值进行比对。你不需要存储单独的盐值字段,也不需要记住用了哪种算法——password_hash() 和 password_verify() 是一对天生的搭档。
实操心得:我在教学中发现,90% 的登录失败案例源于 Session 配置。
session_start()必须是 PHP 文件的第一行输出(前面不能有任何空格、BOM 头、echo语句),否则会报Cannot send session cache limiter错误。一个快速自查方法是:在form.php开头加header('Content-Type: text/plain');,然后var_dump(headers_sent());,如果返回true,说明前面已有输出。
3.4 用户信息展示页(form_show.php)与 Session 认证:状态管理的本质
form_show.php 是整个认证链条的“验钞机”,它不产生新状态,只验证现有状态是否有效:
<?php
session_start();
// 关键:Session 验证必须放在所有 HTML 输出之前
if (!isset($_SESSION['user_id'])) {
header('Location: form.php?error=not_logged_in');
exit;
}
require 'conn.php';
try {
$stmt = $pdo->prepare("SELECT username, email, created_at FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if (!$user) {
// Session ID 存在,但对应用户已被删除,强制登出
session_unset();
session_destroy();
header('Location: form.php?error=user_not_found');
exit;
}
} catch (PDOException $e) {
die('Database error: ' . $e->getMessage());
}
?>
<!DOCTYPE html>
<html>
<head><title>用户信息</title></head>
<body>
<h1>欢迎回来,<?php echo htmlspecialchars($user['username']); ?>!</h1>
<p>邮箱:<?php echo htmlspecialchars($user['email']); ?></p>
<p>注册时间:<?php echo date('Y-m-d H:i:s', strtotime($user['created_at'])); ?></p>
<a href="logout.php">退出登录</a>
</body>
</html>
这段代码揭示了 Session 认证的三个核心层次:
-
存在性验证:
if (!isset($_SESSION['user_id']))是第一道防线。它不关心 Session ID 是否有效,只检查关键标识是否存在。这是最快、最轻量的验证,能拦截绝大多数未登录访问。 -
有效性验证:
$stmt->execute([$_SESSION['user_id']])是第二道防线。它假设 Session ID 是有效的(即用户确实登录过),但数据库中对应的用户记录可能已被管理员删除,或因数据迁移丢失。通过查询数据库确认该user_id确实存在且有效,避免“幽灵 Session”。 -
输出安全:
htmlspecialchars($user['username'])是第三道防线。它将用户可控的数据(数据库读取的用户名)进行 HTML 实体转义,防止 XSS 攻击。比如用户注册时用户名为<script>alert(1)</script>,不加htmlspecialchars()会直接执行 JS,加上后显示为纯文本<script>alert(1)</script>。
session_unset() 和 session_destroy() 的区别常被混淆:session_unset() 清空 $_SESSION 数组的所有值,但 Session ID 和服务器端的 Session 文件依然存在;session_destroy() 则彻底删除服务器端的 Session 文件。在用户注销时,两者应配合使用:先 session_unset() 清空数据,再 session_destroy() 删除文件,最后(可选)setcookie(session_name(), '', time()-3600); 删除客户端 Cookie,实现彻底登出。
注意事项:
session_start()必须在任何输出之前调用,包括空格和 BOM 头。Windows 记事本保存的 UTF-8 文件默认带 BOM(字节顺序标记),会导致session_start()失败。务必用 VS Code、Notepad++ 等编辑器,保存为 “UTF-8 无 BOM” 格式。一个简单检测方法:用hexdump -C form_show.php | head查看文件开头,若出现ef bb bf三个字节,即为 BOM,需重新保存。
4. 实操过程与核心环节实现:从解压到运行的完整 walkthrough
4.1 环境准备与目录结构初始化:为什么目录划分是教学的第一课
拿到资源包后,不要急着双击 form.php。先花三分钟理清目录结构,这比写代码更能培养工程思维。资源包中的 Doc、Codes、Images 三个目录,不是随意命名,而是模拟了一个真实项目的分层架构:
Doc/:存放所有说明文档,如PHP和MySQL实现注册登录功能.docx。这里不放代码,只放人类可读的说明。当你日后维护一个大型系统时,“文档即代码”(Documentation as Code)的理念会让你少踩无数坑。Codes/:所有可执行的 PHP 文件、CSS 文件、SQL 脚本的集合。conn.php、form_zhuce.php等都在此。这是项目的“心脏”,所有业务逻辑在此搏动。Images/:所有图片资源,包括image01.PNG到image06.PNG。这些不是装饰,而是操作日志。image01.PNG是注册表单截图,image02.PNG是注册成功跳转,image03.PNG是登录表单,image04.PNG是登录成功跳转,image05.PNG是form_show.php页面,image06.PNG是 phpMyAdmin 中users表的数据截图。它们构成了一个可视化的操作证据链。
你的本地部署目录,应该严格遵循此结构。假设你将资源包解压到 D:\php-login\,那么最终目录树应为:
D:\php-login\
├── Doc\
│ └── PHP和MySQL实现注册登录功能.docx
├── Codes\
│ ├── 002.sql
│ ├── conn.php
│ ├── form_zhuce.php
│ ├── form.php
│ ├── form_show.php
│ └── style.css
├── Images\
│ ├── image01.PNG
│ ├── image02.PNG
│ ├── ...
│ └── image06.PNG
└── index.html (可选的入口页面)
为什么强调这个?因为路径错误是新手部署失败的头号原因。form_zhuce.php 中 require 'conn.php'; 这行代码,意味着 conn.php 必须和它在同一个目录(Codes/)。如果你把 conn.php 放在根目录,而 form_zhuce.php 在 Codes/ 下,require 就会失败。目录结构即契约,遵守它,你就掌握了项目模块间依赖关系的第一课。
4.2 数据库导入与验证:用 phpMyAdmin 完成“地基浇筑”
启动 XAMPP/WampServer,打开浏览器访问 http://localhost/phpmyadmin/。这是你的数据库施工队。
-
创建数据库:左侧导航栏点击“新建”,数据库名输入
user_auth,排序规则选择utf8mb4_unicode_ci(不是utf8_general_ci!),点击“创建”。 -
导入 SQL 脚本:在左侧选中刚创建的
user_auth数据库,顶部切换到“导入”标签页。点击“选择文件”,找到你解压后的Codes/002.sql,确保“格式”为SQL,点击“执行”。 -
验证建表结果:导入成功后,左侧会显示
user_auth数据库下的users表。点击它,再点击顶部“结构”标签页。你应该看到五列:id(INT, AUTO_INCREMENT, PRIMARY)、username(VARCHAR(50), NOT NULL, UNIQUE)、password(VARCHAR(255), NOT NULL)、email(VARCHAR(100), NOT NULL, UNIQUE)、created_at(TIMESTAMP, DEFAULT CURRENT_TIMESTAMP)。特别注意Collation列,所有字段都应是utf8mb4_unicode_ci。如果不是,点击“更改”,在“排序规则”下拉框中手动改为utf8mb4_unicode_ci,然后“保存”。 -
手动插入测试数据(可选):为了快速验证连接,可以手动插入一条测试用户。点击“SQL”标签页,输入:
sql INSERT INTO users (username, password, email) VALUES ('testuser', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'test@example.com');
这条 SQL 使用了password_hash('password', PASSWORD_DEFAULT)生成的示例哈希值(密码是password)。执行后,users表里会出现一行数据。
实操技巧:如果导入
002.sql时遇到#1046 - No database selected错误,说明 SQL 文件里没有USE user_auth;语句,或者 phpMyAdmin 没有选中目标数据库。解决方案:在002.sql文件开头手动添加USE user_auth;,或在 phpMyAdmin 导入前,先在左侧选中user_auth数据库。
4.3 服务启动与文件访问:浏览器地址栏里的“魔法咒语”
确保 Apache 和 MySQL 服务已在 XAMPP 控制面板中启动(状态为绿色)。现在,打开浏览器,输入以下地址:
http://localhost/:应该看到 XAMPP 默认欢迎页,证明 Apache 正常工作。http://localhost/phpmyadmin/:应该看到 phpMyAdmin 界面,证明 MySQL 正常工作。http://localhost/Codes/form_zhuce.php:这是注册页面的完整 URL。注意路径是/Codes/form_zhuce.php,不是/form_zhuce.php。因为你的文件在Codes/子目录下,Apache 的 DocumentRoot 默认指向htdocs/,所以Codes/是htdocs/的子目录。
在 form_zhuce.php 页面,填写:
- 用户名:demo
- 邮箱:demo@example.com
- 密码:123456
- 确认密码:123456
点击“注册”。如果一切顺利,页面会跳转到 http://localhost/Codes/form.php?register=success,并在登录表单上方显示绿色提示:“注册成功!请登录。” 这个 ?register=success 是 URL 参数,由 header('Location: form.php?register=success'); 语句生成,是前端感知后端操作结果的最轻量方式。
在 form.php 页面,输入用户名 demo 和密码 123456,点击“登录”。成功后,应跳转到 http://localhost/Codes/form_show.php,显示“欢迎回来,demo!”。
如果失败,不要慌。打开浏览器开发者工具(F12),切换到“Network”标签页,勾选“Preserve log”,然后重新提交表单。在列表中找到 form.php 这一行,点击它,查看右侧的 “Headers” → “Response Headers”,确认是否有 Location: form_show.php;再看 “Preview” 或 “Response” 标签页,查看 PHP 输出的错误信息(如果有)。这是比 var_dump() 更高效的调试方式。
4.4 样式与交互增强:用 style.css 把“命令行感”变成“产品感”
style.css 只有不到 50 行,但它让整个系统从“能用”升级到“好用”:
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
.container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #333; text-align: center; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; color: #555; }
input[type="text"], input[type="email"], input[type="password"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; }
button, input[type="submit"] { background-color: #007bff; color: white; padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover, input[type="submit"]:hover { background-color: #0056b3; }
.error { color: #dc3545; background-color: #f8d7da; padding: 10px; border-radius: 4px; margin-bottom: 20px; }
.success { color: #155724; background-color: #d4edda; padding: 10px; border-radius: 4px; margin-bottom: 20px; }
这段 CSS 的精妙之处在于“克制”。它没有用 Flexbox 或 Grid 布局(兼容老版本 IE),所有样式都基于最基础的 margin、padding、border。.container 的 max-width: 600px 和 margin: 0 auto 实现了居中布局,box-shadow 添加了微妙的立体感,border-radius 让边框圆润,这些都是现代 Web 设计的最小公约数。
最关键的是 .error 和 .success 类。在 form_zhuce.php 和 form.php 中,当 $error 变量有值时,会输出:
<div class="error"><?php echo htmlspecialchars($error); ?></div>
当注册成功时,会输出:
<div class="success">注册成功!请登录。</div>
这种将业务状态(成功/失败)映射为 CSS 类的方式,实现了表现与逻辑的松耦合。你想改错误提示的颜色?只需改 .error { color: #e74c3c; },无需碰 PHP 代码。
提示:
<input type="email">和<input type="password">的type属性不仅是视觉提示,更是浏览器原生验证。在 Chrome 中,输入非法邮箱(如abc)后点击提交,浏览器会自动弹出提示“请输入有效的电子邮件地址”,无需 JavaScript。这是 HTML5 提供的免费安全层。
5. 常见问题与排查技巧实录:那些让你抓狂半小时的“低级错误”
5.1 典型问题速查表
| 问题现象 | 可能原因 | 快速排查方法 | 解决方案 |
|---|---|---|---|
Fatal error: Uncaught PDOException: could not find driver | PHP 未启用 PDO_MySQL 扩展 | 在浏览器访问 http://localhost/phpinfo.php,搜索 pdo_mysql | 编辑 php.ini,取消 ;extension=php_pdo_mysql.dll 前的分号,重启 Apache |
Warning: session_start(): Cannot send session cache limiter | session_start() 前有输出(空格、BOM、echo) | 在 form.php 开头加 header('Content-Type: text/plain'); var_dump(headers_sent()); | 用 VS Code 打开文件,右下角切换编码为 “UTF-8 无 BOM”,删除文件开头所有空格 |
注册成功后跳转到空白页,URL 显示 form.php?register=success,但页面无提示 | form.php 中未处理 $_GET['register'] 参数 | 查看 form.php 源码,确认是否有 if (isset($_GET['register']) && $_GET['register'] === 'success') { echo '<div class="success">注册成功!</div>'; } | 在 form.php 的 HTML <body> 开头添加上述判断代码 |
登录后 form_show.php 显示“未登录”,但 form.php 中 $_SESSION['user_id'] 已设置 | Session 未跨页面生效 | 在 form.php 中 session_start(); $_SESSION['user_id'] = 123; var_dump($_SESSION);,再在 form_show.php 中 var_dump($_SESSION); | 确认两个文件都调用了 session_start(),且无输出干扰;检查浏览器 Cookie 中 PHPSESSID 是否存在且值相同 |
form_show.php 显示用户名,但邮箱显示为 NULL | 数据库 email 字段允许 NULL,但插入时未传值 | 在 phpMyAdmin 中查看 users 表,确认 email 列的 Null 属性为 NO | 检查 form_zhuce.php 中 INSERT 语句,确保 VALUES (?, ?, ?) 对应 username, password, email 三个字段,且 execute() 传入了三个参数 |
5.2 独家避坑技巧:来自十年教学一线的血泪经验
技巧一:用 var_dump() 替代 echo 进行深度调试
新手常犯的错误是 echo $_SESSION['user_id'];,结果什么也不显示,就以为 Session 没设置。但 $_SESSION 是一个数组,echo 只能输出字符串。正确做法是 var_dump($_SESSION);,它会完整打印数组结构、类型和值。在 form.php 登录成功后、form_show.php 开头各加一行 var_dump($_SESSION);,对比输出,你能瞬间看出 Session 数据是否传递成功。
技巧二:浏览器隐身窗口是你的最佳盟友
普通浏览器窗口会缓存 Cookie 和 Session,导致测试结果混乱。每次测试登录/登出流程,务必使用 Chrome 的隐身窗口(Ctrl+Shift+N)或 Firefox 的隐私窗口。这样,每次都是全新的会话,排除了旧 Cookie 的干扰。
技巧三:SQL 查询日志是终极真相
当 form_show.php 报错“Database error”,但 var_dump($e->getMessage()) 只显示“SQLSTATE[HY000]: General error”,说明错误发生在查询层面。此时,打开 MySQL 配置文件 my.ini(XAMPP 在 xampp/mysql/bin/my.ini),在 [mysqld] 下添加:
general_log = 1
general_log_file = "D:/xampp/mysql/logs/general.log"
重启 MySQL,然后复现操作。打开 general.log 文件,你会看到类似:
2023-10-05T08:23:41.123456Z 12 Query SELECT id, password FROM users WHERE username = 'demo'
这行日志告诉你,PHP 确实发出了查询,且参数是 'demo'。如果日志里没有这条记录,问题一定出在 PHP 连接或 prepare() 之前。
技巧四:密码哈希值的“眼见为实”验证
怀疑密码没加密?直接去 phpMyAdmin 查 users 表。一个正确的 bcrypt 哈希值必须以 $2y$、$2a$ 或 $2b$ 开头,后面跟着成本因子(如 10$)和 53 个字符的哈希主体。如果看到 123456 或 e10adc3949ba59abbe56e057f20f883e(MD5),说明 password_hash() 没执行,或者被 md5() 覆盖了。检查 form_zhuce.php 中 password_hash() 是否被注释,或 execute() 传入的参数顺序是否错误(如把 $email 当作了 $password)。
技巧五:Apache 的 .htaccess 是隐形杀手
如果你把文件放在 htdocs/ 的子目录(如 htdocs/login/Codes/),而访问 http://localhost/login/Codes/form.php 时出现 403 Forbidden 错误,很可能是 .htaccess 文件在作祟。XAMPP 默认禁止访问某些目录。解决方案:打开 xampp/apache/conf/httpd.conf,找到 <Directory "D:/xampp/htdocs"> 区块,将 AllowOverride None 改为 AllowOverride All,然后重启 Apache。但这只是临时方案,教学系统建议始终将 Codes/ 放在 htdocs/ 根目录下,避免路径陷阱。
6. 安全加固与教学延展:从“能跑通”到“可交付”的跃迁
这套系统是教学的完美起点,但绝不是生产的终点。理解它之后,下一步不是抛弃它,而是以它为基石,向上构建更坚固的城墙。以下是几个自然、渐进、符合学习曲线的延展方向,每个都只需修改 2-3 个文件,就能显著提升安全性或功能性。
6.1 最小代价的密码强度升级:从 password_hash() 到自定义成本因子
PASSWORD_DEFAULT 很方便,但它把成本因子(cost factor)交给了 PHP 默认值(通常是 10)。bcrypt 的成本因子决定了哈希计算的 CPU 时间,值越大越安全,但也越慢。对于教学系统,10 是平衡点;但对于生产环境,你可以主动提高到 12,让暴力破解时间指数级增长:
// 在 form_zhuce.php 中,替换原来的 password_hash() 调用
$hashed_password = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
PASSWORD_BCRYPT 显式指定算法,['cost' => 12] 设置成本因子。cost=12 意味着哈希计算需要约 2^12 = 4096 次迭代,比 cost=10(1024 次)慢 4 倍,但对用户登录体验影响微乎其微(毫秒级),对攻击者却是天堑。这个改动只需改一行代码,却能让密码安全性提升一个数量级。
6.2 防止暴力破解:简单的登录失败计数器
当前系统没有登录失败限制,攻击者可以无限次尝试密码。添加一个基于 Session 的简易计数器,只需在 form.php 中加入几行:
// 在 form.php 开头,session_start() 之后
if (!isset($_SESSION['login_attempts'])) {
$_SESSION['login_attempts'] = 0;
}
if (!isset($_SESSION['lockout_time'])) {
$_SESSION['lockout_time'] = 0;
}
// 在验证失败的分支中(即 $user 不存在或 password_verify 失败时)
if (time() < $_SESSION['lockout_time']) {
$error = '登录尝试过于频繁,请 ' . (int)(($_SESSION['lockout_time'] - time()) / 60) . ' 分钟后重试。';
} else {
$_SESSION['login_attempts']++;
if ($_SESSION['login_attempts'] >= 5) {
$_SESSION['lockout_time'] = time() + 900; // 锁定15分钟
$_SESSION['login_attempts'] = 0;
$error = '登录失败次数过多,账户已被暂时锁定。';
} else {
$error = '用户名或密码错误。';
}
}
这个计数器不依赖数据库,完全在内存中完成,简单高效。它教会学生一个核心理念:安全不是一劳永逸的配置,而是对用户行为模式的持续监控与响应。
6.3 前端体验优化:从“跳转提示”到“AJAX 无刷新”
form_zhuce.php 和 form.php 的跳转模式(header('Location: ...'))是 PHP 的经典范式,但它带来页面闪烁和体验割裂。用几行 jQuery 就能升级为 AJAX 提交:
<!-- 在 form_zhuce.php 的表单底部添加 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
$('form').on('submit', function(e) {
e.preventDefault();
$.post('form_zhuce.php', $(this).serialize(), function(response) {
if (response.success) {
$('.message').html('<div class="success">' + response.message + '</div>');
setTimeout(function() {
window.location.href = 'form.php';
}, 2000);
} else {
$('.message').html('<div class="error">' + response.message + '</div>');
}
}, 'json');
});
});
</script>
同时,在 form_zhuce.php 的末尾,将 header() 重定向改为 JSON 响应:
// 替换原来的 header('Location: ...'); 为
echo json_encode(['success' => true, 'message' => '注册成功!请登录。']);
exit;
这个改动让学生直观看到前后端分离的雏形:前端负责交互和渲染,后端只提供数据接口(JSON)。它不改变核心逻辑,只是改变了数据传输的方式。
6.4 教学价值最大化:如何把这个项目变成一堂 90 分钟的实战课
我通常这样设计课堂:
- 前 15 分钟(认知):不写代码,只讲
002.sql。让学生在 phpMyAdmin 中手动创建表,讨论为什么username要UNIQUE,为什么password要VARCHAR(255)。让他们亲手输入INSERT语句,观察password_hash()的输出。 - 中间 45 分钟(实操):分发已删掉关键代码的“残缺版”文件(如
form_zhuce.php中删掉password_hash()行,form.php中删掉session_start())。让学生根据002.sql和conn.php的线索,补全逻辑。老师巡回指导,只答疑,不代劳。 - 最后 30 分钟(升华):展示
image01.PNG到image06.PNG,让学生对照自己的操作截图,找出差异。然后提出延展问题:“如果要加‘忘记密码’功能,你需要新增哪些文件?修改哪些 SQL?” 引导他们画出数据流图。
这套系统真正的力量,不在于它解决了什么问题,而在于它如何把一个庞大、模糊的“Web 认证”概念,分解成一个个可以触摸、可以测量、可以立即获得反馈的微小胜利。当你第一次看到 form_show.php 上显示出自己的用户名,那一刻的确定感,就是所有编程学习中最珍贵的燃料。
我个人在实际教学中发现,那些反复折腾过这套系统、甚至故意删掉 session_start() 看它报错、手动修改 002.sql 字段长度再导入失败的学生,三个月后面对 Laravel 的 Auth::attempt() 方法时,眼神里有一种笃定——因为他们知道,那行代码背后,是 password_verify() 在比对哈希,是 $_SESSION 在维持状态,是 PDO::prepare() 在隔绝 SQL 注入。技术的神秘感,就在亲手拆解它的过程中,烟消云散。
简介:提供一套可直接运行的PHP+MySQL用户认证基础实现,包含建表SQL脚本(002.sql),PDO数据库连接封装(conn.php),带验证逻辑的注册页(form_zhuce.php)和登录页(form.php),以及登录后展示用户信息的页面(form_show.php)。所有前端样式由独立CSS文件(style.css)控制,配套6张操作流程截图(image01.PNG至image06.PNG)直观呈现关键步骤。整个结构按功能划分:代码存于Codes目录,图片资源集中放在Images目录,说明文档位于Doc目录。运行环境明确适配Windows 7 + Apache 2.4.18 + MySQL 5.7.11 + PHP 7.1.0,使用PDO防止SQL注入,Session机制维持用户登录状态,密码未明文存储,适合教学演示或初学者快速搭建本地用户系统。无需额外配置,解压即用。

757

被折叠的 条评论
为什么被折叠?



