自从从 Spring Boot 2.x 升级到 3.x 以来,我遇到了与 CORS 相关的问题。我在这里将其归结为要点。
RegexCorsConfiguration.java
/**
* Extend the traditional CORS origin check (equalsIgnoreCase) with a regular expression check.
*
* @author Lorent Lempereur <[email protected]>
* Source: https://github.com/looorent/spring-security-jwt/blob/master/src/main/java/be/looorent/security/jwt/RegexCorsConfiguration.java
*/
public class RegexCorsConfiguration extends CorsConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(RegexCorsConfiguration.class);
private final List<Pattern> allowedOriginsRegex;
public RegexCorsConfiguration() {
allowedOriginsRegex = new ArrayList<>();
}
public RegexCorsConfiguration(CorsConfiguration other) {
this();
setAllowCredentials(other.getAllowCredentials());
setAllowedOrigins(other.getAllowedOrigins());
setAllowedHeaders(other.getAllowedHeaders());
setAllowedMethods(other.getAllowedMethods());
setExposedHeaders(other.getExposedHeaders());
setMaxAge(other.getMaxAge());
}
@Override
public void addAllowedOrigin(String origin) {
super.addAllowedOrigin(origin);
try {
allowedOriginsRegex.add(Pattern.compile(origin));
} catch (PatternSyntaxException e) {
LOGGER.warn(
"Wrong syntax for the origin {} as a regular expression. If this origin is not supposed to be a regular expression, just ignore this error.",
origin);
}
}
@Override
public String checkOrigin(String requestOrigin) {
final String result = super.checkOrigin(requestOrigin);
return result != null ? result : checkOriginWithRegularExpression(requestOrigin);
}
private String checkOriginWithRegularExpression(String requestOrigin) {
return allowedOriginsRegex.stream()
.filter(pattern -> pattern.matcher(requestOrigin).matches())
.map(pattern -> requestOrigin)
.findFirst()
.orElse(null);
}
@Override
public CorsConfiguration combine(CorsConfiguration other) {
if (other == null) {
return this;
}
final CorsConfiguration config = new RegexCorsConfiguration(this);
config.setAllowedOrigins(combineInternal(this.getAllowedOrigins(), other.getAllowedOrigins()));
config.setAllowedMethods(combineInternal(this.getAllowedMethods(), other.getAllowedMethods()));
config.setAllowedHeaders(combineInternal(this.getAllowedHeaders(), other.getAllowedHeaders()));
config.setExposedHeaders(combineInternal(this.getExposedHeaders(), other.getExposedHeaders()));
final Boolean allowCredentials = other.getAllowCredentials();
if (allowCredentials != null) {
config.setAllowCredentials(allowCredentials);
}
final Long maxAge = other.getMaxAge();
if (maxAge != null) {
config.setMaxAge(maxAge);
}
return config;
}
private List<String> combineInternal(List<String> source, List<String> other) {
if (other == null || other.contains(ALL)) {
return source;
}
if (source == null || source.contains(ALL)) {
return other;
}
final Set<String> combined = new HashSet<>(source);
combined.addAll(other);
return new ArrayList<>(combined);
}
}
示例WebSecurityAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@EnableAspectJAutoProxy
@EnableWebSecurity(debug = true)
public class ExampleWebSecurityAutoConfiguration {
@Bean(name="com.example.web.security.autoconfigure.ExampleWebSecurityAutoConfiguration")
@Order(50) // allow other implementations to take precedence
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(request -> request
.anyRequest().authenticated()
)
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults());
return http.build();
}
// exclude HandlerMappingIntrospector which implements a default Spring CORS policy. We only want to exclude this
// configuration when a user includes their own customized CORS policy
@Bean
@ConditionalOnMissingBean(ignored = HandlerMappingIntrospector.class)
public CorsConfigurationSource corsConfigurationSource() {
final RegexCorsConfiguration config = new RegexCorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin(".*?://(.+\\.)*example\\.com");
config.addAllowedMethod(CorsConfiguration.ALL);
config.addAllowedHeader(CorsConfiguration.ALL);
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
示例WebSecurityAutoConfigurationTests.java
@Tag("integration")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {
ExampleWebSecurityAutoConfiguration.class
})
@ContextConfiguration(classes = {
ExampleWebSecurityAutoConfigurationTests.Config.class,
ExampleWebSecurityAutoConfigurationTests.SecurityConfig.class
})
@DirtiesContext
class ExampleWebSecurityAutoConfigurationTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testCors() {
final URI uri = restTemplate.getRestTemplate().getUriTemplateHandler().expand("/unauthenticated");
final ResponseEntity<Void> fromSubdomain = restTemplate.exchange(
RequestEntity.options(uri)
.header(HttpHeaders.ORIGIN, "http://stackoverflow.example.com")
.build(),
Void.class);
assertThat(fromSubdomain.getStatusCode())
.isEqualTo(HttpStatus.OK);
final ResponseEntity<Void> fromRoot = restTemplate.exchange(
RequestEntity.options(uri)
.header(HttpHeaders.ORIGIN, "http://example.com")
.build(),
Void.class);
assertThat(fromRoot.getStatusCode())
.isEqualTo(HttpStatus.OK);
final ResponseEntity<Void> fromOther = restTemplate.exchange(
RequestEntity.options(uri)
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(HttpHeaders.ORIGIN, "http://stackoverflow.com")
.build(),
Void.class);
assertThat(fromOther.getStatusCode())
.isEqualTo(HttpStatus.FORBIDDEN);
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@EnableMethodSecurity
static class Config {
@RestController
@RequestMapping
public static class Controller {
@GetMapping("/unauthenticated")
public void test() {
}
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableMethodSecurity
static class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.securityMatchers(matchers -> matchers.requestMatchers("/unauthenticated"))
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.authorizeHttpRequests(requests -> requests
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/unauthenticated").permitAll()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
}
第三个断言针对 CORS 违规,应返回 FORBIDDEN,但由于返回 OK 而失败。我已经尝试添加一些没有效果的东西(@EnableMethodSecurity、securityMatchers、dispatcherTypeMatchers)。
一旦创建了
SecurityFilterChain
,它就会与 cors 配置一起创建,您已将其指定为默认配置。这意味着您在其他地方指定什么并不重要,SecurityFilterChain
已经使用此 CORS 设置创建了。
而是将其与正确的 CORS 设置一起创建,例如:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.cors(c -> {
CorsConfigurationSource source = request -> {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:4200"));
configuration.setAllowedMethods(List.of("GET", "POST"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
return configuration;
};
c.configurationSource(source);
})
// add whatever other configuration that you have
.build();
}