Appearance
Spring MVC CORS 跨域资源共享完全指南 🌐
什么是 CORS?为什么需要它?
想象一下这样的场景:你正在银行网站(https://mybank.com
)上查看账户余额,同时在另一个标签页打开了一个恶意网站(https://evil.com
)。如果没有任何安全限制,恶意网站的 JavaScript 代码就可以向银行 API 发送请求,盗取你的账户信息甚至转账!😱
> **同源策略(Same-Origin Policy)** 是浏览器的一项重要安全机制,它禁止网页向不同源(协议、域名、端口任一不同)的服务器发送 AJAX 请求。这有效防止了恶意网站窃取用户数据。
但在现代 Web 开发中,我们经常需要进行跨域请求:
- 前端应用部署在
https://myapp.com
- 后端 API 部署在
https://api.myapp.com
- 或者需要调用第三方服务的 API
这时就需要 CORS(Cross-Origin Resource Sharing,跨域资源共享) 来解决这个问题!
CORS 工作原理
CORS 是 W3C 制定的标准,它通过 HTTP 头部信息来告诉浏览器:哪些跨域请求是被允许的。
NOTE
CORS 请求分为两种类型:
- 简单请求:直接发送,不需要预检
- 复杂请求:先发送 OPTIONS 预检请求,通过后再发送实际请求
Spring MVC 中的 CORS 配置
1. 使用 @CrossOrigin
注解
这是最简单直接的方式,可以在控制器类或方法上使用:
kotlin
@RestController
@RequestMapping("/api/users")
class UserController {
@CrossOrigin(origins = ["https://myapp.com"])
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): User {
// 只允许 https://myapp.com 访问此接口
return userService.findById(id)
}
@DeleteMapping("/{id}")
fun deleteUser(@PathVariable id: Long) {
// 此方法不允许跨域访问
userService.deleteById(id)
}
}
kotlin
@CrossOrigin(
origins = ["https://myapp.com", "https://admin.myapp.com"],
maxAge = 3600, // 预检请求缓存时间(秒)
allowCredentials = true // 允许携带凭证
)
@RestController
@RequestMapping("/api/products")
class ProductController {
@GetMapping
fun getAllProducts(): List<Product> {
// 继承类级别的 CORS 配置
return productService.findAll()
}
@CrossOrigin(origins = ["https://partner.com"])
@PostMapping
fun createProduct(@RequestBody product: Product): Product {
// 方法级别配置会与类级别配置合并
return productService.save(product)
}
}
> `@CrossOrigin` 注解的默认配置:
- 允许所有源(origins)
- 允许所有头部(headers)
- 允许控制器方法映射的所有 HTTP 方法
allowCredentials = false
maxAge = 30
分钟
2. 全局 CORS 配置
对于大型应用,推荐使用全局配置来统一管理 CORS 策略:
kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
// 为 API 接口配置 CORS
registry.addMapping("/api/**")
.allowedOrigins("https://myapp.com", "https://admin.myapp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count", "X-Page-Info") // 暴露自定义头部
.allowCredentials(true)
.maxAge(3600)
// 为静态资源配置更宽松的 CORS
registry.addMapping("/static/**")
.allowedOrigins("*")
.allowedMethods("GET")
.allowCredentials(false) // 静态资源不需要凭证
}
}
WARNING
当 allowCredentials = true
时,不能使用通配符 *
作为 allowedOrigins
,必须指定具体的域名。这是为了安全考虑。
3. 使用 CORS 过滤器
对于更复杂的场景,可以使用 CorsFilter
:
kotlin
@Configuration
class CorsConfig {
@Bean
fun corsFilter(): CorsFilter {
val config = CorsConfiguration().apply {
allowCredentials = true
addAllowedOrigin("https://myapp.com")
addAllowedOrigin("https://admin.myapp.com")
addAllowedHeader("*")
addAllowedMethod("*")
// 暴露自定义响应头部给前端
addExposedHeader("X-Total-Count")
addExposedHeader("X-Page-Info")
}
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", config)
}
return CorsFilter(source)
}
}
实际业务场景示例
场景 1:电商系统的前后端分离架构
kotlin
@RestController
@RequestMapping("/api/orders")
@CrossOrigin(
origins = ["https://shop.example.com", "https://admin.example.com"],
allowCredentials = true,
maxAge = 7200
)
class OrderController {
@GetMapping
fun getOrders(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int,
request: HttpServletRequest
): ResponseEntity<List<Order>> {
// 获取用户订单列表
val orders = orderService.getUserOrders(page, size)
// 设置分页信息到响应头(需要在 CORS 中暴露这些头部)
val headers = HttpHeaders().apply {
set("X-Total-Count", orders.totalElements.toString())
set("X-Page-Info", "${page}/${orders.totalPages}")
}
return ResponseEntity.ok().headers(headers).body(orders.content)
}
@PostMapping
fun createOrder(@RequestBody orderRequest: OrderRequest): Order {
// 创建新订单
return orderService.createOrder(orderRequest)
}
}
对应的全局 CORS 配置:
kotlin
@Configuration
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://shop.example.com", "https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization", "X-Requested-With")
.exposedHeaders("X-Total-Count", "X-Page-Info")
.allowCredentials(true)
.maxAge(3600)
}
}
场景 2:开放 API 平台
kotlin
@RestController
@RequestMapping("/open-api/v1")
class OpenApiController {
// 公开接口,允许所有域访问
@CrossOrigin(origins = ["*"], allowCredentials = false)
@GetMapping("/products")
fun getPublicProducts(): List<Product> {
return productService.getPublicProducts()
}
// 需要 API Key 的接口,限制特定域
@CrossOrigin(
origins = ["https://partner1.com", "https://partner2.com"],
allowCredentials = true
)
@GetMapping("/premium-data")
fun getPremiumData(@RequestHeader("X-API-Key") apiKey: String): PremiumData {
// 验证 API Key
if (!apiKeyService.isValid(apiKey)) {
throw UnauthorizedException("Invalid API Key")
}
return premiumDataService.getData()
}
}
常见问题与解决方案
问题 1:预检请求失败
kotlin
// ❌ 错误的配置
@CrossOrigin(methods = ["GET"])
@PostMapping("/api/data")
fun postData(@RequestBody data: String): String {
return "OK"
}
kotlin
// ✅ 正确的配置
@CrossOrigin(
origins = ["https://myapp.com"],
methods = ["GET", "POST", "OPTIONS"]
)
@PostMapping("/api/data")
fun postData(@RequestBody data: String): String {
return "OK"
}
TIP
预检请求使用 OPTIONS 方法,确保在 allowedMethods
中包含 OPTIONS
。
问题 2:凭证请求配置错误
kotlin
@CrossOrigin(
origins = ["*"],
allowCredentials = true
)
@GetMapping("/api/user-info")
fun getUserInfo(): UserInfo {
// 这种配置会导致 CORS 错误
return userService.getCurrentUser()
}
kotlin
@CrossOrigin(
origins = ["https://myapp.com"],
allowCredentials = true
)
@GetMapping("/api/user-info")
fun getUserInfo(): UserInfo {
return userService.getCurrentUser()
}
WARNING
当 allowCredentials = true
时,origins
不能使用通配符 *
,必须指定具体的域名。
问题 3:自定义头部无法访问
kotlin
@RestController
class DataController {
@CrossOrigin(
origins = ["https://myapp.com"],
exposedHeaders = ["X-Custom-Header", "X-Total-Count"]
)
@GetMapping("/api/data")
fun getData(): ResponseEntity<List<Data>> {
val data = dataService.findAll()
val headers = HttpHeaders().apply {
set("X-Custom-Header", "custom-value")
set("X-Total-Count", data.size.toString())
}
return ResponseEntity.ok().headers(headers).body(data)
}
}
NOTE
默认情况下,浏览器只能访问标准的响应头部。如果需要访问自定义头部,必须在 exposedHeaders
中明确指定。
安全最佳实践
1. 最小权限原则
kotlin
// ✅ 推荐:精确配置
@CrossOrigin(
origins = ["https://myapp.com", "https://admin.myapp.com"], // 具体域名
methods = ["GET", "POST"], // 只允许需要的方法
allowedHeaders = ["Content-Type", "Authorization"], // 只允许需要的头部
allowCredentials = true,
maxAge = 3600
)
kotlin
// ❌ 不推荐:过于宽松
@CrossOrigin(
origins = ["*"],
methods = ["*"],
allowedHeaders = ["*"],
allowCredentials = false
)
2. 环境相关配置
kotlin
@Configuration
class CorsConfig {
@Value("${app.cors.allowed-origins}")
private lateinit var allowedOrigins: List<String>
@Value("${app.cors.allow-credentials:false}")
private var allowCredentials: Boolean = false
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins(*allowedOrigins.toTypedArray())
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(allowCredentials)
.maxAge(3600)
}
}
配置文件:
yaml
app:
cors:
allowed-origins:
- http://localhost:3000
- http://localhost:8080
allow-credentials: true
yaml
app:
cors:
allowed-origins:
- https://myapp.com
- https://admin.myapp.com
allow-credentials: true
3. 与 Spring Security 集成
kotlin
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration().apply {
allowedOrigins = listOf("https://myapp.com")
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
allowedHeaders = listOf("*")
allowCredentials = true
}
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.cors { it.configurationSource(corsConfigurationSource()) }
.csrf { it.disable() }
.build()
}
}
IMPORTANT
如果使用 Spring Security,建议使用 Security 的 CORS 支持,而不是单独配置 CorsFilter
,避免配置冲突。
总结
CORS 是现代 Web 应用中不可或缺的安全机制。Spring MVC 提供了多种灵活的 CORS 配置方式:
@CrossOrigin
注解:适合简单场景,配置直观- 全局配置:适合大型应用,统一管理
- CORS 过滤器:适合复杂场景,最大灵活性
选择合适的配置方式,遵循安全最佳实践,就能在保证安全的前提下,实现灵活的跨域资源共享!🎉
TIP
在开发过程中,可以先使用宽松的 CORS 配置进行调试,但在生产环境中务必采用严格的安全配置。