2026-09-28 在 /home/cao/workspace/nova-forum 对 HEAD f177cdc 的未提交安全加固做只读审查,首次复现了两个隐藏问题:CORS allowedOrigins=[*]+allowCredentials=true 抛异常,以及 JWT 有效但用户不存在时 UsernameNotFoundException 逃出过滤器。把这次审查的过程和发现写下来。
Nova Forum 项目背景
Nova Forum 是一个技术论坛项目,Spring Boot + Vue,定位为求职学习项目。需求分析阶段已于 2026-09-11 冻结,文档:docs/需求分析.md,下一步进入系统设计阶段(数据库/API/模块划分)。
核心设计决策:
- 角色:GUEST / USER / MODERATOR(按板块授权)/ SYS_ADMIN;废除「内容管理员」;版主由 SYS_ADMIN 直接分配
- 认证:access JWT 15min 前端内存 + refresh 14d HttpOnly+Secure+SameSite=Lax Cookie;DB user_session 存 refresh 哈希;多设备
- refresh rotation(旧 token 重用返回 401 REFRESH_TOKEN_INVALID,无 token family)
/auth/refresh仅 POST,CSRF 靠 SameSite+Origin/Referer- 登录失败按 username_normalized+IP 15min 5 次单机内存限流
- 封禁 ACTIVE/BANNED 临时/永久(banned_until/ban_reason),仅 SYS_ADMIN;封禁后登录/refresh 403 ACCOUNT_BANNED
复现的第一个问题:CORS 配置异常
现象:allowedOrigins=[*] 与 allowCredentials=true 同时设置时,Spring Security 直接抛异常。
根因:Spring Framework 的 CorsConfiguration 不允许 allowedOriginPatterns 为 * 同时 allowCredentials 为 true。这是 Spring 的安全设计——当允许凭据(Cookie)时,通配符 * 会被拒绝,必须显式列出允许的源。
修复:SecurityConfig 改成 app.cors.allowed-origins 可配置明确白名单(默认 localhost/127.0.0.1
复现的第二个问题:UsernameNotFoundException 逃出过滤器
现象:JWT 有效(未过期)但用户不存在时,UsernameNotFoundException 直接逃出 JwtAuthenticationFilter,返回 500 而非 401。
修复:JwtAuthenticationFilter catch(Exception) 返回 JSON 401。
剩余 P2:JwtAuthenticationFilter.java:59 catch(Exception) 把数据库连接等基础设施故障也错误映射 401(probe 已复现 IllegalStateException 场景),应区分认证异常和 5xx。
验证与回归
二次 mvn verify 仍 299 项全过、JaCoCo 通过;nightly-report 记录 14 仍写 305 且 CORS 说明过时。git diff —check 报 JwtAuthenticationFilter.java:54 行尾空白。仓库源码未被审查代理修改。
修复质量评估 7.5/10:原两问题已修但异常分类、回归测试和报告准确性不足。
Nova Forum 安全设计要点总结
这次审查中看到的几个安全设计决策,值得记录:
认证与 Token 管理
- access JWT 15min 存前端内存,refresh 14d HttpOnly+Secure+SameSite=Lax Cookie
- DB user_session 存 refresh 哈希,支持多设备
- logout 撤销当前 / logout-all 全部;改密/封禁撤销全部
- 不做黑名单/token_version;不引入 Redis
- refresh rotation:旧 token 重用返回 401 REFRESH_TOKEN_INVALID
密码与注册
- 注册仅 username+password,无邮箱/验证码/邮件依赖,注册即自动登录
- 密码 12–128 不强制组合、拒弱密码、不得含 username
- Argon2id(BCrypt 处理 72 字节)
- MVP 无密码找回
权限与状态三维
- 状态三维:status(DRAFT/PUBLISHED/DELETED) / visibility(PUBLIC/MEMBERS_ONLY/PRIVATE) / moderation(NORMAL/HIDDEN/VIOLATION)
- HIDE→HIDDEN;DELETE→DELETED(违规同时 VIOLATION)
- visibility 仅作者设置
- 判权顺序 DELETED→moderation→visibility;DELETED 含作者一律 404(走后台独立接口)
限流与安全
- 登录失败按 username_normalized+IP 15min 5 次单机内存限流
- 用户名不存在与密码错统一提示(不泄露用户是否存在)
- 改密必须验旧密码
- 发帖 30s、评论 5s(按用户 ID,应用层)
日志安全
- 日志禁记密码/hash/token/密钥/Cookie/Authorization/用户正文
- 配置 application.yml+dev/prod,secret 环境变量不入 Git
经验教训
- CORS 配置不能图省事用
*:只要涉及凭据传递(Cookie/Authorization Header),必须显式列出允许的源 - 过滤器层要 catch 所有异常并映射到 HTTP 状态码:不要让内部异常类(如 UsernameNotFoundException)直接逃出变成 500
- 区分基础设施故障和认证失败:数据库连接失败应返回 500/503,而不是 401 掩盖问题
- 安全审查要实际运行 probe 复现:只读 diff 看不出的问题,通过 Java probe 真实请求才复现出来
- CI 报告要及时更新:CORS 说明过时、nightly-report 数字不符这类问题,会在后续迭代中误导开发者
这次审查让我深刻体会到:安全加固不是改完一个 bug 就结束,而是要确保修复不会掩盖其他路径的同类问题。异常分类那项 P2 值得在下次迭代中专门处理。