用户注册登录系统

项目概述

用户注册登录系统是Web应用的基础组件,几乎所有的Web应用都需要用户认证功能。这个项目将综合运用前面所学的PHP知识,包括数据库操作、表单处理、会话管理、安全防护等。

项目目标

完成本项目后,你将能够:

  • 掌握完整的用户认证系统开发
  • 理解MVC架构的实际应用
  • 实现安全的密码存储和验证
  • 处理表单验证和错误处理
  • 实现会话管理和权限控制
  • 学会邮件发送功能
  • 掌握安全编码实践

功能特性

核心功能

  • 用户注册(邮箱验证)
  • 用户登录(记住登录状态)
  • 密码重置(通过邮件)
  • 用户资料管理
  • 登出功能

安全特性

  • 密码哈希存储
  • SQL注入防护
  • XSS攻击防护
  • CSRF防护
  • 登录失败锁定
  • 会话安全

系统架构

目录结构

auth-system/
├── public/                    # Web根目录
│   ├── index.php            # 入口文件
│   ├── css/
│   │   └── style.css         # 样式文件
│   ├── js/
│   │   └── script.js         # JavaScript文件
│   └── uploads/              # 上传文件
├── src/                      # 源代码
│   ├── config/
│   │   ├── database.php      # 数据库配置
│   │   ├── app.php           # 应用配置
│   │   └── constants.php     # 常量定义
│   ├── controllers/
│   │   ├── BaseController.php
│   │   ├── AuthController.php
│   │   └── UserController.php
│   ├── models/
│   │   ├── Database.php       # 数据库基类
│   │   ├── User.php           # 用户模型
│   │   ├── Auth.php           # 认证模型
│   │   └── PasswordReset.php  # 密码重置模型
│   ├── views/
│   │   ├── layouts/
│   │   │   ├── header.php
│   │   │   └── footer.php
│   │   ├── auth/
│   │   │   ├── login.php
│   │   │   ├── register.php
│   │   │   └── reset.php
│   │   └── user/
│   │       ├── dashboard.php
│   │       ├── profile.php
│   │       └── edit.php
│   └── helpers/
│       ├── Validator.php     # 验证助手
│       ├── Session.php       # 会话助手
│       ├── Mailer.php        # 邮件助手
│       └── Security.php      # 安全助手
├── database/
│   ├── create_tables.sql      # 建表SQL
│   └── seed_data.sql         # 测试数据
├── logs/                      # 日志目录
├── README.md
└── .htaccess                  # Apache配置

数据库设计

1. 创建数据库表

-- database/create_tables.sql

-- 用户表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email_verified BOOLEAN DEFAULT FALSE,
    email_verification_token VARCHAR(255),
    status ENUM('active', 'inactive', 'suspended') DEFAULT 'inactive',
    role ENUM('user', 'admin') DEFAULT 'user',
    failed_attempts INT DEFAULT 0,
    locked_until TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    last_login_at TIMESTAMP NULL
);

