Support customizing CORS configuration (#6981)

#### What type of PR is this?

/kind improvement
/area core
/milestone 2.20.x

#### What this PR does / why we need it:

This PR adds CorsOptions into SecurityProperties to let users customize their own CORS configuration. e.g.:

```yaml
halo:
  security:
    cors-options:
      disabled: false
      configs:
        - pathPattern: /apis/first.api.halo.run/v1alpha1/**
          config:
            allowedOrigins: [ "*" ]
            allowedHeaders: [ "*" ]
            allowedMethods: [ "*" ]
            exposedHeaders: [ "*" ]
            allowCredentials: true
            maxAge: 30m
        - pathPattern: /apis/second.api.halo.run/v1alpha1/**
          config:
            allowedOrigins: [ "www.halo.run", "www.lxware.cn" ]
            allowedHeaders: [ "Content-Type", "Authorization" ]
            allowedMethods: [ "GET, POST, PUT, DELETE" ]
            allowedCredentials: false
            maxAge: 1h
```

#### Does this PR introduce a user-facing change?

```release-note
支持自定义跨域配置
```
pull/7010/head
John Niang 2024-10-31 16:53:07 +08:00 committed by GitHub
parent 0c1849fdd5
commit 4dbfb930bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 44 additions and 3 deletions

View File

@ -7,6 +7,7 @@ import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.Data; import lombok.Data;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy;
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode; import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode;
@ -17,6 +18,8 @@ public class SecurityProperties {
private final ReferrerOptions referrerOptions = new ReferrerOptions(); private final ReferrerOptions referrerOptions = new ReferrerOptions();
private final CorsOptions corsOptions = new CorsOptions();
private final RememberMeOptions rememberMe = new RememberMeOptions(); private final RememberMeOptions rememberMe = new RememberMeOptions();
private final TwoFactorAuthOptions twoFactorAuth = new TwoFactorAuthOptions(); private final TwoFactorAuthOptions twoFactorAuth = new TwoFactorAuthOptions();
@ -43,6 +46,24 @@ public class SecurityProperties {
} }
@Data
public static class CorsOptions {
private boolean disabled;
private final List<CorsConfig> configs = new ArrayList<>();
}
@Data
public static class CorsConfig {
private String pathPattern;
private CorsEndpointProperties config;
}
@Data @Data
public static class FrameOptions { public static class FrameOptions {

View File

@ -8,17 +8,39 @@ import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.properties.SecurityProperties;
import run.halo.app.security.authentication.SecurityConfigurer; import run.halo.app.security.authentication.SecurityConfigurer;
@Component @Component
@Order(0) @Order(0)
public class CorsConfigurer implements SecurityConfigurer { public class CorsConfigurer implements SecurityConfigurer {
private final SecurityProperties.CorsOptions corsOptions;
public CorsConfigurer(HaloProperties haloProperties) {
corsOptions = haloProperties.getSecurity().getCorsOptions();
}
@Override @Override
public void configure(ServerHttpSecurity http) { public void configure(ServerHttpSecurity http) {
http.cors(spec -> spec.configurationSource(apiCorsConfigSource())); http.cors(spec -> {
if (corsOptions.isDisabled()) {
spec.disable();
return;
}
spec.configurationSource(apiCorsConfigSource());
});
} }
CorsConfigurationSource apiCorsConfigSource() { CorsConfigurationSource apiCorsConfigSource() {
var source = new UrlBasedCorsConfigurationSource();
// additional CORS configuration
this.corsOptions.getConfigs().forEach(corsConfig -> source.registerCorsConfiguration(
corsConfig.getPathPattern(), corsConfig.getConfig().toCorsConfiguration()
));
// default CORS configuration
var configuration = new CorsConfiguration(); var configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedHeaders( configuration.setAllowedHeaders(
@ -26,8 +48,6 @@ public class CorsConfigurer implements SecurityConfigurer {
"X-XSRF-TOKEN", HttpHeaders.COOKIE)); "X-XSRF-TOKEN", HttpHeaders.COOKIE));
configuration.setAllowCredentials(true); configuration.setAllowCredentials(true);
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH"));
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration); source.registerCorsConfiguration("/api/**", configuration);
source.registerCorsConfiguration("/apis/**", configuration); source.registerCorsConfiguration("/apis/**", configuration);
return source; return source;