自定义登录页能显示,username、password 和 action 也一样,提交却失败,仍需核对隐藏字段和会话。Spring Security 的默认页替你做的事情,不一定已经包含在手写表单里。

当时少了隐藏的 _csrf

2019 年旧 OAuth2 服务中,默认登录页能继续拿到授权码,自定义页提交后却出现:

Full authentication is required to access this resource

比较表单发现,自定义页没有默认页中的 _csrf。后来取消 csrf().disable() 的注释,流程恢复。这说明配置改动影响了流程,但错误文本本身不是 CSRF 失败的完整证据,也可能受后续跳转或异常处理影响。

保留保护,让表单带上服务端 token

基于会话的登录表单同样需要 CSRF 防护,token 不是前端随便生成一个随机值,而要与服务端保存的预期值相匹配。Spring Security 5.2 CSRF

下面是由 Thymeleaf 服务端渲染的表单结构,假设登录处理地址是 /login,且框架提供 _csrf 请求属性:

<form th:action="@{/login}" method="post">
  <label for="username">用户名</label>
  <input id="username" name="username" autocomplete="username">
  <label for="password">密码</label>
  <input id="password" name="password" type="password"
         autocomplete="current-password">
  <input type="hidden" th:name="${_csrf.parameterName}"
         th:value="${_csrf.token}">
  <button type="submit">登录</button>
</form>

不能把这段原样当静态 HTML 发布:th:* 必须经过模板引擎处理。Thymeleaf 与 Spring Security 集成也可能自动添加 CSRF 字段,应检查最终渲染结果,避免手工重复。JSP 项目应使用对应标签或服务端属性,不混用模板语法。

登录路径和会话分别确认

GET 登录页需可匿名访问,POST 应由配置的登录处理过滤器接收;页面 action 相同,不代表方法、上下文路径和处理链相同。提交时还要保持同一会话,核对 Cookie 是否受跨域、SameSite、域名或代理配置影响。

在浏览器网络面板查看渲染后的参数名和值、POST 的实际路径、请求 Cookie、响应状态及跳转,再结合服务端过滤器日志确定失败点。认证成功后还要检查授权跳转,不能只看表单响应一次成功。

不用全局关闭保护掩盖问题

permitAll 只涉及授权,不意味着 CSRF 自动失效。若个别无会话接口确实需要例外,应根据认证方式和威胁模型单独设计,不能因为登录调试困难就关闭全站保护。令牌和凭据也不应进入公开日志、URL 或截图。