-- 用户资料表
CREATE TABLE user_profiles (
    user_id INT PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    phone VARCHAR(20),
    avatar VARCHAR(255),
    bio TEXT,
    birth_date DATE,
    gender ENUM('male', 'female', 'other'),
    website VARCHAR(255),
    location VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 密码重置表
CREATE TABLE password_resets (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    token VARCHAR(255) NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    used BOOLEAN DEFAULT FALSE,
    ip_address VARCHAR(45),
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 登录日志表
CREATE TABLE login_logs (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    ip_address VARCHAR(45) NOT NULL,
    user_agent TEXT,
    success BOOLEAN NOT NULL,
    failure_reason VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);

-- 会话表
CREATE TABLE user_sessions (
    id INT PRIMARY KEY AUTO_INCREMENT,
    session_id VARCHAR(255) UNIQUE NOT NULL,
    user_id INT NOT NULL,
    ip_address VARCHAR(45) NOT NULL,
    user_agent TEXT,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 创建索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_password_resets_token ON password_resets(token);
CREATE INDEX idx_password_resets_expires ON password_resets(expires_at);
CREATE INDEX idx_login_logs_user_id ON login_logs(user_id);
CREATE INDEX idx_login_logs_created_at ON login_logs(created_at);
CREATE INDEX idx_user_sessions_session_id ON user_sessions(session_id);
CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at);

核心代码实现

1. 配置文件

数据库配置 (src/config/database.php)

<?php
class Database {
    private static $instance = null;
    private $pdo;
    private $host = 'localhost';
    private $dbname = 'auth_system';
    private $username = 'root';
    private $password = '';
    private $charset = 'utf8mb4';

    private function __construct() {
        $dsn = "mysql:host={$this->host};dbname={$this->dbname};charset={$this->charset}";

        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_PERSISTENT => true
        ];

        try {
            $this->pdo = new PDO($dsn, $this->username, $this->password, $options);
        } catch (PDOException $e) {
            die("数据库连接失败: " . $e->getMessage());
        }
    }

    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function getConnection() {
        return $this->pdo;
    }

    public function query($sql, $params = []) {
        try {
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            return $stmt;
        } catch (PDOException $e) {
            error_log("查询错误: " . $e->getMessage());
            throw new Exception("数据库操作失败");
        }
    }

    public function beginTransaction() {
        return $this->pdo->beginTransaction();
    }

    public function commit() {
        return $this->pdo->commit();
    }

    public function rollBack() {
        return $this->pdo->rollBack();
    }

    public function lastInsertId() {
        return $this->pdo->lastInsertId();
    }
}

2. 基础控制器 (src/controllers/BaseController.php)

<?php
class BaseController {
    protected $db;

    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
    }

    protected function render($view, $data = []) {
        extract($data);

        // 包含头部
        include __DIR__ . '/../views/layouts/header.php';

        // 包含视图
        include __DIR__ . '/../views/' . $view;

        // 包含尾部
        include __DIR__ . '/../views/layouts/footer.php';
    }

    protected function json($data, $statusCode = 200) {
        http_response_code($statusCode);
        header('Content-Type: application/json');
        echo json_encode($data);
        exit;
    }

    protected function redirect($url) {
        header("Location: {$url}");
        exit;
    }

    protected function isPost() {
        return $_SERVER['REQUEST_METHOD'] === 'POST';
    }

    protected function getPost($key = null, $default = null) {
        if ($key === null) {
            return $_POST;
        }
        return $_POST[$key] ?? $default;
    }

    protected function getGet($key = null, $default = null) {
        if ($key === null) {
            return $_GET;
        }
        return $_GET[$key] ?? $default;
    }

    protected function getSession($key = null, $default = null) {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        if ($key === null) {
            return $_SESSION;
        }
        return $_SESSION[$key] ?? $default;
    }

    protected function setSession($key, $value) {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        $_SESSION[$key] = $value;
    }

    protected function unsetSession($key) {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        unset($_SESSION[$key]);
    }
}

3. 用户模型 (src/models/User.php)

<?php
require_once __DIR__ . '/../config/database.php';

class User extends BaseController {

    public function create($userData) {
        // 验证邮箱是否已存在
        if ($this->findByEmail($userData['email'])) {
            throw new Exception("邮箱已被注册");
        }

        // 验证用户名是否已存在
        if ($this->findByUsername($userData['username'])) {
            throw new Exception("用户名已被使用");
        }

        // 密码哈希
        $passwordHash = password_hash($userData['password'], PASSWORD_DEFAULT);
        $emailToken = bin2hex(random_bytes(32));

        $sql = "INSERT INTO users (username, email, password_hash, email_verification_token, status, created_at)
                VALUES (?, ?, ?, ?, 'inactive', NOW())";

        $params = [
            $userData['username'],
            $userData['email'],
            $passwordHash,
            $emailToken
        ];

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        $userId = $this->db->lastInsertId();

        // 创建用户资料记录
        $this->createProfile($userId);

        return $userId;
    }

    public function authenticate($email, $password, $ip = null) {
        $user = $this->findByEmail($email);

        if (!$user) {
            // 记录失败日志
            $this->recordLoginAttempt(null, $ip, false, '用户不存在');
            return false;
        }

        // 检查账户状态
        if ($user['status'] !== 'active') {
            $this->recordLoginAttempt($user['id'], $ip, false, '账户未激活');
            return false;
        }

        // 检查账户是否被锁定
        if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
            $this->recordLoginAttempt($user['id'], $ip, false, '账户被锁定');
            return false;
        }

