<?php练功房

第 5 章 · Web 开发 · 第 5.4 课

密码与安全

约 15 分钟

密码绝不能明文存

数据库有一天可能会泄露。如果里面存的是明文密码,所有用户在其他网站用的同一个密码也跟着泄露了。

也不要用 md5、sha1 这类快速哈希:它们太快了,攻击者每秒能试几十亿次,常见密码早就被整理成了查询表。

正确做法只有一个:password_hash() 存,password_verify() 验。它用的是专门为密码设计的慢速算法(默认 bcrypt),还会自动加随机“盐”,所以同一个密码每次生成的哈希都不一样。

安全的随机数和令牌

生成验证码、重置密码链接、API 密钥时,要用密码学安全的随机函数:random_int() 和 random_bytes()。rand()、mt_rand() 可以被预测,不能用在安全场景。

比较两个令牌时用 hash_equals(),它的耗时和内容无关,能防止“计时攻击”。

CSRF:别让别的网站替你点按钮

假设你登录着银行网站,又打开了一个恶意网页,它偷偷向银行提交一个转账表单。浏览器会自动带上你的 cookie,银行以为是你本人操作。这叫 CSRF(跨站请求伪造)。

防御方法:给每个会话生成一个随机令牌,放进表单的隐藏字段;提交时检查令牌是否一致。恶意网站拿不到这个令牌。本站所有的表单和保存进度的接口都做了这个检查。

<?php
// 生成表单时
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
echo '<input type="hidden" name="csrf" value="' . $_SESSION['csrf'] . '">';

// 处理提交时
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(403);
    exit('请求无效');
}

安全清单

风险对策
XSS输出到 HTML 前 htmlspecialchars
SQL 注入预处理语句,值用占位符传入
密码泄露password_hash / password_verify
CSRF表单带随机令牌,提交时 hash_equals 校验
暴力破解登录失败次数限制
会话劫持登录后 session_regenerate_id(true),cookie 设 HttpOnly

选择题

存储用户密码应该用哪种方式?

选择题

生成“重置密码”链接里的令牌,应该用哪个函数?

练习 1

注册和登录

用一个数组 $db 模拟用户表(键是用户名,值是密码哈希),写两个函数:

register(array &$db, string $user, string $pass): string

  • 用户名已存在:返回 "用户名已存在"
  • 密码少于 8 位:返回 "密码太短"
  • 否则把密码的 哈希 存进 $db[$user],返回 "注册成功"

login(array $db, string $user, string $pass): bool

  • 用户存在且密码正确返回 true,否则返回 false

注意 register 的第一个参数前面有 &,表示函数里对 $db 的修改会影响到外面的变量。