滚动新闻 →
诺贝尔经济学奖揭晓 美两学者获殊荣 【学英语】高中毕业前,我第一次认真考虑自己的未来 免费试用 7 天还是 30 天,SaaS 产品应该如何设计试用期 微软吁建立AI控制系统 确保事故时可紧急关闭 经济模型研究获成果 美国两学者获诺贝尔奖 法校园枪击案 校长教职员遇袭 14岁少年被捕 笔记本电脑屏幕出现彩色条纹,维修前需要进行哪些测试 英语中表示改变的常用词有哪些 modify alter transform adjust revise reform 的区别与用法 加拿大国会众院通过法案 推动制裁跨国镇压 Windows 11 文件资源管理器主页怎么设置?常用文件夹可以这样管理 凌晨4点的街头 孩子在外卖箱中独自哭泣 申真谞完胜李志贤 连续四届跻身烂柯杯决赛 中共遍地架摄像头 挨批“孩子丢了却找不到” 俄鼠疫研究员死因成谜 恐重演中共病毒疫情 英基地恐袭未遂案 英相:几乎确认是伊朗策划 PHP 验证码应该怎么实现?简单登录验证系统的开发思路 涉移民欺诈调查 美议员奥马尔住所遭搜查 杜牧的诗为什么有一种独特的清醒感 中共将欠缴社保纳入税务处罚 网忧养老金亏空严重 比亚迪女员工反职场霸凌掌掴高管 一小时获2万点赞 波兰高级将领:美将推迟交付爱国者拦截导弹 以色列官员称 伊朗正重建及加速扩大导弹库存 茶与中国传统养生文化 书法中的中锋与侧锋有什么区别 孩子喜欢欺负弱小同学怎么办 受杭州酒局丑闻影响 浙江商务KTV大批关停 1080p和4K视频有什么区别 俄罗斯爆鼠疫疑云 中国多省发布风险提示 华男投毒师姐仅判一天 受害人公开铁证 八王之乱为何会导致严重后果 黑客入侵法国超市冷柜 升温至50度毁食品 华为手机屏蔽央媒批尊界文章? 官媒忙“辟谣” 美超微承包商在美认罪 AI晶片违法输中案升温 国安专家:中共间谍活动猖獗 旨在侵害美国 卫生间照明为什么经常被忽视 美暂停中港澳短期交流项目 间谍案延伸? 各地庆祝哥伦布日 川普宣布竖立新雕像 法国近300家连锁超市遭网攻 生鲜食材报废 A股普跌缩量3627亿 沪指失守3800点 巴黎逾400年历史的市场发生大火 “彻底摧毁”

PHP 验证码应该怎么实现?简单登录验证系统的开发思路

发布时间: 2026-10-12 17:00:01    最后更新: 2026-10-12 18:37:05    阅读:11  约17 分钟阅读     

PHP 验证码应该怎么实现?简单登录验证系统的开发思路
图片说明:示意图   图片来源:Public Domain(公有领域)
PHP 验证码应该怎么实现 登录系统不能只靠图片验证码

在 PHP 网站中,验证码经常被当成登录页面上的一个小功能:服务器生成一张图片,把几个数字或字母画进去,用户输入答案,PHP 再拿用户输入的内容和 Session 中保存的字符串进行比较。这个实现并不复杂,但如果把验证码直接等同于登录安全,整个认证系统的设计就容易出现问题。

一个可靠的登录验证系统应该把验证码看成风险控制的一层,而不是认证机制本身。密码负责证明用户掌握账户凭证,Session 负责维持登录状态,HTTPS 负责保护传输过程,密码哈希负责保护数据库中的凭证,而验证码、速率限制、风险识别等措施主要负责降低自动化攻击的效率。

PHP 验证码的基本工作原理

传统图片验证码的流程并不复杂。服务器首先生成一个随机验证码,然后创建 GD 图像画布,设置背景色,加入干扰线、噪点和文字,最后以 PNG 或 JPEG 格式输出。

典型的 GD 流程包括 imagecreatetruecolor() 创建真彩色画布,imagecolorallocate() 分配颜色,imageline() 绘制干扰线,imagefilledellipse() 生成噪点,最后使用 imagettftext() 绘制字体。