        // 验证密码
        if (!password_verify($password, $user['password_hash'])) {
            $this->handleFailedLogin($user, $ip);
            return false;
        }

        // 登录成功
        $this->handleSuccessfulLogin($user, $ip);

        return $user;
    }

    public function findById($id) {
        $sql = "SELECT u.*, p.first_name, p.last_name, p.avatar, p.bio
                FROM users u
                LEFT JOIN user_profiles p ON u.id = p.user_id
                WHERE u.id = ? AND u.status = 'active'";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([$id]);

        return $stmt->fetch();
    }

    public function findByEmail($email) {
        $sql = "SELECT * FROM users WHERE email = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$email]);

        return $stmt->fetch();
    }

    public function findByUsername($username) {
        $sql = "SELECT * FROM users WHERE username = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$username]);

        return $stmt->fetch();
    }

    public function updateProfile($userId, $profileData) {
        $sql = "UPDATE user_profiles
                SET first_name = ?, last_name = ?, phone = ?, bio = ?,
                    gender = ?, birth_date = ?, website = ?, location = ?
                WHERE user_id = ?";

        $params = [
            $profileData['first_name'],
            $profileData['last_name'],
            $profileData['phone'],
            $profileData['bio'],
            $profileData['gender'],
            $profileData['birth_date'],
            $profileData['website'],
            $profileData['location'],
            $userId
        ];

        $stmt = $this->db->prepare($sql);
        return $stmt->execute($params);
    }

    public function updatePassword($userId, $newPassword) {
        $passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);

        $sql = "UPDATE users SET password_hash = ?, updated_at = NOW() WHERE id = ?";
        $stmt = $this->db->prepare($sql);

        return $stmt->execute([$passwordHash, $userId]);
    }

    public function verifyEmail($token) {
        $sql = "SELECT id FROM users WHERE email_verification_token = ? AND status = 'inactive'";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$token]);

        $user = $stmt->fetch();

        if ($user) {
            $sql = "UPDATE users SET status = 'active', email_verified = TRUE,
                             email_verification_token = NULL, updated_at = NOW()
                     WHERE id = ?";
            $stmt = $this->db->prepare($sql);
            $stmt->execute([$user['id']]);

            return true;
        }

        return false;
    }

    private function createProfile($userId) {
        $sql = "INSERT INTO user_profiles (user_id, created_at) VALUES (?, NOW())";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$userId]);
    }

    private function handleFailedLogin($user, $ip) {
        // 增加失败次数
        $failedAttempts = $user['failed_attempts'] + 1;

        $sql = "UPDATE users SET failed_attempts = ?, updated_at = NOW() WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$failedAttempts, $user['id']]);

        // 如果失败次数达到5次,锁定账户30分钟
        if ($failedAttempts >= 5) {
            $lockedUntil = date('Y-m-d H:i:s', time() + 1800);
            $sql = "UPDATE users SET locked_until = ? WHERE id = ?";
            $stmt = $this->db->prepare($sql);
            $stmt->execute([$lockedUntil, $user['id']]);

            $this->recordLoginAttempt($user['id'], $ip, false, '账户被锁定');
        } else {
            $this->recordLoginAttempt($user['id'], $ip, false, '密码错误');
        }
    }

    private function handleSuccessfulLogin($user, $ip) {
        // 重置失败次数
        $sql = "UPDATE users SET failed_attempts = 0, locked_until = NULL,
                             last_login_at = NOW(), updated_at = NOW()
                     WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$user['id']]);

        // 记录成功登录
        $this->recordLoginAttempt($user['id'], $ip, true);
    }

    private function recordLoginAttempt($userId, $ip, $success, $reason = null) {
        $sql = "INSERT INTO login_logs (user_id, ip_address, user_agent, success, failure_reason, created_at)
                VALUES (?, ?, ?, ?, ?, NOW())";

        $params = [
            $userId,
            $ip ?? $_SERVER['REMOTE_ADDR'],
            $_SERVER['HTTP_USER_AGENT'] ?? null,
            $success,
            $reason
        ];

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
    }
}

4. 认证控制器 (src/controllers/AuthController.php)

<?php
require_once 'BaseController.php';
require_once __DIR__ . '/../models/User.php';
require_once __DIR__ . '/../models/PasswordReset.php';
require_once __DIR__ . '/../helpers/Validator.php';
require_once __DIR__ . '/../helpers/Mailer.php';
require_once __DIR__ . '/../helpers/Security.php';

class AuthController extends BaseController {
    private $user;
    private $passwordReset;

    public function __construct() {
        parent::__construct();
        $this->user = new User();
        $this->passwordReset = new PasswordReset();
    }

    // 显示注册页面
    public function register() {
        if ($this->getSession('user_id')) {
            $this->redirect('dashboard.php');
        }

        $this->render('auth/register.php');
    }

    // 处理注册请求
    public function doRegister() {
        if (!$this->isPost()) {
            $this->json(['success' => false, 'message' => '无效的请求方法']);
        }

        $data = [
            'username' => trim($this->getPost('username')),
            'email' => trim($this->getPost('email')),
            'password' => $this->getPost('password'),
            'confirm_password' => $this->getPost('confirm_password')
        ];

        // 验证数据
        $validator = new Validator();
        $errors = $validator->validateRegistration($data);

        if (!empty($errors)) {
            $this->json(['success' => false, 'errors' => $errors]);
        }

        try {
            // CSRF保护
            if (!Security::validateCsrfToken($this->getPost('csrf_token'))) {
                throw new Exception('请求无效,请刷新页面重试');
            }

            // 创建用户
            $userId = $this->user->create($data);

            // 发送验证邮件
            $user = $this->user->findById($userId);
            $mailer = new Mailer();
            $mailer->sendVerificationEmail($user['email'], $user['email_verification_token']);

            $this->json([
                'success' => true,
                'message' => '注册成功!请查收邮件验证账户。'
            ]);

        } catch (Exception $e) {
            error_log("注册错误: " . $e->getMessage());
            $this->json(['success' => false, 'message' => $e->getMessage()]);
        }
    }

    // 显示登录页面
    public function login() {
        if ($this->getSession('user_id')) {
            $this->redirect('dashboard.php');
        }

        $this->render('auth/login.php');
    }

    // 处理登录请求
    public function doLogin() {
        if (!$this->isPost()) {
            $this->json(['success' => false, 'message' => '无效的请求方法']);
        }

        $email = trim($this->getPost('email'));
        $password = $this->getPost('password');
        $remember = $this->getPost('remember') === 'true';

        try {
            // 验证CSRF令牌
            if (!Security::validateCsrfToken($this->getPost('csrf_token'))) {
                throw new Exception('请求无效,请刷新页面重试');
            }

            // 验证用户
            $user = $this->user->authenticate($email, $password);

            if (!$user) {
                throw new Exception('邮箱或密码错误');
            }

            // 创建会话
            $this->createSession($user, $remember);

            $this->json([
                'success' => true,
                'message' => '登录成功',
                'redirect' => 'dashboard.php'
            ]);

        } catch (Exception $e) {
            $this->json(['success' => false, 'message' => $e->getMessage()]);
        }
    }

    // 邮箱验证
    public function verifyEmail() {
        $token = $this->getGet('token');

        if (!$token) {
            $this->setSession('error', '验证链接无效');
            $this->redirect('login.php');
        }

        if ($this->user->verifyEmail($token)) {
            $this->setSession('success', '邮箱验证成功,请登录');
        } else {
            $this->setSession('error', '验证链接无效或已过期');
        }

        $this->redirect('login.php');
    }

    // 显示忘记密码页面
    public function forgotPassword() {
        $this->render('auth/reset.php', ['step' => 1]);
    }

    // 处理忘记密码请求
    public function doForgotPassword() {
        if (!$this->isPost()) {
            $this->json(['success' => false, 'message' => '无效的请求方法']);
        }

        $email = trim($this->getPost('email'));

        // 验证邮箱格式
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->json(['success' => false, 'message' => '邮箱格式错误']);
        }

        $user = $this->user->findByEmail($email);

