Spring Security 配了 permitAll,为什么 POST 还是 403
permitAll 不跳过 CSRF 检查。默认会话应用先获取当前 token,再随表单或同源 JSON 请求提交;区分安全链、过期会话和请求体错误,保留 CSRF 防护。
GET 能访问,POST 却返回 403,明明该路径已经写了 permitAll()。先看请求是否被 CsrfFilter 拒绝:permitAll 允许通过授权判断,CSRF 检查仍会执行。浏览器使用会话 Cookie 时,POST 不能只带 Cookie,还要带当前会话对应的 CSRF token。
下面针对 Spring Boot 3、Spring Security 6 的 Servlet 应用,使用默认会话 token 存储与默认请求处理方式。自定义过 token repository、request handler,或者同时存在多个 SecurityFilterChain 的项目,应先确认命中的那条链;Cookie token 的单页应用配置不能直接套用这里的默认会话示例。
先确认 403 来自哪一步
在本机或隔离环境临时打开 CSRF 包的 DEBUG 日志:
logging.level.org.springframework.security.web.csrf=DEBUG
重现一次请求,按时间与路径查找 Invalid CSRF token found,或者项目异常处理里出现的 MissingCsrfTokenException、InvalidCsrfTokenException。日志会包含请求地址,诊断前去掉查询字符串中的敏感值,不记录 Cookie 或 token;检查结束后恢复原日志级别。
确认 CSRF 拒绝后再处理 token。若 CSRF 日志没有这个判断,继续检查实际 HTTP 方法、匹配到的安全链、路径授权、方法级权限和自定义过滤器。异常转发到 /error 也可能让原来的 400、404、415 被另一次权限拒绝遮住,应对照最早的异常,不能看到 403 就一律更改 CSRF。
POST 通过安全过滤器后仍返回 400,可能是 JSON、参数或校验失败;415 则要检查 Content-Type 与控制器接收格式。token 正确只说明这项校验通过,不会让请求体自动变得有效。
同源 JavaScript:先取 token,再提交 JSON
可以在现有 MVC 应用中提供一个按需取得 token 的端点。下面的控制器假定 Spring Security 的默认 CSRF 支持保持启用:
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CsrfEndpoint {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}
若登录前就需要提交表单,在已有的 authorizeHttpRequests 中为 GET /csrf 加上明确规则,放在兜底规则之前:
.requestMatchers(HttpMethod.GET, "/csrf").permitAll()
该片段使用 org.springframework.http.HttpMethod,不是新建另一条放行所有请求的安全链。业务 POST 是否公开,仍由原来的授权规则决定。获取 token 的响应不应进入共享缓存,不要为此允许任意跨站读取,也不要把 token 传到 URL 中。
同源页面可按下面的方式提交。/feedback 是示例业务路径,假定它已有 JSON POST 控制器,并按项目需求配置了授权;调用会产生实际业务请求,应在隔离环境替换为受控目标:
async function sendFeedback(message) {
const tokenResponse = await fetch("/csrf", {
credentials: "same-origin",
cache: "no-store"
});
if (!tokenResponse.ok) {
throw new Error(`读取 CSRF token 失败:${tokenResponse.status}`);
}
const csrf = await tokenResponse.json();
const response = await fetch("/feedback", {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
[csrf.headerName]: csrf.token
},
body: JSON.stringify({ message })
});
if (!response.ok) {
throw new Error(`提交失败:${response.status}`);
}
return response;
}
这里传回的是端点暴露的 token,头名称也从响应取得。默认会话存储通常使用 X-CSRF-TOKEN;不要硬套 Cookie token 方案的 X-XSRF-TOKEN。默认处理器会处理 token 的编码,客户端直接使用响应值,不自行解码或换成另一处缓存的值。
两个请求必须使用同一会话。请求跨站、Cookie 的 SameSite、Secure、域或路径设置不符合实际入口,都会让后一个请求拿不到原会话。不要为排错放宽全部 Cookie 或 CORS 规则;先在同源入口核对。应用部署在非根 context path 时,也要把示例地址调整为实际应用路径。
认证成功和退出成功会清除旧 token,客户端应重新获取;会话过期后也不能继续使用旧页面缓存的 token。遇到 403 不要自动重发所有业务 POST,先确认拒绝原因和业务是否已经执行,保留应用的幂等约束。
服务端表单,检查最终 HTML 中的隐藏字段
使用与 Spring 集成的 Thymeleaf 时,POST 表单可交给 th:action 处理,通常会自动加入 CSRF 隐藏字段。例如下面是模板片段,/feedback 在这个表单版本中应接收表单参数,而不是前面的 JSON 请求体:
<form th:action="@{/feedback}" method="post">
<input type="text" name="message">
<button type="submit">发送</button>
</form>
查看渲染后提交的表单数据,确认包含当前会话的 token。只有静态 action、页面没有经过模板引擎,或者自行拼接 HTML 时,不能靠模板语法期待 token 自动出现。需要手动加入时,应从当前请求暴露的 _csrf 取 parameterName 和 token,不要把一份固定 token 写进静态文件,也不要重复加入两份互相冲突的字段。
保留 CSRF 防护后,在隔离环境分别核对有效 token、缺失 token、其他会话的 token 和会话失效:有效请求应到达原业务处理,无效请求应继续被拒绝。若失败的是 WebSocket 握手,应另查Spring 允许的 Origin,HTTP POST 的 token 处理不能替代握手来源检查。