其中 imagettftext() 对字体文件路径有要求,因此生产环境不能假定某个字体一定存在。应该使用应用程序明确配置的字体文件,并确认 PHP GD 支持 TrueType 字体。

例如验证码字符可以使用:

$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';

这里故意去掉容易混淆的 0、O、1、I 等字符。验证码长度并没有一个适用于所有系统的固定答案,四位、五位、六位都可以,关键取决于验证码的用途、攻击模型和用户体验。

如果验证码只是作为登录风险控制的一层,盲目增加字符数量和干扰程度并不会自动带来更高安全性。复杂到人眼都难以识别的验证码,往往只是增加正常用户的失败率。

验证码不能直接明文放进客户端

验证码的答案应该保存在服务器端,而不是通过 HTML、JavaScript、Cookie 等方式直接交给浏览器。

例如可以将验证码答案放入 Session:

$_SESSION['captcha_hash'] = password_hash(
$captcha,
PASSWORD_DEFAULT
);

$_SESSION['captcha_expires'] = time() + 300;

验证时读取 Session 中的数据:

if (
empty($_SESSION['captcha_hash']) ||
empty($_SESSION['captcha_expires']) ||
time() > $_SESSION['captcha_expires'] ||
!password_verify($userCaptcha, $_SESSION['captcha_hash'])
) {
exit('验证码错误或已过期');
}

对于这种短生命周期随机值,也可以采用服务器端随机状态存储。不过如果验证码本身只在 Session 中存在几分钟,直接保存哈希可以减少验证码明文长期存在于应用状态中的机会。

这里有一个容易被忽略的问题:验证码应该具有一次性。

验证成功以后,应立即删除:

unset(
$_SESSION['captcha_hash'],
$_SESSION['captcha_expires']
);

否则同一个验证码可能被重复提交。

验证码验证失败时,也应该根据实际策略决定是否立即失效。对于登录场景,可以让失败次数受到限制,而不是允许攻击者无限次尝试同一个验证码。

不要使用 setcookie 保存验证码答案

原稿把验证码有效期和 setcookie() 联系起来,这个思路并不合适。

Cookie 是浏览器侧状态,而验证码答案属于服务器端认证流程中的临时状态。验证码是否有效,应该由服务器决定。

如果需要保存验证码对应的状态,可以使用 Session、Redis 或其他服务器端状态存储。Cookie 最多保存一个随机的、不可直接推导验证码内容的会话标识,而不是验证码答案本身。

验证码的过期时间也不需要固定成所谓“5至10分钟标准”。登录页面通常没有必要让一个验证码存在很长时间。几分钟已经足够覆盖正常用户输入,同时减少验证码被长期利用的机会。

不要使用 strcasecmp 处理所有验证码

如果验证码字符集本身不区分大小写,那么可以在生成时就统一使用大写字母,或者在验证前将输入统一转换成大写。

例如:

$userCaptcha = strtoupper(trim($_POST['captcha'] ?? ''));

但这里必须明确一点:不要把 strcasecmp() 当成验证码安全措施。

验证码真正重要的是随机性、服务器端状态、有效期、一次性使用以及失败次数控制。

trim() 可以处理用户误输入的首尾空格,但不应该对所有输入进行随意转换,否则可能改变原始数据的语义。

如果验证码字符集本来就只有数字,那么更简单,直接比较数字字符串即可。

随机数不能用 rand()

验证码最容易被忽视的问题之一是随机数来源。

不要使用简单的:

rand();

或者:

mt_rand();

作为安全验证码的核心随机源。

PHP 提供了适用于安全随机场景的 random_int() 和 random_bytes()。

例如:

$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
$captcha = '';

for ($i = 0; $i < 6; $i++) {
$captcha .= $chars[random_int(0, strlen($chars) - 1)];
}

这样生成验证码时,攻击者不能通过观察普通伪随机数序列轻易预测后续结果。

对于认证、密码重置、邮箱验证等安全相关功能,随机数生成器的选择应该按照密码学安全要求来决定,而不是为了代码短小方便而使用普通随机函数。

图片验证码只是自动化攻击的一道门槛

很多网站把验证码当成“防暴力破解系统”,这是一个常见误区。

攻击者如果能够持续向 /login.php 发送请求,即使每次都必须输入验证码,也可能通过 OCR、机器视觉或者第三方验证码识别服务进行自动化处理。