        // 无论用户是否存在都显示成功信息(防止邮箱枚举攻击)
        if ($user) {
            try {
                $token = $this->passwordReset->createToken($user['id']);
                $mailer = new Mailer();
                $mailer->sendPasswordResetEmail($email, $token);
            } catch (Exception $e) {
                error_log("发送重置邮件错误: " . $e->getMessage());
            }
        }

        $this->json([
            'success' => true,
            'message' => '如果该邮箱已注册,您将收到密码重置链接'
        ]);
    }

    // 显示重置密码页面
    public function resetPassword() {
        $token = $this->getGet('token');
        $step = $this->getGet('step', 2);

        if (!$token) {
            $this->setSession('error', '重置链接无效');
            $this->redirect('login.php');
        }

        // 验证令牌
        $resetData = $this->passwordReset->validateToken($token);
        if (!$resetData) {
            $this->setSession('error', '重置链接无效或已过期');
            $this->redirect('login.php');
        }

        $this->render('auth/reset.php', [
            'step' => $step,
            'token' => $token,
            'email' => $resetData['email']
        ]);
    }

    // 处理重置密码
    public function doResetPassword() {
        if (!$this->isPost()) {
            $this->json(['success' => false, 'message' => '无效的请求方法']);
        }

        $token = $this->getPost('token');
        $password = $this->getPost('password');
        $confirmPassword = $this->getPost('confirm_password');

        // 验证密码
        if (strlen($password) < 6) {
            $this->json(['success' => false, 'message' => '密码至少6个字符']);
        }

        if ($password !== $confirmPassword) {
            $this->json(['success' => false, 'message' => '两次输入的密码不一致']);
        }

        // 验证令牌
        $resetData = $this->passwordReset->validateToken($token);
        if (!$resetData) {
            $this->json(['success' => false, 'message' => '重置链接无效或已过期']);
        }

        try {
            // 更新密码
            $this->user->updatePassword($resetData['user_id'], $password);

            // 标记令牌为已使用
            $this->passwordReset->markAsUsed($token);

            $this->json(['success' => true, 'message' => '密码重置成功']);

        } catch (Exception $e) {
            $this->json(['success' => false, 'message' => '密码重置失败']);
        }
    }

    // 登出
    public function logout() {
        // 销毁会话
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_destroy();
        }

        // 清除Cookie
        if (isset($_COOKIE['remember_token'])) {
            setcookie('remember_token', '', time() - 3600, '/');
            unset($_COOKIE['remember_token']);
        }

        $this->redirect('login.php');
    }

    // 创建会话
    private function createSession($user, $remember = false) {
        // 生成会话ID
        $sessionId = bin2hex(random_bytes(32));
        $expires = time() + ($remember ? 86400 * 30 : 3600); // 30天或1小时

        // 存储会话
        $sql = "INSERT INTO user_sessions (session_id, user_id, ip_address, user_agent, expires_at)
                VALUES (?, ?, ?, ?, ?)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $sessionId,
            $user['id'],
            $_SERVER['REMOTE_ADDR'],
            $_SERVER['HTTP_USER_AGENT'],
            date('Y-m-d H:i:s', $expires)
        ]);

        // 设置Session
        $this->setSession('user_id', $user['id']);
        $this->setSession('username', $user['username']);
        $this->setSession('email', $user['email']);
        $this->setSession('role', $user['role']);
        $this->setSession('session_id', $sessionId);

        // 设置记住登录Cookie
        if ($remember) {
            $token = bin2hex(random_bytes(32));
            $this->db->prepare("UPDATE users SET remember_token = ? WHERE id = ?")
                 ->execute([$token, $user['id']]);

            setcookie('remember_token', $token, $expires, '/', '', true, true);
        }
    }
}

5. 视图文件示例

