接手一个老项目,发现用户密码居然是明文存储,认证逻辑散落在 7 个文件中。借助 AI 工具分析遗留代码、设计安全方案并渐进式迁移,最终在不中断业务的前提下完成了安全升级。

接盘即惊吓:明文密码和散落的认证逻辑
年初接手了一个内部管理系统,技术栈是 PHP 5.6 + jQuery,没有框架,没有 ORM,用户认证直接调 MySQL。当我看到数据库 users 表里明晃晃存着 password 字段,值是用户注册时填的原始密码时,后背一凉------这个系统已经跑了四年,有两百多个企业客户在用。
更头疼的是认证逻辑散落在至少 7 个文件里:
login.php处理用户名密码校验check_auth.php在每个页面顶部 include 做 session 检查logout.php清理 sessionreset_password.php管理员直接改数据库api/auth.php给几个简单的 API 做 token 校验(token 也是明文存在表里)- 还有几个页面直接写了
if ($_SESSION['user_id'] == '')这种硬编码判断
没有任何加密、没有密码强度要求、没有登录失败限制、没有 CSRF 防护。 这个模块不仅是技术债,更是一个安全定时炸弹。
重构的目标很明确:用 bcrypt 存储密码、引入 JWT 做无状态认证、统一认证入口、加基础防护。但老系统没有测试,贸然重写万一漏了某个边缘逻辑就会影响线上业务。我需要先彻底理解现有逻辑 ,再设计安全方案 ,最后渐进式迁移。这个过程中 AI 工具帮了大忙。
第一步:用 AI 从遗留代码中提取认证状态机
传统做法是手动翻 7 个文件,画流程图。这次我把所有相关代码文件拖进 Cursor,用它的项目级索引功能,然后问:
"分析 login.php、check_auth.php、logout.php、reset_password.php、api/auth.php,提取所有与用户认证相关的逻辑,输出以下内容:
- 认证流程(从登录到 session 校验的完整路径)
- 所有涉及密码操作的代码位置和操作类型(读取、写入、校验、重置)
- 各文件之间的依赖关系
- 潜在的安全漏洞(按严重程度排序)"
Cursor 基于 Claude Sonnet 4 模型,在几十秒内给出了分析报告。关键发现:
- 密码在 4 个地方被读取/写入,全部是明文。
- session 校验逻辑不一致:有的地方检查
$_SESSION['user_id'],有的检查$_SESSION['login'],还有一个页面自己维护了一套 cookie 校验。 - API 的 token 生成用了
md5(username + 'secret'),可以被预测。 - 没有密码重置的验证机制,管理员可以直接改任意用户的密码。
这一步帮我省了至少半天的人工梳理时间 ,而且 AI 不会遗漏分散在边角的逻辑------有个 download_report.php 里藏了一句独立的 session 检查,人工排查大概率会漏掉。
第二步:用 AI 设计安全方案并评估风险
搞清现状后,我让 Cursor 帮我设计目标方案:
"基于以上认证逻辑,设计一个安全的认证方案,要求:
- 密码用 bcrypt 存储,cost factor 建议值
- 使用 JWT 替代 session + API token
- 统一的认证中间件
- 登录失败次数限制(5 次锁定 30 分钟)
- 密码重置必须通过邮箱验证
- 输出数据库 schema 变更、API 设计、中间件伪代码"
AI 给出的方案包含:
users表新增password_hash、login_attempts、locked_until字段,保留旧的password字段作为过渡。- JWT 的 access token 15 分钟过期,refresh token 7 天,存在 httpOnly cookie 中。
- 中间件用 PSR-7 风格(因为是 PHP),统一校验
Authorizationheader。 - 登录失败计数器用 Redis 原子操作,避免并发绕过。
我对方案做了一处重要修改 :AI 建议直接废弃旧 password 字段,我坚持保留作为过渡期兼容------因为还有其他微服务直接读这个字段。这是 AI 不会知道的业务上下文,人的判断仍然不可或缺。
第三步:渐进式迁移------不停机切换认证方式
新旧认证方式需要共存一段时间,既要让新登录逻辑生效,又要保证老 session 和 API 继续工作。我设计了一个双轨运行期:
php
// 认证适配器(简化版)
class AuthAdapter {
// 新用户或已迁移用户走 JWT
public function authenticate($username, $password) {
$user = $this->db->getUser($username);
// 优先用 bcrypt 验证已迁移的密码
if (!empty($user['password_hash'])) {
if (password_verify($password, $user['password_hash'])) {
return $this->issueJwt($user);
}
}
// 回退到明文验证(仅过渡期)
if ($password === $user['password']) {
// 验证成功后立即迁移密码
$this->migratePassword($user['id'], $password);
return $this->issueJwt($user);
}
// 记录失败次数
$this->recordFailedAttempt($user['id']);
return false;
}
// 迁移密码
private function migratePassword($userId, $plainPassword) {
$hash = password_hash($plainPassword, PASSWORD_BCRYPT, ['cost' => 12]);
$this->db->update($userId, [
'password_hash' => $hash,
'password' => null // 清除明文
]);
}
}
这样每个用户首次用明文密码登录后,密码立即被迁移为 bcrypt 并删除明文。整个过程对用户透明。
AI 在这个阶段的作用 :我让 Cursor 帮我找到所有直接读取 password 字段的 SQL 语句和代码(共 11 处),确保迁移逻辑不会漏掉任何读操作。这个搜索人工做容易漏,AI 几秒就扫完了。
第四步:补充安全防护------AI 生成测试用例和防护规则
迁移完成后,AI 帮我做了两件事:
1. 生成安全测试用例
"为新的认证模块生成 PHPUnit 测试用例,覆盖:
- 正常登录、错误密码、用户不存在
- 密码迁移逻辑(明文密码首次登录)
- JWT 过期、篡改、缺失
- 登录失败锁定(5 次、6 次、锁定后 30 分钟解锁)
- CSRF token 校验"
AI 生成了 30 多个测试用例,我手动补充了 3 个边缘场景(如并发登录失败计数),全部通过。
2. 配置安全响应头
AI 帮我生成了 Nginx 和 PHP 的安全头配置:
nginx
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
迁移结果与数据对比
整个迁移在两周内完成,期间系统正常运行,没有用户反馈登录问题。
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 密码存储方式 | 明文 | bcrypt (cost 12) |
| 认证文件数 | 7 个 | 1 个中间件 + 1 个 service |
| 登录失败限制 | 无 | 5 次/30 分钟 |
| 密码重置流程 | 管理员直接改库 | 邮箱验证链接 |
| API 认证 | 固定 token (MD5) | JWT (15 分钟过期) |
| 安全测试覆盖 | 0% | 87% |
最重要的是,这次重构让我对遗留系统的安全升级有了一套可复用的方法论:AI 做分析和方案设计,人做关键决策和验证,渐进式迁移不中断业务。
几个关键经验
- 不要一次性重写:双轨运行虽然代码丑一点,但安全。我跑了两周双轨,确认所有用户都完成密码迁移后,才删除了旧逻辑。
- AI 分析遗留代码的效率远超人工 :尤其是找"隐式依赖"------比如那个藏在
download_report.php里的独立 session 检查,AI 帮我一网打尽。 - 安全方案要结合业务上下文:AI 建议全量迁移,但我保留了过渡期兼容老系统。AI 没有你的业务知识,方案必须人审。
- 渐进迁移的核心是"自动升级":在用户无感知的情况下完成密码升级,这比发通知让所有人改密码的用户体验好得多。
如果你也在维护遗留系统,建议花半天时间让 AI 帮你做一次安全审计,你会发现自己一直不敢动的代码其实没那么可怕。
你接手过最吓人的遗留系统是什么样的?有没有在重构时踩过什么坑?欢迎评论区交流。