跨域是什么?
跨域问题是浏览器的安全机制,即同源策略(Same-origin policy)
限制不同源之间的交互,从而保证资源的安全
同源策略限制内容
- Cookie、LocalStorage、IndexedDB 等存储性内容只有同源才能访问
- AJAX 请求发送后,响应内容被浏览器拦截了
- DOM
允许跨域加载的资源
- img src=XXX
- link href=XXX
- script src=XXX
为什么需要「同源策略」?
其实从上面的表现形式就能看出来了——我们不希望将我们资源被恶意网站获取
所以在浏览器加以限制,阻止向未经授权的跨站数据访问和跨站请求。一定程度上避免 XSS、CSRF 攻击
动态请求就会有跨域的问题?
跨域只存在于浏览器,不存在于 node.js/python/java 等其它环境
跨域请求时,请求是否被发送出去了?
表单的方式可以发起跨域请求可以正常发送请求,因为它不需要 JavaScript 来直接访问响应内容
AJAX 的请求将会被正常发送,但响应的结果被浏览器拦截了。因为响应内容往往涉及 JavaScript 的读写,浏览器认为不安全,所以拦截了响应,不将数据传递我们使用
以上也说明了跨域并不能完全阻止 CSRF,毕竟请求是发出去了的
同源
只有当协议、域名、端口三者完全一致,才认为同源
当不同源时,就会出现上面所说的「跨域问题」
示例
http://www.a.com/a.js
与http://www.a.com/b.js
同源http://www.a.com
与https://www.a.com
不同源,因为它们分别为 http 和 https,协议不同。同时,端口也不同,http 默认端口为 80,https 默认端口为 443http://www.a.com
与https://www.b.com
不同源,因为域名不同http://www.a.com:8888
与http://www.a.com:7777
不同源,因为端口不同
解决跨域的方式
一般我们要解决的是「AJAX 请求」的跨域问题
因为这种跨域问题的存在,使得我们正常的请求响应也被浏览器拦截了
所以,问题的核心在于——只允许我们期望的跨域请求响应接收,除此之外的跨域请求响应都应该被阻止
CORS
CORS(跨域资源共享,Cross-Origin Resource Sharing)是一种跨域请求机制
允许服务器声明哪些外部域名可以访问其资源,浏览器通过响应头判断是否被允许跨域请求
CORS 机制会在实际的请求之前,对于「非简单请求」,会先发出一个预检请求(OPTIONS 请求),来询问服务器是否接受跨域请求。而 OPTION 请求不受浏览器的「同源策略」限制
通过 HTTP 头部中的 Access-Control-Allow-Origin
等字段,服务器可以明确指定允许哪些源的请求访问资源
涉及到的 HTTP 请求头部
- Access-Control-Request-Method: 表示实际请求的 HTTP 方法(例如 POST、PUT 等)
- Access-Control-Request-Headers: 表示实际请求中自定义的请求头部
涉及到的 HTTP 响应头部
- Access-Control-Allow-Origin: 服务器响应头部,指定哪些域名可以访问资源。可以是单一域名或
*
(表示允许所有域名访问) - Access-Control-Allow-Methods: 允许的方法,如
GET
,POST
,PUT
,DELETE
等 - Access-Control-Allow-Headers: 指定允许的请求头部
- Access-Control-Allow-Credentials: 是否允许携带凭证。如 true 表示可以携带 cookie
CORS 的 cookie 问题
想要请求可以传递 cookie,需要同时满足以下 3 个条件:
- web 请求设置 withCredentials。默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie
- HTTP 响应头 Access-Control-Allow-Credentials 为 true
- HTTP 响应头 Access-Control-Allow-Origin 为非 *
只要不满足以上其一条件,浏览器会报错,获取不到返回值
示例
假设前端应用在 http://example.com
,后端 API 在 http://api.example.com
发现是跨域请求,且为「非简单请求」,浏览器会向后端发送 OPTION 请求:
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
后端 API 需要在响应中加入以下头部来支持跨域请求:
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
浏览器通过当前网页的 URL 和请求的方法,与 CORS 响应头比较,决定是否允许跨域访问
假设此时我们访问的是http://foo.com
,此时向后端发送 OPTION 请求,获得被允许的域为http://example.com
。浏览器发现当前网页的 URL 和被允许的域不一致,浏览器将禁止该网页向该后端服务器跨域
SpringBoot 解决跨域示例
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
}
以上配置就是通过 CORS 来解决跨域的。不过需要注意的是:
- 假设使用了
allowCredentials(true)
,即允许跨域请求携带凭证(例如 Cookies 或 Authorization 头),那么allowedOrigins("*")
是 不允许的,因为这会带来潜在的安全风险——允许任意来源携带凭证,可能导致 跨站请求伪造(CSRF) 攻击 - 为了提高灵活性,Spring 5.x 引入了
allowedOriginPatterns
配置项,它允许使用 通配符(例如*
)来匹配多个域名,而不会引起上面提到的限制问题。即allowedOriginPatterns("*")
和allowCredentials(true)
可以同时使用 - 上面代码的配置将允许所有请求访问,这将带来安全隐患。浏所以,在生产环境中,应该指定特定的域名、请求方法、请求头
- 一般后端服务还会设置全局的「拦截器」,用于拦截所有请求,判断是否登录。所以,全局「拦截器」需要把所有 OPTION 请求放行,否则将无法触发上面配置的 CORS 代码,导致 OPTION 请求无法送达,进一步导致浏览器无法发送跨域请求
在后端服务中使用 CORS 解决跨域问题的缺点
由服务端来配置允许哪些请求的访问,实现简单
但是,如果有多个不同服务要部署,此时要修改跨域的配置的话,不仅需要去修改代码,还要将服务重新编译打包上线。这将带来非常大的工作量。主要问题在于跨域处理和业务代码耦合了
所以后端服务指定允许跨域请求的方案,不适合在大型服务中使用,只适合简单的测试环境
Nginx 反向代理
Nginx 是 Web 网关,可以用于静态资源映射、URL 重写、动态修改请求头、反向代理等功能
Nginx 也常常被用来解决跨域问题
方案一:让前端和后端“同源”
Nginx 是中间层,前端实际上只与 Nginx 交互,至于后端是谁来服务并不关心,即 Nginx 充当反向代理的作用
浏览器访问网页,前端页面是 Nginx 通过静态资源映射获取的。而前端向后端请求也是由 Nginx 转发的。所以,在 Nginx 的协商下,前端和后端可以看做“同源”
适用场景:前端静态映射和后端都使用同一个 Nginx
假设有一个前端应用和一个后端 API 上:
- 前端应用/Nginx 地址:
http://localhost
- 后端 API:
http://localhost:8888
前端和运维沟通好:
- 当路径为
/api
,则转发到后端http://localhost:8888
- 当路径为
/
,则为静态资源映射,访问本地的静态资源
server {
listen 80;
server_name localhost; # 替换为你的域名或 IP 地址
# 处理 /api 路径的请求,代理到本地的 8888 端口
location /api/ {
proxy_pass http://localhost:8888/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 对于所有其他请求,映射到静态资源
location / {
root /path/to/your/static/files; # 替换为你的静态文件路径
index index.html index.htm;
try_files $uri $uri/ =404;
}
}
方案二:Nginx 添加 CORS 头部
如果前端服务和后端反向代理的 Nginx 并不在同一个服务器,那么,前端页面向反向代理的后端 Nginx 发送请求,肯定会遇到跨域问题(浏览器和 Nginx 之间) 。所以需要在 Nginx 中添加 CORS 头部,解决浏览器和 Nginx 的跨域问题。而后端服务与 Nginx 之间,是不需要解决跨域问题的,因为它们并没有「同源策略」的机制
适用场景:Nginx 和前端页面并不同源
假设有一个前端应用和一个后端 API:
- 前端应用:
http://frontend.com
- 后端 API:
http://backend.com
设置 Nginx 反向代理并设置 CORS 响应头
server {
listen 80;
server_name frontend.com; # 前端域名
location /api/ { # 假设 API 路径以 /api/ 开头
proxy_pass http://backend.com/; # 转发请求到后端 API
# 设置 CORS 头,允许前端跨域访问后端资源
add_header 'Access-Control-Allow-Origin' '*' always; # 允许特定来源访问
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
# add_header 'Access-Control-Allow-Credentials' 'true' always;
# 处理预检请求(OPTIONS 请求)
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'http://frontend.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Max-Age' 3600; # 缓存预检请求的时间,单位为秒
return 204; # 预检请求的响应状态码
}
# 反向代理设置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
JSONP
JSON with Padding
通过动态添加 script
标签的方式绕过浏览器的同源策略,因为 script
标签本身不受同源策略的限制
存在问题:仅支持 GET 方法
function handleResponse(data) {
console.log(data);
}
var script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';
document.body.appendChild(script);
关闭浏览器跨域
跨域是浏览器自身实现的安全机制。在其他服务中,一般是没有实现跨域机制的。比如,通过 RPC 在两个不同端口的 Java 服务互相调用时,是不受跨域限制的
既然跨域是浏览器开启的安全机制,那自然是可以关闭的
不过不推荐关闭浏览器的跨域机制,弊远大于利
总结
跨域是浏览器限制与非同源交互,所实现的安全机制
实际上还有其他解决跨域的方案:WebSocket、document.domain + Iframe、window.postMessage 等等。不过这些方案都只是在特定的场景中才能使用
对于实际的项目部署,可以采用以下更通用的方案:
- 如果只是简单的开发测试环境,可以选择服务端配置 CORS
- 如果是实际的生产环境,推荐 Nginx + CORS
公众号【牛肉烧烤屋】
B 站【爱烤猪蹄的乔治】
参考资料
https://juejin.cn/post/6844903767226351623
https://juejin.cn/post/6844903553069219853
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。