因此真正的登录防护应该放在登录接口本身。

例如应该同时考虑 IP 地址、账户、账户与 IP 的组合、设备或会话以及整个登录接口的请求速率。

对于普通网站,可以在反向代理、WAF、Web 服务器或者应用层设置速率限制。对于访问量较大的系统,可以使用 Redis 等高速状态存储记录失败次数,而不是每一次登录失败都写 MySQL 再重新查询统计。

不要简单设置“输错五次就锁账户”

账户锁定听起来很安全,但设计不好反而可以被攻击者利用。

例如攻击者知道某个用户的邮箱地址,只需要连续提交错误密码,就可以让服务器锁定这个账户。这样攻击者甚至不需要知道正确密码,就可以主动制造账户拒绝服务。

因此比起简单的永久或者长时间锁定,更合理的方式通常是采用逐级增加的限制。

例如同一个 IP 短时间大量失败,可以降低该 IP 的请求速率;同一个账户持续出现失败,可以增加挑战强度;异常情况下要求验证码或者其他验证;对于管理员账户,再增加 MFA。

这种方式的目标不是让攻击者永远不能尝试,而是让自动化攻击的成本快速上升,同时尽可能避免正常用户因为一次输错密码就无法登录。

登录密码绝对不能自己加密后存数据库

用户密码与验证码的安全模型完全不同。

密码不是简单使用 AES、MD5 或 SHA-256 做一次“加密”就保存。

PHP 应该使用:

$hash = password_hash(
$password,
PASSWORD_DEFAULT
);

登录时使用:

if (password_verify($password, $hash)) {
// 密码正确
}

数据库保存的是密码哈希,而不是用户原始密码。

如果 PHP 当前使用的默认密码算法参数发生变化,还可以通过:

password_needs_rehash()

判断现有密码哈希是否需要升级。

这套机制比自己设计“密码加盐后 SHA-256”可靠得多,因为密码哈希算法的设计本身涉及计算成本、盐值和抗离线破解能力,不应该由业务开发者重新发明。

登录查询必须使用预处理语句

数据库部分也不能为了验证码而忽略 SQL 注入。

例如:

$stmt = $pdo->prepare(
'SELECT id, username, password_hash
FROM users
WHERE username = :username
LIMIT 1'
);

$stmt->execute([
':username' => $username
]);

$user = $stmt->fetch(PDO::FETCH_ASSOC);

然后:

if (!$user || !password_verify($password, $user['password_hash'])) {
// 登录失败
}

这里不应该先通过 SQL 查询判断“用户名是否存在”,再向用户返回“用户名不存在”或者“密码错误”两种不同信息。

更安全的做法是让外部用户看到统一的登录失败提示,例如“用户名或密码错误”。

这样可以降低账户枚举风险。

验证码和 CSRF 是两个不同的问题

原稿把验证码和 CSRF 放在一起,容易让人误认为验证码可以防止 CSRF。

实际上两者解决的是完全不同的问题。

CSRF 防护关注的是攻击者能否利用用户当前浏览器的登录状态,诱导浏览器向目标网站发送未经授权的请求。

因此修改密码、账户设置、支付、管理员操作等状态改变请求,应该根据实际架构使用 CSRF Token、SameSite Cookie 等机制。

验证码不能代替 CSRF Token。

反过来,CSRF Token 也不能代替验证码、登录速率限制和密码安全策略。

一个成熟的认证系统应该让这些安全机制各自承担自己的职责,而不是寻找一个“万能字段”。

登录成功以后必须重新生成 Session ID

登录系统还有一个经常被教程忽略的问题:Session Fixation。

用户成功认证以后,应重新生成 Session ID:

session_regenerate_id(true);

然后再把用户身份写入 Session:

$_SESSION['user_id'] = $user['id'];
$_SESSION['logged_in'] = true;

同时应该通过 HTTPS 传输登录过程,并合理设置 Session Cookie 的 Secure、HttpOnly 和 SameSite 属性。

否则即使密码验证本身没有问题,Session 管理仍然可能成为攻击入口。

验证码接口本身也需要防滥用

如果网站把验证码图片放在:

/captcha.php

那么攻击者可能不断请求这个地址,让服务器不停生成图片。

