提问者:小点点

Spring Boot:如何在运行时更改内容安全策略?


我正在尝试重新加载Spring Boot应用程序的内容安全策略(CSP)的更改,即用户应该能够通过管理员UI更改它而无需重新启动服务器。

Spring Boot中的常规方法是:

@Configuration
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) {
        // ... lots more config here...
        http.headers()
            .addHeaderWriter(
                 StaticHeadersWriter(
                     "Content-Security-Policy", 
                     "<some policy string>"
                 )
            )
    } 
}

…但是一旦被分配,这就不允许重新配置。

我可以在运行时使其(重新)可配置吗?重新加载应用程序上下文不是一个选项,我需要能够只适应这个特定的设置。


共2个答案

匿名用户

Easy-PEasy,我们只需要将(n适当的)HeaderWriter公开为bean!ContentSecurityPolicHeaderWriter看起来很合适

private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";

@Bean
public ContentSecurityPolicyHeaderWriter myWriter(
        @Value("${#my.policy.directive:DEFAULT_SRC_SELF_POLICY}") String initalDirectives
) {
  return new ContentSecurityPolicyHeaderWriter(initalDirectives);
}

然后用:

@Autowired
private ContentSecurityPolicyHeaderWriter myHeadersWriter;

@Override
public void configure(HttpSecurity http) throws Exception {
  // ... lots more config here...
  http.headers()
    .addHeaderWriter(myHeadersWriter);
}

…,我们可以使用这些演示控制器更改标头值:

@GetMapping("/")
public String home() {
  myHeadersWriter.setPolicyDirectives(DEFAULT_SRC_SELF_POLICY);
  return "header reset!";
}

@GetMapping("/foo")
public String foo() {
  myHeadersWriter.setPolicyDirectives("FOO");
  return "Hello from foo!";
}

@GetMapping("/bar")
public String bar() {
  myHeadersWriter.setPolicyDirectives("BAR");
  return "Hello from bar!";
}

我们可以测试:

@SpringBootTest
@AutoConfigureMockMvc
class DemoApplicationTests {

  @Autowired
  private MockMvc mockMvc;

  @Test
  public void testHome() throws Exception {
    this.mockMvc.perform(get("/"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("header reset!")))
            .andExpect(header().string(CONTENT_SECURITY_POLICY_HEADER, DEFAULT_SRC_SELF_POLICY));
  }

  @Test
  public void testFoo() throws Exception {
    this.mockMvc.perform(get("/foo"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Hello from foo!")))
            .andExpect(header().string(CONTENT_SECURITY_POLICY_HEADER, "FOO"));
  }

  @Test
  public void testBar() throws Exception {
    this.mockMvc.perform(get("/bar"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Hello from bar!")))
            .andExpect(header().string(CONTENT_SECURITY_POLICY_HEADER, "BAR"));
  }
}

…也在浏览器中:

所有在一个github。(对不起,所有在主类!:)

裁判:只有这个

匿名用户

(我)接受的答案的问题是:

(只是为了展示案例,但是:)我们在(每个)请求上修改“单例范围属性”!!!

当我们添加这样的“压力”测试包装时。

(…等到所有线程在java完成它们的工作??-

它严重失败(标头没有“预期”值):

@Test
void testParallel() throws Exception {
  // 200 cycles, with  0 (== #cpu) threads ...
  final StressTester<Void> stressTestHome = new StressTester<>(Void.class, 200, 0, // ... and these (three) jobs (firing requests at our app):
    () -> {
      home(); // here the original tests
      return null;
    },
    () -> {
      foo(); // ... with assertions ...
      return null;
    },
    () -> {
      bar(); // ... moved to private (non Test) methods
      return null;
    }
  );
  stressTestHome.test(); // run it, collect it and:
  stressTestHome.printErrors(System.out);
  assertTrue(stressTestHome.getExceptionList().isEmpty());
}

与模拟一样,与(完整)服务器模式一样…;(;(;(

当我们想从较低的作用域(比单例…所以任何其他作用域)更改该标头时,我们会遇到同样的问题:) ;(;(;(

如果我们想要该标头的单例范围策略,并且只触发重新加载(对于所有后续请求),我们可以停止阅读。(答案1是确定的,因为我实际上“最初理解”了这个问题

但是如果我们想要带有spring-security的“per request header”,我们必须通过这个测试!:)

一种可能的解决方案:方法注入!

所以回到我们自定义的HeaderWriter实现:

package com.example.demo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.header.HeaderWriter;
// abstract!
public abstract class MyContentSecurityPolicyHeaderWriter implements HeaderWriter {
  // ... no state!!!
  public static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";

  public static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";

  @Override // how cool, that there is a HttpServletRequest/-Response "at hand" !?!
  public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
    if (!response.containsHeader(CONTENT_SECURITY_POLICY_HEADER)) {
      // responsible for the header key, but for the value we ask: delegate
      response.setHeader(CONTENT_SECURITY_POLICY_HEADER, policyDelegate().getPolicyDirectives());
    }
  }

  // TLDR xDxD
  protected abstract MyContentSecurityDelegate policyDelegate();
}

再次感谢!;)

有了这个微小的(但受管理的)“上下文持有者”:

package com.example.demo;
import lombok.*;

@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class MyContentSecurityDelegate {

  @Getter
  @Setter
  private String policyDirectives;
}

我们这样做(使用spring-java-config,如何使用@Bean创建bean在Spring启动抽象类):

@Configuration 
class FreakyConfig {

  @Value("${my.policy.directive:DEFAULT_SRC_SELF_POLICY}")
  private String policy;

  @Bean
  @RequestScope // !! (that is suited for our controllers)
  public MyContentSecurityDelegate delegate() {
    return MyContentSecurityDelegate.of(policy);
  }

  @Bean
  public MyContentSecurityPolicyHeaderWriter myWriter() {
    return new MyContentSecurityPolicyHeaderWriter() { // anonymous inner class
      @Override
      protected MyContentSecurityDelegate policyDelegate() {
        return delegate(); // with request scoped delegate.
      }
    };
  }
}

…然后我们的控制器这样做(自动配线

@Autowired // !
private MyContentSecurityDelegate myRequestScopedDelegate;

@GetMapping("/foo")
public String foo() {
  // !!
  myRequestScopedDelegate.setPolicyDirectives("FOO");
  return "Hello from foo!";
}

然后所有测试都通过!:)推送到(同一个)github。

但是为了实现目标:“写入标头请求(甚至线程)特定”,我们可以使用任何其他技术(匹配我们的堆栈

>

  • 带或不带Spring靴
    • servlet

      >

    • 带spring-mvc/不带

      java x. ser v let.*

      任何作为Spring bean的ServletFilter或servlet*Listener实例都向嵌入式容器注册。

      从将Servlet、过滤器和侦听器注册为Spring Bean

      或反应性…

      Mo'链接:

      • 如何在Spring Boot中添加过滤器类?
      • https://www.baeldung.com/spring-response-header
      • https://www.baeldung.com/spring-boot-add-filter

      编码快乐!