从账号泄露到 N+1 优化:一次 Spring Boot + Vue 项目的大改造
一次图书管理系统的全面优化,涵盖安全漏洞修复、N+1 查询优化、凭证管理强化以及数据库索引调优。从踩坑到解决,完整还原优化全过程。
🎵 配乐:暖阳下的代码时光
轻柔钢琴与弦乐交织,适合编程与思考的浪漫背景音乐
前言
做项目最怕的不是功能做不出来,而是上线之后才发现有安全漏洞,或者用户抱怨系统”慢得像蜗牛”。最近对图书管理系统进行了一次全面”体检”,发现了不少问题:用户密码明文暴露、N+1 查询导致审核页面卡死、OSS 凭证硬编码在代码里、数据库缺少关键索引……这些问题单个看都不大,但叠加在一起,足以让一个系统从”能用”变成”难用”。
本文完整记录了这次优化全过程,包括问题分析、解决思路、核心代码改动,以及数据库索引优化策略。如果你也在维护类似技术栈(Spring Boot + Vue)的项目,相信能从中学到一些实战经验。
一、问题全景:这次优化了什么
在开始深入细节之前,先整体看一下这次优化的四个核心问题:
| 编号 | 问题 | 类型 | 优先级 | 涉及范围 |
|---|---|---|---|---|
| OPT-51 | User.password 泄露 + 密码修改逻辑缺陷 | 安全 | 高 | User.java, UserController.java, Password.vue |
| OPT-70 | BookAudit N+1 查询优化 | 性能 | 高 | Book.java, BookMapper.java, StudentBookController.java, BookAudit.vue |
| OPT-54 | 硬编码凭证清理 + 环境变量化 | 安全 | 高 | OssConfig.java, application.properties |
| OPT-60/61 | 数据库索引优化 Round4 | 性能 | 中 | sql/ 脚本,user/book/teacher/lend_rule 等表 |
四个问题分为两类:安全加固和性能提升。安全问题是底线,性能问题影响体验,两者都不能忽视。
二、密码泄露危机:User.password 的 JsonIgnore 修复

图1:密码泄露问题示意
2.1 问题是怎么被发现的
在做登录功能测试时,我随手打了一个 console.log(user),结果在控制台看到了这个:
1 | { |
密码居然是明文!当时就意识到这是一个严重的安全漏洞。只要有人通过浏览器的 Network 面板或者抓包工具查看登录接口的返回值,就能拿到所有用户的明文密码。
进一步排查发现,User.java 的 password 字段缺少 @JsonIgnore 注解。这意味着:
- 登录接口返回用户信息 → 密码暴露
- 任何将 User 对象序列化的地方 → 密码暴露
- 如果把用户信息存入 sessionStorage 或 LocalStorage → 密码暴露
2.2 密码修改功能的连锁问题
修复 @JsonIgnore 之后,新的问题接踵而至:Password.vue 第 97 行有一段客户端旧密码比对逻辑:
1 | const storedUser = JSON.parse(sessionStorage.getItem('user') || '{}'); |
加了 @JsonIgnore 之后,从 sessionStorage 取出的 user.password 变成了 undefined,导致所有旧密码验证都失败。用户输入正确密码也会被系统拒绝。
2.3 解决方案:服务端验证 + 移除客户端逻辑
正确的做法是把密码验证逻辑后移到后端,前端只负责收集用户输入:
第一步:User.java 添加 @JsonIgnore
1 | import com.fasterxml.jackson.annotation.JsonIgnore; |
第二步:UserController.java 增加旧密码验证
1 |
|
第三步:Password.vue 删除客户端验证
1 | async submitForm() { |
2.4 经验总结
- 永远不要在客户端存储或比对密码:密码验证必须在服务端完成,客户端只负责传输用户输入。
@JsonIgnore是敏感字段的标配:任何序列化场景(API 响应、缓存存储、日志输出)都要考虑是否包含敏感字段。- 先找影响范围再动手:修复安全问题时,最好先梳理清楚所有依赖点,避免”修复一个 bug 引入三个新 bug”。
三、N+1 查询噩梦:BookAudit 审核页面的性能陷阱

图2:N+1 查询优化前后对比
3.1 问题现象
打开图书审核页面,加载 20 本待审核的图书,需要大约 5-6 秒。这个速度对于一个内部管理系统来说实在太慢了。
通过浏览器的 Network 面板分析请求,发现了问题:主请求只有 1 个,但 /user/{id} 请求有 20 个!每显示一本书,都要单独请求一次上传者的用户名。
这就是经典的 N+1 查询问题:1 次主查询 + N 次关联查询。
3.2 问题根因分析
1 | 主请求: GET /studentBook/pendingAudit |
图书审核页面需要显示上传者名称,但后端接口只返回 Book 实体,不包含用户信息。前端不得不逐行调用用户接口获取上传者名称,导致请求数量暴增。
3.3 解决方案:JOIN 查询一次搞定
思路转变:与其让前端发多次请求,不如后端直接 JOIN 一次查出来。
第一步:Book.java 添加虚拟字段
1 | public class Book { |
第二步:BookMapper.java 新增 JOIN 方法
1 |
|
第三步:StudentBookController.java 改用新方法
1 |
|
第四步:BookAudit.vue 删除 loadUploaderInfo()
1 | // 整个 loadUploaderInfo 方法删除 |
3.4 性能对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 请求数(审核 20 本书) | 21 次 | 1 次 |
| 预计耗时 | ~500ms+ | ~50ms |
| RTT 开销 | 高 | 低 |
优化后页面加载时间从 5-6 秒降到不足 1 秒,用户体验大幅提升。
3.5 经验总结
- 后端思维:如果页面需要展示关联数据,优先在后端用 JOIN 一次查出来,而不是让前端发多次请求。
- N+1 是性能大敌:在设计接口时就要考虑是否会触发 N+1 查询,提前规避。
@TableField(exist = false)的妙用:用于接收 JOIN 查询的衍生字段,不影响原有数据库结构。
四、凭证裸奔:OSS 和 API Key 的硬编码危机

图3:凭证安全管理的关键——环境变量注入
4.1 问题有多严重
检查代码时发现,OssConfig.java 里写着:
1 | public OssConfig() { |
application.properties 里也有:
1 | aliyun.oss.access-key-id=LTAI4xxxxxxxxxxxxxx |
这意味着:代码提交到 Git 仓库之后,这些凭证就永久暴露了。任何能访问代码库的人都能拿到云资源的访问权限。
更危险的是,启动日志还会打印这些凭证:
1 | log.info("OSS配置加载,accessKeyId: {}", accessKeyId); |
4.2 解决方案:环境变量注入
OssConfig.java 重构
1 |
|
application.properties 改用占位符
1 | # OSS凭证 — 通过环境变量注入,不提交到代码库 |
生产环境配置
1 | # 在服务器上设置环境变量 |
4.3 安全建议
- 生产环境使用密钥管理服务:如阿里云 KMS、AWS Secrets Manager 等。
- 本地开发使用
.env文件:已在.gitignore中,避免误提交。 - 定期轮换凭证:即使泄露了,及时轮换也能降低损失。
五、数据库索引优化:让查询飞起来

图4:索引优化让数据库查询效率大幅提升
5.1 为什么需要索引优化
项目跑了一段时间后,一些高频查询开始变慢。排查发现,部分热点查询缺少索引,导致数据库进行全表扫描。举个例子,用户登录时按用户名查询:
1 | SELECT * FROM user WHERE username = 'admin'; |
没有索引的话,MySQL 需要遍历全表才能找到匹配行。随着数据量增长,这个查询会越来越慢。
5.2 新增索引(8 个)
| 表名 | 索引名 | 字段 | 类型 | 用途 |
|---|---|---|---|---|
| user | idx_username | username | UNIQUE | 登录/注册热点查询 |
| book | uk_isbn | isbn | UNIQUE | 业务主键查询 |
| book | idx_name | name | INDEX | 图书名 LIKE 查询 |
| book | idx_create_time | create_time | INDEX | 仪表盘排序 |
| teacher | idx_name_status | (name, status) | INDEX | 教师查询 |
| lend_rule | idx_user_type_active | (user_type, is_active) | INDEX | 借阅规则查询 |
| book_review | idx_isbn_deleted_time | (isbn, deleted, create_time) | INDEX | 书评列表查询 |
| student | idx_name | name | INDEX | 学生名搜索 |
5.3 删除冗余索引(4 个)
有些索引是冗余的,不仅没用,还会增加写入开销:
| 表名 | 索引名 | 删除原因 |
|---|---|---|
| bookwithuser | idx_reader_id | uk_reader_isbn 的左前缀,冗余 |
| bookwithuser | idx_bwu_reader_id | 索引在自增主键上,无用 |
| bookwithuser | idx_bwu_isbn | 与 idx_isbn 重复 |
| lend_record | idx_fine_amount | 从未被查询使用 |
5.4 完整 SQL 脚本
1 | -- ============================================ |
5.5 性能影响预估
| 场景 | 优化前 | 优化后 |
|---|---|---|
| 用户登录查询 | 全表扫描 | 索引查找 |
| 图书 ISBN 查询 | 全表扫描 | UNIQUE 查找 |
| 图书名搜索 | 全表扫描 | 索引查找 |
| 仪表盘加载 | filesort | 索引排序 |
六、优化成果汇总
经过这轮优化,系统在安全性和性能上都有了显著提升:
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 密码泄露风险 | 暴露明文密码 | @JsonIgnore 保护 | 安全加固 |
| 密码修改验证 | 客户端验证 | 服务端验证 | 安全加固 |
| 审核页面请求数 | 21 次 | 1 次 | 减少 95% |
| 凭证管理 | 硬编码 | 环境变量 | 安全加固 |
| 热点查询 | 部分全表扫描 | 索引覆盖 | 性能提升 |
结语
这次优化让我深刻体会到:安全和性能不是事后补丁,而是设计之初就要考虑的。代码写得太随意,上线之后就会踩坑;提前多花一点时间设计,上线之后就能少很多麻烦。
如果你也在做类似的项目,建议从一开始就注意这些问题:
- 敏感数据加
@JsonIgnore,别等泄露了再补救 - 密码验证放后端,别依赖客户端
- 接口设计时考虑 N+1 问题
- 凭证用环境变量,别硬编码
- 数据库建表时同步规划索引
希望这篇文章对你有帮助!如果有任何问题,欢迎留言交流。
相关代码已脱敏处理,请勿直接照搬,结合实际项目情况调整。
配乐:《暖阳下的代码时光》— 轻柔钢琴与弦乐交织,适合编程与思考的浪漫背景音乐