验证码生成虽然通常不算特别昂贵,但如果字体渲染、复杂图像处理或者高分辨率图片生成占用大量 CPU,攻击者仍然可以利用这个接口制造资源消耗。

因此验证码接口本身也应该受到速率限制。

验证码图片最好使用短生命周期的随机标识与 Session 或 Redis 状态关联,而不是每一次请求都无限制生成新状态。

如果用户不断刷新验证码,还应该考虑旧验证码是否立即失效以及同一个会话允许多高频率刷新。

高并发系统不一定要把验证码存 MySQL

低流量网站直接使用 PHP Session 通常已经足够。

如果服务器采用多台 PHP 节点,而且 Session 不共享,就需要考虑集中式 Session 存储,例如 Redis,或者使用其他可靠的共享状态方案。

这时候验证码状态也可以放入 Redis,并设置短 TTL。

例如:

captcha:{random_id}

保存验证码哈希、创建时间和失败次数,并设置自动过期。

这种设计比不断向 MySQL 写入验证码记录更加适合高并发场景。

但也不要为了一个普通后台登录页面就过早引入复杂的分布式架构。安全设计首先应该与实际流量和风险匹配。

日志记录应该服务于安全分析

登录系统应该记录登录成功和失败事件,但不能把密码、Session Cookie、验证码答案或者其他认证秘密直接写进日志。

比较有价值的字段包括用户 ID、标准化后的账户标识、来源 IP、User-Agent、时间、登录结果、失败原因类别以及请求 ID。

这些数据可以用于判断异常行为,例如一个账户在几分钟内从大量不同 IP 发起失败登录,或者某一个 IP 同时攻击大量不同账户。

日志的作用不是保存所有用户输入,而是帮助安全人员重建攻击行为。

图片验证码并不是所有登录页面都需要

现代 Web 应用并不一定需要在页面加载时永久显示一个验证码。

如果每个正常用户每次登录都必须输入验证码,体验会明显下降。

更合理的方式通常是风险触发。例如正常登录保持低摩擦,当某个 IP、账户、设备或者请求行为表现出异常时,再提高验证要求。

验证码可以成为风险控制链条中的一环,而不是所有用户必须完成的固定步骤。

对于管理员、财务系统、后台管理系统等高价值账户,MFA 往往比一张复杂图片验证码更有价值。

验证码主要解决自动化请求成本问题,而 MFA 解决的是账户凭证泄露后的第二道认证问题,两者不能相互替代。

一个可靠的 PHP 登录系统应该怎样组织

从实际工程角度看,一个比较完整的 PHP 登录流程应该包含几个相互配合的层次:HTTPS 保护传输,输入验证和规范化处理用户数据,PDO 预处理语句防止 SQL 注入,password_hash() 和 password_verify() 管理密码,Session 管理认证状态,登录成功后重新生成 Session ID,CSRF Token 防止跨站请求伪造,IP 和账户维度的速率限制降低暴力破解风险,必要时使用验证码提高自动化攻击成本,高价值账户使用 MFA,日志系统记录异常认证行为。

验证码本身只负责其中很小的一部分。

如果只是开发一个普通 PHP 网站,GD 生成一张随机验证码图片并把短期状态放在 Session 中已经可以完成基础功能。但如果这是一个真正暴露在互联网中的登录系统,就不能停留在“生成图片、比较字符串”这个层面。

登录安全最终取决于整个认证链条,而不是验证码画得有多复杂。一个六位数、满屏干扰线的验证码,如果登录接口没有速率限制,账户可以被枚举,Session 可以被固定,密码可以被低成本破解,那么这张验证码图片实际上只是增加了正常用户的操作成本,并没有解决核心安全问题。对于 PHP 应用来说,优先建立正确的认证架构,再决定验证码在什么风险条件下出现,通常比单纯把验证码做得越来越复杂更加可靠。

喜欢这篇报道?

使用下面的功能,方便以后继续阅读和分享 MNewsTV

设为 Google 新闻首选来源 让 Google 新闻优先显示 MNewsTV 的最新报道 ›
★ 我的收藏 查看已经收藏的文章
关于文章收藏 收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。 删除收藏请进入「我的收藏」进行管理。
分享这篇报道

捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP