Spring Boot with Kotlin provides multiple elegant ways to configure CORS. This guide covers modern, secure implementations for both reactive (WebFlux) and traditional (Spring MVC) applications.
Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.
Which approach should I use?
spring-boot-starter-webflux dependencyspring-boot-starter-web dependencyCheck your build.gradle.kts or pom.xml to determine which stack you're using.
For reactive applications using Spring WebFlux:
// CorsConfig.kt
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.reactive.CorsWebFilter
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
@Configuration
class CorsConfig {
@Bean
fun corsWebFilter(): CorsWebFilter {
val corsConfig = CorsConfiguration().apply {
// Specify allowed origins (NOT wildcard)
allowedOrigins = listOf(
"https://example.com",
"https://app.example.com"
)
// Allowed HTTP methods
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
// Allowed headers
allowedHeaders = listOf("Content-Type", "Authorization")
// Enable credentials
allowCredentials = true
// Preflight cache duration (seconds)
maxAge = 86400L
}
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", corsConfig)
}
return CorsWebFilter(source)
}
}
For traditional servlet-based Spring Boot applications:
// CorsConfig.kt
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration
class CorsConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins(
"https://example.com",
"https://app.example.com"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization")
.allowCredentials(true)
.maxAge(86400)
}
}
Apply CORS configuration to an entire controller:
// ApiController.kt
import org.springframework.web.bind.annotation.*
// Per-controller CORS
@RestController
@RequestMapping("/api")
@CrossOrigin(
origins = ["https://example.com", "https://app.example.com"],
methods = [
RequestMethod.GET,
RequestMethod.POST,
RequestMethod.PUT,
RequestMethod.DELETE
],
allowedHeaders = ["Content-Type", "Authorization"],
allowCredentials = "true",
maxAge = 86400
)
class ApiController {
@GetMapping("/data")
fun getData(): Map<String, String> {
return mapOf("message" to "CORS enabled")
}
// Override CORS for specific method
@CrossOrigin(origins = ["https://public.example.com"])
@GetMapping("/public")
fun getPublicData(): Map<String, String> {
return mapOf("message" to "Public endpoint")
}
}
Apply CORS to individual methods for fine-grained control:
// ApiController.kt
@RestController
@RequestMapping("/api")
class ApiController {
// CORS only on this specific method
@CrossOrigin(
origins = ["https://example.com"],
allowCredentials = "true"
)
@PostMapping("/secure")
fun secureEndpoint(@RequestBody data: Map<String, Any>): Map<String, String> {
return mapOf("status" to "success")
}
// No CORS on this method
@GetMapping("/internal")
fun internalEndpoint(): Map<String, String> {
return mapOf("message" to "Internal only")
}
}
When using Spring Security, configure CORS in your security configuration:
// SecurityConfig.kt
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.cors { cors ->
cors.configurationSource(corsConfigurationSource())
}
.csrf { it.disable() }
.authorizeHttpRequests { auth ->
auth.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
}
return http.build()
}
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration().apply {
allowedOrigins = listOf(
"https://example.com",
"https://app.example.com"
)
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
allowedHeaders = listOf("Authorization", "Content-Type")
allowCredentials = true
maxAge = 86400L
}
return UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", configuration)
}
}
}
Important: When using Spring Security, configure CORS before other security settings like CSRF. Place .cors { } as the first configuration in your security chain.
Use environment variables or profiles to manage allowed origins across environments:
// CorsConfig.kt
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration
class CorsConfig(
@Value("\${cors.allowed-origins}")
private val allowedOrigins: List<String>
) : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins(*allowedOrigins.toTypedArray())
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization")
.allowCredentials(true)
.maxAge(86400)
}
}
application.yml:
cors:
allowed-origins:
- https://example.com
- https://app.example.com
# Or with profiles
---
spring:
config:
activate:
on-profile: production
cors:
allowed-origins:
- https://example.com
---
spring:
config:
activate:
on-profile: development
cors:
allowed-origins:
- http://localhost:3000
- http://localhost:5173
How to tell which you're using:
// If you have spring-boot-starter-webflux
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
}
// Use WebFlux approach (Method 1)
// If you have spring-boot-starter-web
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
}
// Use MVC approach (Method 2)
WebFlux (Reactive):
CorsWebFilter beanorg.springframework.web.cors.reactive.* importsMVC (Traditional):
WebMvcConfigurerorg.springframework.web.servlet.config.annotation.* importsWhen multiple CORS configurations exist, they are applied in this order (highest to lowest priority):
More specific configurations override general ones.
Kotlin's DSL makes configuration more readable and concise:
// Clean Kotlin DSL with apply
val corsConfig = CorsConfiguration().apply {
allowedOrigins = listOf("https://example.com")
allowedMethods = listOf("GET", "POST")
allowCredentials = true
}
// vs Java
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Arrays.asList("https://example.com"));
corsConfig.setAllowedMethods(Arrays.asList("GET", "POST"));
corsConfig.setAllowCredentials(true);
// CorsTest.kt
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
@SpringBootTest
@AutoConfigureMockMvc
class CorsTest @Autowired constructor(
private val mockMvc: MockMvc
) {
@Test
fun `should allow whitelisted origin`() {
mockMvc.perform(
get("/api/data")
.header("Origin", "https://example.com")
)
.andExpect(status().isOk)
.andExpect(
header().string(
"Access-Control-Allow-Origin",
"https://example.com"
)
)
}
@Test
fun `should reject non-whitelisted origin`() {
mockMvc.perform(
get("/api/data")
.header("Origin", "https://evil.com")
)
.andExpect(status().isOk)
.andExpect(
header().doesNotExist("Access-Control-Allow-Origin")
)
}
@Test
fun `should handle preflight request`() {
mockMvc.perform(
options("/api/data")
.header("Origin", "https://example.com")
.header("Access-Control-Request-Method", "POST")
.header("Access-Control-Request-Headers", "Content-Type")
)
.andExpect(status().isOk)
.andExpect(
header().string(
"Access-Control-Allow-Methods",
containsString("POST")
)
)
}
}
Run tests with:
./gradlew test # or ./mvnw test
Add to application.yml to debug CORS issues:
logging:
level:
org.springframework.web.cors: DEBUG
org.springframework.security.web.cors: DEBUG
This combination is INVALID per CORS specification:
// INVALID - Will fail in browsers @CrossOrigin(origins = ["*"], allowCredentials = "true")
Must use specific origins:
// VALID
@CrossOrigin(
origins = ["https://example.com"],
allowCredentials = "true"
)
Make sure you're using the correct imports for your stack:
// WebFlux (Reactive) - CORRECT import org.springframework.web.cors.reactive.CorsWebFilter import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource // MVC (Servlet) - CORRECT import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
If you use Spring Security without CORS configuration, your CORS settings will be ignored. Always configure CORS in your Security Config when using Spring Security.
For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.
The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.
Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain