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

Spring Boot 3.4 Webflux配置动态Nonce的CSP时Header未设置问题

Spring Boot 3.4 Webflux 动态Nonce的CSP配置问题

我在Spring Boot 3.4 Webflux应用中配置带有动态Nonce的CSP(内容安全策略),编写了SecurityWebFilterChain配置、CSPFilter过滤器以及NonceResponseDecorator响应包装类,但发现CSP Header始终未被设置,相关代码如下:

安全链配置

@Bean
public SecurityWebFilterChain otherSecurityWebFilterChain(final ServerHttpSecurity http) {

    http
            .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/**"))
            .authorizeExchange(auth -> auth.anyExchange().permitAll())
            .headers(headers -> headers
                    .contentSecurityPolicy(csp ->
                            csp.policyDirectives("base-uri 'self'; form-action 'self'; script-src 'nonce-{nonce}' 'strict-dynamic' https:; object-src 'self';")
                    )
            )
            .addFilterBefore(new CSPFilter(), SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);

    return http.build();
}

CSP过滤器

public class CSPFilter implements WebFilter {


    @Override
    public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {

        final var request = exchange.getRequest();
        final var response = exchange.getResponse();

        final var nonce = this.generateNonce();
        
        final var decoratedResponse = new NonceResponseDecorator(response, nonce);

        request.getAttributes().put("nonce", nonce);

        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    private String generateNonce() {

        final var nonceArray = new byte[64];
        new SecureRandom().nextBytes(nonceArray);

        return Base64.getEncoder().encodeToString(nonceArray);
    }

}

响应包装类

class NonceResponseDecorator extends ServerHttpResponseDecorator {

    private final String nonce;

    public NonceResponseDecorator(final ServerHttpResponse delegate, final String nonce) {

        super(delegate);
        this.nonce = nonce;
    }

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

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

    @Override
    public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body) {

        this.setCspHeader();
        return super.writeWith(body);
    }

    @Override
    public Mono<Void> writeAndFlushWith(final Publisher<? extends Publisher<? extends DataBuffer>> body) {

        this.setCspHeader();
        return super.writeAndFlushWith(body);
    }

    private void setCspHeader() {

        final HttpHeaders headers = this.getDelegate().getHeaders();
        final String cspHeaderValue = headers.getFirst("Content-Security-Policy");
        if (cspHeaderValue != null) {
            headers.set("Content-Security-Policy", this.getHeaderValue("Content-Security-Policy", cspHeaderValue));
        }
    }

}

问题原因及解决方案

核心问题:过滤器顺序错误

当前代码将CSPFilter添加在SecurityWebFiltersOrder.HTTP_HEADERS_WRITER之前,而HTTP_HEADERS_WRITER是Spring Security负责写入配置好的CSP头的组件。这导致当CSPFilter包装响应时,CSP头还未被写入,后续NonceResponseDecorator无法读取到带占位符的CSP值进行替换,最终CSP头无法正确生成。

方案一:简化逻辑,直接在过滤器中生成CSP头

移除Security配置中的CSP策略定义,直接在过滤器中动态生成完整的CSP头,避免响应包装的时机问题:

修正后的安全链配置

@Bean
public SecurityWebFilterChain otherSecurityWebFilterChain(final ServerHttpSecurity http) {
    http
            .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/**"))
            .authorizeExchange(auth -> auth.anyExchange().permitAll())
            // 移除Security内置的CSP配置
            .addFilterAfter(new CSPFilter(), SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);

    return http.build();
}

修正后的CSP过滤器

public class CSPFilter implements WebFilter {

    @Override
    public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
        final String nonce = generateNonce();
        final ServerHttpResponse response = exchange.getResponse();
        
        // 直接生成完整的CSP策略,替换nonce占位符
        String cspPolicy = String.format(
            "base-uri 'self'; form-action 'self'; script-src 'nonce-%s' 'strict-dynamic' https:; object-src 'self';",
            nonce
        );
        response.getHeaders().set("Content-Security-Policy", cspPolicy);
        
        // 将nonce存入请求属性,供视图层使用(如Thymeleaf、Freemarker等)
        exchange.getAttributes().put("nonce", nonce);

        return chain.filter(exchange);
    }

    private String generateNonce() {
        final byte[] nonceArray = new byte[64];
        new SecureRandom().nextBytes(nonceArray);
        return Base64.getEncoder().encodeToString(nonceArray);
    }
}

方案二:保留响应包装,调整过滤器顺序

如果坚持使用响应包装的方式,只需调整过滤器的添加顺序,确保CSPFilter在HTTP_HEADERS_WRITER之后执行,保证CSP头已被写入响应:

修正后的安全链配置

@Bean
public SecurityWebFilterChain otherSecurityWebFilterChain(final ServerHttpSecurity http) {
    http
            .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/**"))
            .authorizeExchange(auth -> auth.anyExchange().permitAll())
            .headers(headers -> headers
                    .contentSecurityPolicy(csp ->
                            csp.policyDirectives("base-uri 'self'; form-action 'self'; script-src 'nonce-{nonce}' 'strict-dynamic' https:; object-src 'self';")
                    )
            )
            // 将过滤器放在HTTP_HEADERS_WRITER之后,确保CSP头已写入
            .addFilterAfter(new CSPFilter(), SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);

    return http.build();
}

此时原有的CSPFilter和NonceResponseDecorator无需修改,即可正常读取并替换CSP头中的占位符。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 14:17:15