You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Security错误页Content-Security-Policy nonce不匹配问题

问题描述

普通Thymeleaf模板页面中可正常使用Content-Security-Policy nonce机制,但在404等自定义错误页(同样基于Thymeleaf模板生成)中,作为模型属性传入页面的nonce值与Content-Security-Policy HTTP响应头中指定的nonce值不匹配,触发内容安全策略违规,导致自定义错误页出现脚本加载错误。Chrome控制台报错信息如下:

Content Security Policy: The page’s settings blocked the loading of a resource at http://localhost:8080/webjars/jquery/3.6.0/jquery.min.js (“script-src”).
Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”).

现有实现代码

Spring Security CSP基础配置

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return
        http.headers()
            .contentSecurityPolicy("script-src 'strict-dynamic' 'nonce-{nonce}'")
            .and().and()
            .addFilterBefore(new ContentSecurityPolicyNonceFilter(), HeaderWriterFilter.class)
            .build();
}

自定义CSP nonce生成过滤器

public class ContentSecurityPolicyNonceFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        var nonceArray = new byte[32];
        (new SecureRandom()).nextBytes(nonceArray);

        var nonce = Base64Utils.encodeToString(nonceArray);
        request.setAttribute("cspNonce", nonce);

        chain.doFilter(request, new NonceResponseWrapper((HttpServletResponse) response, nonce));
    }
}

Nonce响应包装类

class NonceResponseWrapper extends HttpServletResponseWrapper {
    private final String nonce;

    NonceResponseWrapper(HttpServletResponse response, String nonce) {
        super(response);
        this.nonce = nonce;
    }

    private String getHeaderValue(String name, String value) {
        final String retVal;

        if (name.equals("Content-Security-Policy") && StringUtils.hasText(value)) {
            retVal = value.replace("{nonce}", nonce);
        } else {
            retVal = value;
        }

        return retVal;
    }

    @Override
    public void setHeader(String name, String value) {
        super.setHeader(name, getHeaderValue(name, value));
    }

    @Override
    public void addHeader(String name, String value) {
        super.addHeader(name, getHeaderValue(name, value));
    }
}

全局Nonce注入逻辑

nonce值通过ControllerAdvice全局注入页面模型,代码如下:

@ControllerAdvice
public class ContentSecurityPolicyControllerAdvice {

    @ModelAttribute
    public void addAttributes(Model model, HttpServletRequest request) {
        model.addAttribute("nonce", request.getAttribute("cspNonce"));
    }
}
页面模板实现

正常运行的首页与异常的错误页在Thymeleaf、HTML层面遵循完全相同的编写规则。

正常首页index.html

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Application</title>

        <script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}" th:nonce="${nonce}"></script>

        <script th:inline="javascript" th:nonce="${nonce}">
            const randomNumber = /*[[${randomNumber}]]*/ -1;

            $(function() {
                $('#a-number').text(randomNumber);
            });
        </script>
    </head>
    <body>
        <h1>Welcome</h1>

        <p>Your random number is <span id="a-number">unknown</span>.</p>
    </body>
</html>

404错误页error/404.html

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>404 Error</title>

        <script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}" th:nonce="${nonce}"></script>

        <script th:nonce="${nonce}">
            $(function() {
                const timestampString = new Date().toISOString();
                $('#timestamp').text(timestampString);
            });
        </script>
    </head>
    <body>
        <h1>404 - Page Not Found</h1>

        <p>The current time is <span id="timestamp">unknown</span>.</p>
    </body>
</html>
问题排查日志

加载无效URL触发404时的应用调试输出如下:

Nonce for request = qPhdJiUAAkKHrwQBvxzxUz0OUUU4UXaxLcDErhl4g7U=
Content-Security-Policy = script-src 'strict-dynamic' 'nonce-qPhdJiUAAkKHrwQBvxzxUz0OUUU4UXaxLcDErhl4g7U='
Nonce for request = OiZmhtGlYMgb4X+pcFIwM41GzEkre3YvfkLCHFqoqIU=
Nonce for view model = OiZmhtGlYMgb4X+pcFIwM41GzEkre3YvfkLCHFqoqIU=
Nonce for request = sCbXWXA0TPjw+I/dui2bmee1vKKXG1Y2Xt3G7JkuZ04=
Content-Security-Policy = script-src 'strict-dynamic' 'nonce-sCbXWXA0TPjw+I/dui2bmee1vKKXG1Y2Xt3G7JkuZ04='
Nonce for request = hsGwh4+5oqg0W51zNprrT41rHnEeJRdHHO8KTMCSwL8=
Content-Security-Policy = script-src 'strict-dynamic' 'nonce-hsGwh4+5oqg0W51zNprrT41rHnEeJRdHHO8KTMCSwL8='

本次运行中,插入到CSP响应头的nonce值为qPhdJiUAAkKHrwQBvxzxUz0OUUU4UXaxLcDErhl4g7U=,但页面从视图模型获取的nonce值为OiZmhtGlYMgb4X+pcFIwM41GzEkre3YvfkLCHFqoqIU=,二者不一致。
已提供可通过./gradlew bootRun直接运行的最小复现代码,供排查参考,寻求该问题的根因与解决方案。


内容的提问来源于stack exchange,提问作者user19281570

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 21:27:18