DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

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 处理不能替代握手来源检查。