登录页面不是陆页只有表单和按钮。真正写起来,面完码最容易被忽略的整代是错误处理——比如用户提交空密码,或者数据库查不到这个人。陆页下面这份代码我用了好几年,面完码改过几轮,整代现在算是陆页比较稳的版本。
先看目录结构:login.php 负责显示和验证,面完码config.php 放数据库连接,整代style.css 管外观。陆页这样拆开,面完码以后换数据库或改样式不用动逻辑。整代
config.php 很简单,陆页但别把密码硬编码在里面。面完码我习惯用环境变量,整代没有的话给个默认值:
<?php $host = getenv('DB_HOST') ?: 'localhost'; $db = getenv('DB_NAME') ?: 'test_db'; $user = getenv('DB_USER') ?: 'root'; $pass = getenv('DB_PASS') ?: ''; $charset = 'utf8mb4'; $dsn = "mysql:host=$host;dbname=$db;charset=$charset"; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; try { $pdo = new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { die('数据库连接失败:' . $e->getMessage()); } ?> 注意 PDO::ATTR_EMULATE_PREPARES 设为 false,这样预处理语句才是真的预处理,不是拼字符串。很多注入漏洞就出在这。
login.php 分两段:先处理 POST,再渲染 HTML。处理逻辑放最上面,避免“headers already sent”错误。
<?php session_start(); require 'config.php'; $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; if ($username === '' || $password === '') { $error = '用户名和密码都不能为空'; } else { $stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE username = ? LIMIT 1'); $stmt->execute([$username]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { session_regenerate_id(true); $_SESSION['user_id'] = $user['id']; header('Location: dashboard.php'); exit; } else { $error = '用户名或密码错误'; } } } ?> 几个细节:trim() 去掉用户名两边的空格,但密码不能 trim,因为空格可能是密码的一部分。session_regenerate_id(true) 防止会话固定攻击。登录成功后跳转,不要直接输出内容,否则刷新会重复提交。
下面是 HTML 部分,和 PHP 混在一起,但保持干净:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="login-box"> <h1>登录</h1> <?php if ($error): ?> <p class="error"><?= htmlspecialchars($error) ?></p> <?php endif; ?> <form method="post" action=""> <label for="username">用户名</label> <input type="text" id="username" name="username" required autofocus> <label for="password">密码</label> <input type="password" id="password" name="password" required> <button type="submit">登录</button> </form> </div> </body> </html> htmlspecialchars($error) 不能省,万一错误信息里带了用户输入,会变成 XSS 漏洞。表单的 action 留空,提交到当前页面,这样不用维护两个文件。
style.css 就简单写几行,够用就行:
body { font-family: system-ui, sans-serif; background: #f2f2f2; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } .login-box { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 320px; } .login-box h1 { margin: 0 0 1.5rem; font-size: 1.5rem; } .login-box label { display: block; margin-bottom: 0.25rem; font-size: 0.9rem; } .login-box input { width: 100%; padding: 0.5rem; margin-bottom: 1rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } .login-box button { width: 100%; padding: 0.6rem; background: #0066cc; color: #fff; border: none; border-radius: 4px; cursor: pointer; } .login-box button:hover { background: #0055aa; } .error { color: #cc0000; font-size: 0.9rem; margin-bottom: 1rem; } 数据库表结构别忘了建:
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL ); 插入测试用户时,用 password_hash('你的密码', PASSWORD_DEFAULT) 生成哈希,别存明文。我见过有人直接存 md5,那跟没加密差不多。
最后提一句:这个登录页面没有验证码、没有登录失败次数限制,生产环境得加上。但作为基础骨架,它已经避开了最常见的几个坑——SQL 注入、XSS、会话固定。剩下的按需扩展就行。


相关文章




精彩导读
热门资讯
关注我们