1. 什么是同源策略 (Same-Origin Policy, SOP)?
要理解跨域,首先必须理解同源策略。这是浏览器最核心、最基本的安全功能。
“源”(Origin) 由三个部分组成:
- 协议 (Protocol):例如
http
、https
- 域名 (Domain):例如
www.example.com
、api.example.com
- 端口 (Port):例如
80
、443
、8080
只有当这三者完全相同时,两个 URL才属于“同源”。
URL 1 | URL 2 | 是否同源 | 原因 |
---|---|---|---|
http://a.com:80/ | http://a.com:80/path/ | 是 | 协议、域名、端口都相同 |
http://a.com:80/ | https://a.com:80/ | 否 | 协议不同 (http vs https ) |
http://a.com:80/ | http://b.com:80/ | 否 | 域名不同 (a.com vs b.com ) |
http://a.com:80/ | http://a.com:8080/ | 否 | 端口不同 (80 vs 8080 ) |
同源策略的目的: 它的主要目的是防止恶意网站读取其他网站的敏感数据。想象一下,如果你在浏览器的一个标签页中登录了你的网上银行,然后在另一个标签页中打开了一个恶意网站。如果没有同源策略,那个恶意网站的 JavaScript 脚本就可以向你的网上银行发送请求,并读取返回的 JSON 数据(比如你的账户余额、交易记录等),然后将这些数据发送到它自己的服务器。同源策略阻止了这种行为的发生。
同源策略的限制: 浏览器会限制JS脚本发起的跨源 HTTP 请求。具体来说,一个源的脚本不能读取另一个源的响应数据。
浏览器并不会阻止请求的发送。对于某些类型的请求,请求实际上已经到达了服务器,服务器也处理了并返回了响应。但是,浏览器在接收到响应后,会检查其来源,如果发现是跨源的,并且没有得到服务器的明确许可,就会拦截这个响应,不允许你的 js代码访问它,并在控制台抛出跨域错误。
2. 为什么需要跨域?
在现代 Web 开发中,前后端分离架构(如 Vue/React/Angular + Spring Boot)和微服务架构非常普遍。
- 前后端分离:前端应用(例如运行在
http://localhost:8080
)需要调用后端 API(例如运行在http://localhost:9090/api
)。这显然是跨域的(端口不同)。 - 微服务:一个主应用(
https://app.com
)可能需要调用不同子域下的服务,比如用户服务(https://user-api.com
)和订单服务(https://order-api.com
)。这也是跨域的。
因此,我们需要一种安全、可控的方式来“绕过”同源策略的限制。
3. CORS (Cross-Origin Resource Sharing) 跨域资源共享
CORS 是一种 W3C 标准,它允许服务器明确声明哪些源(Origin)有权限访问其资源。它不是去“关闭”同源策略,而是为同源策略增加了一套“白名单”机制。
CORS 的工作原理: CORS 通过一系列HTTP 头部字段来工作。浏览器和服务器通过这些头部字段进行“协商”,以确定请求是否安全。
A. 简单请求 (Simple Requests)
如果一个请求同时满足以下所有条件,它就是一个“简单请求”:
- 请求方法是以下三者之一:
GET
HEAD
POST
- HTTP 头部不超出以下几种字段:
Accept
Accept-Language
Content-Language
Content-Type
(值仅限于application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)
- 请求中没有自定义的 HTTP 头部(例如
Authorization
)。
简单请求的流程: - 浏览器在请求头中自动添加一个
Origin
字段,表明请求来自哪个源。
GET /data HTTP/1.1
Host: api.example.com
Origin: http://ui.another.com
- 服务器收到请求后,检查
Origin
字段的值。 - 如果服务器允许这个源的请求,它会在响应头中添加一个
Access-Control-Allow-Origin
字段,并将其值设为请求的Origin
值(或者*
表示允许所有源)。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://ui.another.com
Content-Type: application/json {"data": "some value"}
- 浏览器收到响应后,检查
Access-Control-Allow-Origin
头部。如果值匹配或者为*
,就将响应数据交给 JavaScript 处理。否则,浏览器会拦截响应,并在控制台报错。
B. 非简单请求 (Preflighted Requests)
不满足“简单请求”条件的都是“非简单请求”。常见的例子包括:
- 使用了
PUT
,DELETE
,PATCH
等方法的请求。 Content-Type
为application/json
的POST
请求。- 带有自定义请求头(如
Authorization: Bearer ...
)的请求。
非简单请求的流程(包含“预检”请求): 在发送实际请求之前,浏览器会自动发送一个“预检”(Preflight)请求。这是一个OPTIONS
方法的请求。
- 预检请求 (OPTIONS Request): 浏览器向服务器发送一个
OPTIONS
请求,其中包含了几个关键的头部:Origin
: 表明请求来源。Access-Control-Request-Method
: 告知服务器,实际请求将使用哪种 HTTP 方法。Access-Control-Request-Headers
: 告知服务器,实际请求将携带哪些自定义头部。
OPTIONS /data/123 HTTP/1.1
Host: api.example.com
Origin: http://ui.another.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
- 预检响应 (OPTIONS Response): 服务器收到预检请求后,进行判断,如果允许,就在响应中返回一系列
Access-Control-*
头部,告诉浏览器它允许的范围。这个响应没有 body。Access-Control-Allow-Origin
: 允许的源。Access-Control-Allow-Methods
: 允许的方法列表(如GET, POST, PUT, DELETE
)。Access-Control-Allow-Headers
: 允许的自定义头部列表。Access-Control-Max-Age
: 预检请求的有效时间(秒)。在此期间,浏览器无需为相同的请求再次发送预检请求。
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://ui.another.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600
- 浏览器决策: 浏览器检查预检响应。如果服务器允许了实际请求的方法和头部,浏览器才会发送实际的请求(例如
PUT
请求)。否则,就在控制台报错,实际请求根本不会被发送。 - 实际请求与响应: 如果预检通过,浏览器发送实际请求(和简单请求一样,也带着
Origin
头)。服务器处理后返回数据,并且响应中必须再次包含Access-Control-Allow-Origin
。
4. Spring 中的 @CrossOrigin
注解
@CrossOrigin
是 Spring Framework 提供的一个注解,用于在服务器端方便地配置 CORS。你可以把它看作是自动帮你处理上面提到的那些 Access-Control-*
响应头的工具。
- 使用位置
@CrossOrigin
可以用在两个级别:- 方法级别:只对被注解的那个 Controller 方法生效。
- 类(Controller)级别:对该 Controller 下的所有方法都生效。如果方法上也有
@CrossOrigin
,则方法级别的配置会覆盖类级别的配置。
- 基本用法
- 场景:允许来自
http://localhost:8080
的所有跨域请求访问这个接口。
@RestController
public class MyController {
@CrossOrigin(origins = "http://localhost:8080") // 精确指定允许的源
@GetMapping("/data")
public String getData() {
return "This is sensitive data.";
}
}
- 如果整个 Controller 都需要允许这个源,可以把注解放在类上:
@RestController
@CrossOrigin(origins = "http://localhost:8080") // 应用于该类所有方法
public class UserController {
...
}
5. @CrossOrigin
的常用属性
@CrossOrigin
注解提供了丰富的属性,可以精细化地控制 CORS 策略,这些属性直接对应 CORS 的响应头。
origins
(或value
):- 作用:指定允许的源列表,对应
Access-Control-Allow-Origin
。 - 类型:
String[]
- 示例:
@CrossOrigin(origins = {"http://a.com", "http://b.com"})
- 特殊值:
"*"
表示允许所有源。但在生产环境中要慎用,特别是当allowCredentials
为true
时。
- 作用:指定允许的源列表,对应
methods
:- 作用:指定允许的 HTTP 方法,对应
Access-Control-Allow-Methods
。 - 类型:
RequestMethod[]
- 示例:
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.POST})
- 默认值:允许所有方法。
- 作用:指定允许的 HTTP 方法,对应
allowedHeaders
:- 作用:指定允许的请求头,对应
Access-Control-Allow-Headers
。 - 类型:
String[]
- 示例:
@CrossOrigin(allowedHeaders = {"Content-Type", "Authorization"})
- 特殊值:
"*"
表示允许所有请求头。 - 默认值:允许所有请求头。
- 作用:指定允许的请求头,对应
exposedHeaders
:- 作用:指定浏览器可以访问的响应头。默认情况下,浏览器只能访问一些简单响应头(如
Cache-Control
,Content-Type
等)。如果你在响应头中设置了自定义头部(比如 JWT Token),必须在这里声明,前端才能获取到。 - 类型:
String[]
- 示例:
@CrossOrigin(exposedHeaders = "X-Auth-Token")
- 作用:指定浏览器可以访问的响应头。默认情况下,浏览器只能访问一些简单响应头(如
allowCredentials
:- 作用:布尔值,是否允许发送 Cookie,对应
Access-Control-Allow-Credentials
。 - 类型:
String
(“true” or “false”) - 默认值:
"false"
。 - 重要:如果设为
true
,origins
不能是*
,必须是具体的域名。这是浏览器安全策略的要求。前端的 AJAX 请求(如 Axios 或 Fetch API)也需要设置相应的选项(withCredentials: true
)才能发送 Cookie。 - 示例:
@CrossOrigin(origins = "http://trusted.com", allowCredentials = "true")
- 作用:布尔值,是否允许发送 Cookie,对应
maxAge
:- 作用:指定预检请求的缓存时间(秒),对应
Access-Control-Max-Age
。 - 类型:
long
- 默认值:
1800
(30分钟)。 - 示例:
@CrossOrigin(maxAge = 3600)
- 作用:指定预检请求的缓存时间(秒),对应
6. 全局配置 vs @CrossOrigin
虽然 @CrossOrigin
很方便,但在整个应用中有很多 Controller 都需要相同的 CORS 策略时,为每个类都添加注解会很繁琐。更好的做法是进行全局配置。
在 Spring Boot 中,可以通过实现 WebMvcConfigurer
接口来配置全局 CORS 规则。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有路径
.allowedOrigins("*") // 所有源
.allowedMethods("GET", "POST", "PUT", "DELETE") // 允许方法
.allowCredentials(true) // 允许凭证
.maxAge(3600); // 缓存1小时
}
}
全局配置和 @CrossOrigin
的关系:
- 如果一个请求路径同时匹配了全局配置和
@CrossOrigin
注解,Spring 会优先采用@CrossOrigin
的配置(更具体的配置优先)。 - 最佳实践:使用全局配置来定义通用的、全站范围的 CORS 策略,然后只在需要特殊处理的个别 Controller 或方法上使用
@CrossOrigin
进行覆盖。