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”和 spring-security,我们必须通过这个测试! :)
一个可能的解决方案: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();
}
有了这个微小的(但受管理的)“上下文持有者”:
package com.example.demo;
import lombok.*;
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class MyContentSecurityDelegate {
@Getter
@Setter
private String policyDirectives;
}
我们这样做(spring-java-config, 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 特定的请求(甚至线程)”,我们可以使用任何其他技术(匹配我们的堆栈和需求,超出 spring-security):
- 有或没有 spring-boot
servlet
有spring-mvc/无
javax.servlet.*
:
Any Servlet
, Filter
, or servlet *Listener
instance that is a Spring bean is registered with the embedded container..
来自 Registering Servlets, Filters, and Listeners as Spring Beans
或被动...
Mo' 链接:
- How can I add a filter class in Spring Boot?
- https://www.baeldung.com/spring-response-header
- https://www.baeldung.com/spring-boot-add-filter
编码愉快!
我正在尝试热重载我的 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”和 spring-security,我们必须通过这个测试! :)
一个可能的解决方案: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();
}
有了这个微小的(但受管理的)“上下文持有者”:
package com.example.demo;
import lombok.*;
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class MyContentSecurityDelegate {
@Getter
@Setter
private String policyDirectives;
}
我们这样做(spring-java-config, 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 特定的请求(甚至线程)”,我们可以使用任何其他技术(匹配我们的堆栈和需求,超出 spring-security):
- 有或没有 spring-boot
servlet
有spring-mvc/无
javax.servlet.*
:Any
Servlet
,Filter
, or servlet*Listener
instance that is a Spring bean is registered with the embedded container..来自 Registering Servlets, Filters, and Listeners as Spring Beans
或被动...
Mo' 链接:
- How can I add a filter class in Spring Boot?
- https://www.baeldung.com/spring-response-header
- https://www.baeldung.com/spring-boot-add-filter
编码愉快!