注册页面 (src/views/auth/register.php)

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">
                    <h4 class="text-center">用户注册</h4>
                </div>
                <div class="card-body">
                    <?php if (isset($_SESSION['error'])): ?>
                        <div class="alert alert-danger">
                            <?php
                                echo $_SESSION['error'];
                                unset($_SESSION['error']);
                            ?>
                        </div>
                    <?php endif; ?>

                    <?php if (isset($_SESSION['success'])): ?>
                        <div class="alert alert-success">
                            <?php
                                echo $_SESSION['success'];
                                unset($_SESSION['success']);
                            ?>
                        </div>
                    <?php endif; ?>

                    <form id="registerForm">
                        <input type="hidden" name="csrf_token" value="<?php echo Security::generateCsrfToken(); ?>">

                        <div class="form-group mb-3">
                            <label for="username">用户名</label>
                            <input type="text" class="form-control" id="username" name="username" required>
                            <div class="invalid-feedback"></div>
                        </div>

                        <div class="form-group mb-3">
                            <label for="email">邮箱</label>
                            <input type="email" class="form-control" id="email" name="email" required>
                            <div class="invalid-feedback"></div>
                        </div>

                        <div class="form-group mb-3">
                            <label for="password">密码</label>
                            <input type="password" class="form-control" id="password" name="password" required>
                            <div class="invalid-feedback"></div>
                            <small class="text-muted">密码至少8个字符,包含字母和数字</small>
                        </div>

                        <div class="form-group mb-3">
                            <label for="confirm_password">确认密码</label>
                            <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
                            <div class="invalid-feedback"></div>
                        </div>

                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary">注册</button>
                        </div>
                    </form>

                    <hr>
                    <div class="text-center">
                        <p>已有账户? <a href="login.php">立即登录</a></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
document.getElementById('registerForm').addEventListener('submit', function(e) {
    e.preventDefault();

    const formData = new FormData(this);
    const submitBtn = this.querySelector('button[type="submit"]');
    const originalText = submitBtn.textContent;

    submitBtn.disabled = true;
    submitBtn.textContent = '注册中...';

    fetch('api/register.php', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            alert(data.message);
            window.location.href = 'login.php';
        } else {
            // 显示错误信息
            if (data.errors) {
                Object.keys(data.errors).forEach(field => {
                    const input = document.querySelector(`[name="${field}"]`);
                    const feedback = input.nextElementSibling;
                    input.classList.add('is-invalid');
                    feedback.textContent = data.errors[field];
                });
            } else {
                alert(data.message);
            }
        }
    })
    .catch(error => {
        console.error('Error:', error);
        alert('注册失败,请稍后重试');
    })
    .finally(() => {
        submitBtn.disabled = false;
        submitBtn.textContent = originalText;
    });
});
</script>

6. 安全助手 (src/helpers/Security.php)

<?php
class Security {
    // 生成CSRF令牌
    public static function generateCsrfToken() {
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }

    // 验证CSRF令牌
    public static function validateCsrfToken($token) {
        if (!isset($_SESSION['csrf_token'])) {
            return false;
        }

        return hash_equals($_SESSION['csrf_token'], $token);
    }

    // XSS过滤
    public static function escape($string) {
        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
    }

    // SQL注入过滤(虽然使用预处理语句,但额外的保护总是好的)
    public static function escapeSql($value) {
        // 转义特殊字符
        $search = array("\\", "\x00", "\n", "\r", "'", '"', "\x1a");
        $replace = array("\\\\", "\\0", "\\n", "\\r", "\\'", '\\"', "\\Z");

        return str_replace($search, $replace, $value);
    }

    // 清理输入
    public static function cleanInput($input) {
        if (is_array($input)) {
            return array_map([self::class, 'cleanInput'], $input);
        }

        $input = trim($input);

        // 移除可能的恶意代码
        $input = preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', '', $input);
        $input = preg_replace('/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/mi', '', $input);

        return $input;
    }

    // 生成安全的随机字符串
    public static function generateRandomString($length = 32) {
        return bin2hex(random_bytes($length / 2));
    }

    // 验证文件上传
    public static function validateFileUpload($file, $allowedTypes = [], $maxSize = 5242880) { // 5MB
        $errors = [];

        // 检查文件是否存在
        if (!isset($file) || $file['error'] === UPLOAD_ERR_NO_FILE) {
            $errors[] = '没有选择文件';
            return $errors;
        }

        // 检查上传错误
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $errors[] = '文件上传失败';
            return $errors;
        }

        // 检查文件大小
        if ($file['size'] > $maxSize) {
            $errors[] = '文件大小超过限制';
        }

