bigywater.tech
Spring · 114

记住我与登录失败防护

记住我到底把什么放进了 cookie?登出以后这个 cookie 还能用吗?重启服务器用户会不会被踢下线?连续输错密码有没有内置的锁定?这篇在 Spring Boot 4.0.8、Spring Security 7 上手工带 Cookie 逐个验证。

14 天
remember-me cookie 的 Max-Age(1209600 秒),值是 username:过期:SHA256:签名
登出仍有效
登出前保存的 remember-me cookie 登出之后原样发回去还是 200
30 次
连续 30 次错误密码后,第 31 次正确密码照样登录成功,框架默认没有锁定

rememberMe 的行为

实测 · 本机执行输出
  不开 rememberMe,登录时勾了「记住我」→ HTTP 302,Set-Cookie 的名字 ['JSESSIONID']
  开了 rememberMe,登录时没勾 → Set-Cookie 的名字 ['JSESSIONID']
  登录时勾了「记住我」→ HTTP 302,remember-me 这条 Set-Cookie 的属性:Expires=Thu, 05 Nov 2026 00:43:00 GMT; Max-Age=1209600; Path=/; HttpOnly
  cookie 值 base64 解码后有 4 段:用户名=alice,过期时间距现在 14.0 天,第 3 段是 SHA256,最后一段是签名(长度 64)
  只带 remember-me cookie(没有 JSESSIONID)GET /me → HTTP 200 RememberMeAuthenticationToken,name=alice,服务端给了新的 ['JSESSIONID']
  同一个浏览器 GET /sensitive(要求 fullyAuthenticated;现在的会话是 remember-me 恢复的)→ HTTP 302,Location http://127.0.0.1:9602/login
  用密码登录的那个会话 GET /sensitive → HTTP 200 敏感页:UsernamePasswordAuthenticationToken
  服务端改了 alice 的密码之后,拿旧的 remember-me cookie GET /me → HTTP 302,Location http://127.0.0.1:9602/login

配置加上勾选才会下发 cookie;恢复的身份是 RememberMeAuthenticationToken,过不了 fullyAuthenticated;改密码使旧 cookie 失效。

登出与重启

实测 · 本机执行输出
  POST /logout → HTTP 302,清除的 cookie ['remember-me=']
  登出之后,把登出前保存的 remember-me cookie 原样发回去 GET /me → HTTP 200 RememberMeAuthenticationToken,name=alice
  应用重启(没配 key)后,再用这个 cookie GET /me → HTTP 302,Location http://127.0.0.1:9603/login
  rememberMe 配了固定 key(用户的密码是固定的 bcrypt 哈希),应用重启后用重启前的 cookie GET /me → HTTP 200 RememberMeAuthenticationToken,name=alice
  用户密码在内存里写成 {noop}pw:登录前存的密码前缀 {noop},登录一次之后变成 {bcrypt}
  固定 key,但密码是 {noop}pw 的用户:重启后用重启前的 cookie GET /me → HTTP 302,Location http://127.0.0.1:9605/login

登出不会作废已保存的 cookie;默认 key 随机,重启后失效;固定 key 要求用户密码哈希不变。

默认没有锁定

实测 · 本机执行输出
  连续 30 次错误密码 → 全部是 ['302login?error']
  第 31 次用正确密码 → HTTP 302,Location http://127.0.0.1:9606/
  不存在的用户 nobody 登录失败后,登录页上的提示:「Invalid credentials」
  alice 密码错误,登录页上的提示:「Invalid credentials」

30 次错误密码后正确密码照样成功;登录页不区分用户是否存在。

自己做锁定

实测 · 本机执行输出
  第 1 次错误密码 → HTTP 302,Location http://127.0.0.1:9607/login?error,提示「Invalid credentials」
  第 2 次错误密码 → HTTP 302,Location http://127.0.0.1:9607/login?error,提示「Invalid credentials」
  第 3 次错误密码 → HTTP 302,Location http://127.0.0.1:9607/login?error,提示「Invalid credentials」
  第 4 次用正确密码 → HTTP 302,Location http://127.0.0.1:9607/login?error,提示「Invalid credentials」
  对照:不存在的用户 nobody → 提示「Invalid credentials」
  服务端收到的失败事件:AuthenticationFailureBadCredentialsEvent:Bad credentials | AuthenticationFailureBadCredentialsEvent:Bad credentials | AuthenticationFailureBadCredentialsEvent:Bad credentials | AuthenticationFailureLockedEvent:User account is locked | AuthenticationFailureBadCredentialsEvent:Bad credentials
  清掉失败计数之后用正确密码 → HTTP 302,Location http://127.0.0.1:9607/

监听失败事件计数、返回 accountLocked;页面提示不泄露锁定状态。

分步演示

记住我与登录失败防护
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动(Spring Security 7.0.7,表单登录,用户 alice 存在 InMemoryUserDetailsManager 里,密码是固定的 bcrypt 哈希,{noop} 的版本只用来演示密码升级)。HTTP 全部由 Python 手工发出并手工维护 Cookie;「重启」是把进程停掉再起一个同端口的新进程;锁定逻辑是我自己写的实验代码(内存里的计数 + accountLocked),不是框架内置的
    • 没有验证:签名的具体构成(我从「改密码、密码哈希变了就失效」和重启后 DEBUG 日志里的签名不匹配推断它包含密码,没有读源码);持久化令牌方案(PersistentTokenBasedRememberMeServices,JDBC 表);rememberMe 的 useSecureCookie、SameSite;cookie 被盗用时的防护(token 轮换);Basic 认证下的失败行为;时间窗口自动解锁;防暴力破解的限流(按 IP);登录时序差异(用户不存在时的耗时);日志与告警;WebFlux 里的 rememberMe
    京ICP备2026067502号-1京公网安备11010502063487号