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

Spring Boot: How to change the Content Security Policy at runtime?

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

Spring 引导中的常规方法是:

@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>"
                 )
            )
    } 
}

...但这不允许在分配后重新配置。

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

Easy-Peasy,我们只需要暴露一个(n个合适的)HeaderWriter作为bean即可! ContentSecurityPolicyHeaderWriter 对我们来说看起来合适且足够,但我们也可以自由地实现自定义:

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);
}

...,我们可以使用此演示控制器更改 header 值:

@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"));
  }
}

...也在浏览器中:

All in one github.(对不起,主要 class!:)


参考文献:only this

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

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

当我们添加一个“压力”测试包装器时like this

( ... wait until all threads finish their work in java ?? -> ExecutorCompletionService, 因为 Java:1.5;)

严重失败(header不是“预期”值):

@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());
}

模拟和(完整)服务器模式一样...;(;(;(

我们遇到同样的问题,当我们想要改变那个header from a "lower scope" (than singleton..所以任何其他范围:);(;( ;(

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

但是如果我们想要“根据请求header” ,我们必须通过这个测试! :)


一个可能的解决方案:Method Injection!

所以回到我们的自定义 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();
}

Thanks, again!;)

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

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

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

  @Getter
  @Setter
  private String policyDirectives;
}

我们这样做(, How to create bean using @Bean in spring boot for abstract class):

@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!";
}

那么所有测试都通过了! :) pushed to (same)github.


但要实现目标:“编写 header 特定的请求(甚至线程)”,我们可以使用任何其他技术(匹配我们的堆栈和需求,超出 ):

Mo' 链接:

编码愉快!