        // 检查文件类型
        if (!empty($allowedTypes)) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            $mimeType = finfo_file($finfo, $file['tmp_name']);
            finfo_close($finfo);

            if (!in_array($mimeType, $allowedTypes)) {
                $errors[] = '文件类型不被允许';
            }
        }

        return $errors;
    }

    // 安全的文件名生成
    public static function generateSafeFilename($originalName) {
        $extension = pathinfo($originalName, PATHINFO_EXTENSION);
        $filename = pathinfo($originalName, PATHINFO_FILENAME);

        // 清理文件名
        $filename = preg_replace('/[^A-Za-z0-9_\-]/', '', $filename);
        $filename = substr($filename, 0, 50); // 限制长度

        // 生成唯一文件名
        $uniqueName = $filename . '_' . date('YmdHis') . '_' . uniqid();

        return $uniqueName . ($extension ? '.' . $extension : '');
    }
}

7. 邮件助手 (src/helpers/Mailer.php)

<?php
class Mailer {
    private $fromEmail;
    private $fromName;
    private $smtpHost;
    private $smtpPort;
    private $smtpUser;
    private $smtpPass;

    public function __construct() {
        $this->fromEmail = 'noreply@yourdomain.com';
        $this->fromName = '您的网站';

        // SMTP配置(使用PHPMailer)
        $this->smtpHost = 'smtp.yourdomain.com';
        $this->smtpPort = 587;
        $this->smtpUser = 'your-email@yourdomain.com';
        $this->smtpPass = 'your-password';
    }

    // 发送验证邮件
    public function sendVerificationEmail($toEmail, $token) {
        $subject = '验证您的邮箱地址';
        $verificationUrl = "http://" . $_SERVER['HTTP_HOST'] . "/verify-email.php?token={$token}";

        $message = $this->getEmailTemplate('verification', [
            'name' => '',
            'verification_url' => $verificationUrl,
            'token' => $token
        ]);

        return $this->send($toEmail, $subject, $message);
    }

    // 发送密码重置邮件
    public function sendPasswordResetEmail($toEmail, $token) {
        $subject = '重置您的密码';
        $resetUrl = "http://" . $_SERVER['HTTP_HOST'] . "/reset-password.php?token={$token}&step=2";

        $message = $this->getEmailTemplate('password_reset', [
            'reset_url' => $resetUrl,
            'token' => $token
        ]);

        return $this->send($toEmail, $subject, $message);
    }

    // 通用邮件发送方法
    private function send($to, $subject, $message) {
        // 邮件头
        $headers = "MIME-Version: 1.0" . "\r\n";
        $headers .= "From: {$this->fromName} <{$this->fromEmail}>" . "\r\n";
        $headers .= "Reply-To: {$this->fromEmail}" . "\r\n";
        $headers .= "Content-Type: text/html; charset=UTF-8" . "\r\n";

        // 使用PHPMailer的示例(需要安装PHPMailer)
        // 这里提供基本的mail()函数实现
        $success = mail($to, $subject, $message, $headers);

        if (!$success) {
            error_log("邮件发送失败: 收件人={$to}, 主题={$subject}");
        }

        return $success;
    }

    // 获取邮件模板
    private function getEmailTemplate($type, $data = []) {
        $templates = [
            'verification' => '
                <html>
                <head>
                    <meta charset="UTF-8">
                    <title>邮箱验证</title>
                </head>
                <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
                    <h2 style="color: #333;">欢迎注册!</h2>
                    <p>感谢您注册我们的网站。请点击下面的链接验证您的邮箱地址:</p>
                    <p><a href="{verification_url}" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">验证邮箱</a></p>
                    <p>如果按钮无法点击,请将以下链接复制到浏览器地址栏:</p>
                    <p style="word-break: break-all;">{verification_url}</p>
                    <p style="color: #666; font-size: 14px;">此链接24小时内有效。</p>
                </body>
                </html>
            ',

            'password_reset' => '
                <html>
                <head>
                    <meta charset="UTF-8">
                    <title>密码重置</title>
                </head>
                <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
                    <h2 style="color: #333;">重置密码</h2>
                    <p>您请求重置密码。请点击下面的链接重置您的密码:</p>
                    <p><a href="{reset_url}" style="background-color: #dc3545; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">重置密码</a></p>
                    <p>如果按钮无法点击,请将以下链接复制到浏览器地址栏:</p>
                    <p style="word-break: break-all;">{reset_url}</p>
                    <p style="color: #666; font-size: 14px;">此链接1小时内有效。</p>
                    <p style="color: #666; font-size: 14px;">如果您没有请求重置密码,请忽略此邮件。</p>
                </body>
                </html>
            '
        ];

        $template = $templates[$type] ?? '';

        // 替换模板变量
        foreach ($data as $key => $value) {
            $template = str_replace('{' . $key . '}', $value, $template);
        }

        return $template;
    }
}

