开启图形验证码后,登录接口一直返回“验证码已失效,请刷新后重试”。后端生成验证码时已经写入 HttpSession,但登录请求读取不到相同的 Session。

根因不是验证码内容过期,而是验证码请求与登录请求没有使用同一个 JSESSIONID:浏览器未保存 Cookie,或登录请求没有把 Cookie 带回 API。

一、验证码为什么依赖同一个 Session

① 前端请求 /captcha
        ↓
② 后端创建 Session,并把验证码写入 session["captcha"]
        ↓
③ 响应通过 Set-Cookie 返回 JSESSIONID
        ↓
④ 浏览器保存 Cookie
        ↓
⑤ 前端请求 /login,并携带相同 JSESSIONID
        ↓
⑥ 后端定位原 Session,读取验证码并校验

任一步没有完成,登录请求都会进入一个没有 captcha 属性的 Session。request.getSession() 还可能自动创建新 Session,使问题看起来像“验证码被清空”。


二、后端示例

生成验证码

登录时校验

  • 使用 getSession(false) 可以区分“Cookie 对应的 Session 不存在”和“验证码属性不存在”。
  • 校验成功后删除验证码,保证一次性使用。
  • 自行保存创建时间,才能实现明确的业务过期时间。

三、前端必须携带凭证

Axios 的跨源请求默认不会携带 Cookie。获取验证码与登录两个请求都必须使用 withCredentials。

只给登录请求设置 withCredentials 不够:如果验证码响应阶段没有凭证模式,浏览器可能直接忽略跨源响应中的 Set-Cookie。

四、Spring CORS 配置

  • 凭证请求必须返回 Access-Control-Allow-Credentials: true。
  • 启用凭证时,Access-Control-Allow-Origin 不能使用 *,应明确列出可信 Origin。
  • 如果使用 Spring Security,应确保 CORS 在安全过滤链中正确启用,并正确处理 OPTIONS 预检。

五、Cookie 的 SameSite 与 Secure

Origin 与 Site 不是同一个概念。协议、主机或端口不同会造成跨 Origin;SameSite 判断还会考虑 scheme。原来的前端 http://lvmaoya.cn:8520 与 API https://api.lvmaoya.cn 同时存在协议和 Origin 差异,因此更容易触发 Cookie 限制。

推荐:前后端统一 HTTPS

把前端部署为 https://lvmaoya.cn,API 保持 https://api.lvmaoya.cn。二者仍是跨 Origin,需要 CORS 与 withCredentials,但在常见浏览器的 schemeful same-site 判断中属于同站点。

确实需要跨站 Cookie 时

server.servlet.session.cookie.same-site=none
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.path=/
SameSite=None 必须同时设置 Secure,也就是通过 HTTPS 传输。HttpOnly 可以阻止前端 JavaScript 读取 Session Cookie。

六、最稳妥的部署方式:同源反向代理

更简单的方案是让浏览器始终请求 https://lvmaoya.cn/api,再由 Nginx 或 Caddy 转发到后端。对浏览器而言前端与 API 同源,可以减少 CORS 和跨站 Cookie 配置。

location /api/ {
    proxy_pass http://blog-api:8081/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

不建议把 proxy_cookie_path / "/; SameSite=None; Secure" 当作长期首选方案。它会批量改写代理响应中的 Cookie 属性,容易掩盖应用配置问题;应优先在 Spring Boot 中声明 Session Cookie 策略。


七、浏览器排查步骤

  1. 打开开发者工具 Network,清除旧 Cookie 后重新请求 /captcha。
  2. 检查响应是否包含 Set-Cookie: JSESSIONID=...,并查看浏览器是否给出拦截原因。
  3. 在 Application / Storage → Cookies 中确认 JSESSIONID 已保存到正确域名和路径。
  4. 请求 /login,检查 Request Headers 中是否出现 Cookie: JSESSIONID=...。
  5. 确认 /captcha 与 /login 请求携带的 JSESSIONID 完全相同。
  6. 检查 CORS 响应头是否返回明确 Origin 和 Access-Control-Allow-Credentials: true。
  7. 检查是否发生负载均衡、Session 未共享、服务重启或 Session 超时。

八、仍然失效时继续检查

  • Cookie 的 Domain 或 Path 与登录接口不匹配。
  • 前后端混用 HTTP 与 HTTPS,导致 Secure 或 SameSite 策略不满足。
  • 多实例部署未配置粘性会话,也没有使用 Spring Session / Redis 共享 Session。
  • 浏览器隐私设置或第三方 Cookie 策略阻止了 Cookie。
  • 重复刷新验证码覆盖了原值,多标签页使用同一 Session 时相互影响。
  • 验证码接口被 CDN 或浏览器缓存,应设置 no-store 并给图片请求增加缓存破坏参数。

九、安全补充

  • 验证码只能降低自动化攻击风险,不能替代登录限流、账号锁定和审计。
  • 基于 Cookie 的认证仍需评估 CSRF 防护,不能因为配置了 CORS 就忽略 CSRF。
  • 不要在日志中输出验证码、Session ID、密码或完整 Cookie。
  • 失败提示应避免泄露账号是否存在等敏感状态。
最终判断标准很简单:验证码请求设置的 JSESSIONID,必须在登录请求中原样返回,并由后端定位到同一个 Session。