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
相关产品推荐
相关产品推荐