API接口

1. 注册API (public/api/register.php)

<?php
require_once __DIR__ . '/../src/controllers/AuthController.php';

header('Content-Type: application/json');

$controller = new AuthController();
$controller->doRegister();

2. 登录API (public/api/login.php)

<?php
require_once __DIR__ . '/../src/controllers/AuthController.php';

header('Content-Type: application/json');

$controller = new AuthController();
$controller->doLogin();

3. 忘记登录功能

// 在AuthController中添加
public function checkRememberLogin() {
    if ($this->getSession('user_id')) {
        return true;
    }

    $token = $_COOKIE['remember_token'] ?? null;
    if (!$token) {
        return false;
    }

    $sql = "SELECT id FROM users WHERE remember_token = ? AND status = 'active'";
    $stmt = $this->db->prepare($sql);
    $stmt->execute([$token]);

    $user = $stmt->fetch();

    if ($user) {
        $this->createSession($user, true);
        return true;
    }

    return false;
}

部署和测试

1. 部署步骤

# 1. 配置Apache
# 确保 .htaccess 文件正确配置

# 2. 设置目录权限
chmod 755 public/
chmod 755 src/
chmod 644 public/*.php
chmod 644 src/**/*.php
chmod 777 logs/
chmod 777 uploads/

# 3. 创建数据库
mysql -u root -p < database/create_tables.sql

# 4. 配置数据库连接
# 编辑 src/config/database.php 中的数据库配置

# 5. 测试功能
# 访问 http://yourdomain.com/ 开始测试

2. 功能测试清单

## 测试清单

### 注册功能
- [ ] 正常注册
- [ ] 邮箱重复注册
- [ ] 用户名重复注册
- [ ] 密码过短
- [ ] 密码不一致
- [ ] 邮件验证功能

### 登录功能
- [ ] 正常登录
- [ ] 错误密码登录
- [ ] 不存在的用户登录
- [ ] 账户未激活登录
- [ ] 记住登录功能

### 密码重置
- [ ] 发送重置邮件
- [ ] 验证重置链接
- [ ] 重置密码功能
- [ ] 链接过期处理

### 安全性
- [ ] SQL注入防护
- [ ] XSS防护
- [ ] CSRF防护
- [ ] 密码哈希存储
- [ ] 登录失败锁定

扩展功能建议

1. 增强功能

  • 手机号注册
  • 第三方登录(微信、QQ、GitHub)
  • 双因素认证(2FA)
  • 用户等级系统
  • 积分系统
  • 邀请码注册

2. 管理功能

  • 管理员后台
  • 用户管理
  • 权限管理
  • 操作日志
  • 数据统计

3. 安全增强

  • IP白名单
  • 设备管理
  • 异地登录提醒
  • 密码强度要求
  • 账户安全评分

总结

通过这个用户注册登录系统项目,你已经实践了:

  1. MVC架构设计:学会了如何组织代码结构
  2. 数据库操作:掌握了PDO和预处理语句
  3. 表单处理:学会了表单验证和错误处理
  4. 会话管理:理解了Session和Cookie的使用
  5. 安全编程:实践了各种安全防护措施
  6. 邮件发送:学会了邮件功能的实现
  7. 项目规划:掌握了完整的开发流程

这个项目为你后续的Web开发打下了坚实的基础。你可以基于这个项目继续开发更复杂的应用,如博客系统、电商网站等。

记住,安全性是Web应用的生命线。在实际开发中,要时刻保持安全意识,遵循最佳实践,保护用户的数据和隐私。