[英]Spring Boot: How to change the Content Security Policy at runtime?
I'm trying to hot-reload a change in the content security policy (CSP) of my Spring Boot application, ie the user should be able to change it via an admin UI without restarting the server.我正在尝试热重新加载我的 Spring 引导应用程序的内容安全策略 (CSP) 的更改,即用户应该能够通过管理 UI 更改它而无需重新启动服务器。
The regular approach in Spring Boot is: 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>"
)
)
}
}
... but this doesn't allow for reconfiguration once it has been assigned. ...但是一旦分配就不允许重新配置。
Can I make this (re-)configurable at runtime?我可以在运行时使这个(重新)可配置吗? Reloading the application context is not an option, I need to be able to adapt only this particular setting.
重新加载应用程序上下文不是一个选项,我只需要能够调整这个特定的设置。
Easy-Peasy;十分简单; ;)
;)
We only need to implement our custom, smarter (singleton bean?;;) HeaderWriter
.我们只需要实现我们自定义的、更智能的(单例 bean?;;)
HeaderWriter
。 Unfortunately we cannot extend ContentSecurityPolicyHeaderWriter
, since it is final, but Ctrl+C , Ctrl+V ;), there we go:不幸的是,我们不能扩展
ContentSecurityPolicyHeaderWriter
,因为它是最终的,但是Ctrl+C , Ctrl+V ;),我们有 go:
@Getter // lombok
@Setter // lombok
@Component // (spring) for "cool things" ...
static final class MyContentSecurityPolicyHeaderWriter implements HeaderWriter {
public static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";
public static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";
// ...like this!
@Value("${my.policy.directive:DEFAULT_SRC_SELF_POLICY}")
private String policyDirectives;
/**
* Creates a new instance. Default value: default-src 'self'
*/
public MyContentSecurityPolicyHeaderWriter() {
setPolicyDirectives(DEFAULT_SRC_SELF_POLICY);
}
/**
* Creates a new instance
*
* @param policyDirectives maps to {@link #setPolicyDirectives(String)}
* @throws IllegalArgumentException if policyDirectives is null or empty
*/
public MyContentSecurityPolicyHeaderWriter(String policyDirectives) {
setPolicyDirectives(policyDirectives);
}
/**
* @see org.springframework.security.web.header.HeaderWriter#writeHeaders(jakarta.servlet.http.HttpServletRequest,
* jakarta.servlet.http.HttpServletResponse)
*/
@Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (!response.containsHeader(CONTENT_SECURITY_POLICY_HEADER)) {
response.setHeader(CONTENT_SECURITY_POLICY_HEADER, policyDirectives);
}
}
/**
* Sets the security policy directive(s) to be used in the response header.
*
* @param policyDirectives the security policy directive(s)
* @throws IllegalArgumentException if policyDirectives is null or empty
*/
public void setPolicyDirectives(String policyDirectives) {
Assert.hasLength(policyDirectives, "policyDirectives cannot be null or empty");
this.policyDirectives = policyDirectives;
}
@Override
public String toString() {
return getClass().getName() + " [policyDirectives=" + policyDirectives + "]";
}
}
(We deleted the report*
stuff, made it a @Component
, and the policyDirectives
accessible.) (我们删除了
report*
内容,将其设为@Component
,并且policyDirectives
可访问。)
Then with:然后:
@Autowired
private MyContentSecurityPolicyHeaderWriter myHeadersWriter;
@Override
public void configure(HttpSecurity http) throws Exception {
// ... lots more config here...
http.headers()
.addHeaderWriter(myHeadersWriter);
}
And this demo controllers:这个演示控制器:
@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!";
}
We can test:我们可以测试:
@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"));
}
}
... also in browser: ...也在浏览器中:
All in one github.多合一 github。 (sorry all in main class::)
(对不起所有主要的 class::)
The problem with the (my) accepted answer is: (我的)接受的答案的问题是:
(just for the show case, but:) We modify "singleton scope property" on (every) request!!! (仅用于展示案例,但是:)我们在(每个)请求上修改“单例 scope 属性”!!!
When we add a "stress" test wrapper like this .当我们像这样添加“压力”测试包装器时。
(... wait until all threads finish their work in java ?? -> ExecutorCompletionService , since Java:1.5;) (...等到所有线程在 java ?? -> ExecutorCompletionService中完成它们的工作,因为 Java:1.5;)
It badly fails (header has not the "expected" value):它严重失败(标题没有“预期”值):
@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());
}
As in mock as in (full) server mode... ;(;(;(在模拟和(完整)服务器模式中一样...... ;(;(;(
We will encounter the same problem, when we want to change that header from a "lower scope" (than singleton ..so any other scope:);(;(;(我们会遇到同样的问题,当我们想从“较低的范围”(比 singleton ..so 任何其他 scope:) 更改 header 时
If we want singleton scope policy for that header, and only "trigger the reload" (for all subsequent requests), we can stop reading.如果我们想要header 的 singleton scope 策略,并且只“触发重新加载”(对于所有后续请求),我们可以停止阅读。 (answer 1 is ok, as i actually "initially understood" the question & answered:)
(答案 1 没问题,因为我实际上“最初理解”了这个问题并回答了:)
But if we want that "per request header" with spring-security , we have to pass this test: :)但是如果我们想要spring-security的“每个请求标头” ,我们必须通过这个测试::)
One possible solution: Method Injection !一种可能的解决方案: 方法注入!
So back to our custom HeaderWriter
implementation:回到我们自定义的
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();
}
With this tiny (but managed) "context holder":有了这个小小的(但有管理的)“上下文持有者”:
package com.example.demo;
import lombok.*;
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class MyContentSecurityDelegate {
@Getter
@Setter
private String policyDirectives;
}
We do this (with spring-java-config ):我们这样做(使用spring-java-config ):
@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.
}
};
}
}
..then our controllers do that (autowire & "talk" to the delegate): ..然后我们的控制器会这样做(自动连接并与代表“交谈”):
@Autowired // !
private MyContentSecurityDelegate myRequestScopedDelegate;
@GetMapping("/foo")
public String foo() {
// !!
myRequestScopedDelegate.setPolicyDirectives("FOO");
return "Hello from foo!";
}
Then all tests pass: :) pushed to (same)github .然后所有测试都通过::)推送到(相同的)github 。
But to achieve the goal: "Write headers request (even thread) specific", we can use any other technique (matching our stack & needs, beyond spring-security ):但是为了实现目标:“写头请求(甚至线程)特定”,我们可以使用任何其他技术(匹配我们的堆栈和需求,除了spring-security ):
servlet小服务程序
with spring-mvc /without带spring-mvc /不带
javax.servlet.*
: javax.servlet.*
:
Any
Servlet
,Filter
, or servlet*Listener
instance that is a Spring bean is registered with the embedded container..任何作为 Spring bean的
Servlet
、Filter
或 servlet*Listener
实例都已注册到嵌入式容器。
from Registering Servlets, Filters, and Listeners as Spring Beans从将 Servlets、过滤器和侦听器注册为 Spring Bean
or reactive...或反应...
Mo' Links:莫链接:
Happy Coding!快乐编码!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.