引言:在一个前/后端分离的项目开发中,常常会出现前端向后端发送一个请求时,浏览器报错:
Access to XMLHttpRequest at 'http://localhost:8080/' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.,也就是通常说的"跨域访问"的问题,由此导致前端代码不能读取到后端数据。
摘要:所谓"跨域问题",本质上是浏览器在同源策略约束下,主动阻止 JavaScript 读取跨源请求响应的一种安全保护行为。解决跨域问题主要通过服务器端设置CORS(跨域资源共享)机制------浏览器放行跨域请求响应的数据;或者Nginx/网关的代理功能------跨域的请求实际由网关代发,浏览器端依旧是同源请求。
什么是跨域访问
跨域访问 指的是:当前网页所在的"源(Origin)"去访问另一个"不同源"的资源 ,而该访问被浏览器安全策略所限制或拦截的情况。
在浏览器中一个"源"由三部分组成:协议(Protocol) + 域名(Host) + 端口(Port),只要有一个部分不一样就是跨源 ,也即跨域。例如:
| URL | 协议 | 域名 | 端口 | 是否同源 |
|---|---|---|---|---|
http://example.com |
http | example.com |
80 | 基准 |
http://example.com:8080 |
http | example.com |
8080 | 跨域(端口不同) |
https://example.com |
https | example.com |
443 | 跨域(协议不同) |
http://api.example.com |
http | api.example.com |
80 | 跨域(域名不同) |
这里需要强调:对"跨域访问"进行限制是浏览器的安全策略导致的,并不是前端或后端技术框架引起的。
为什么跨域访问请求"得不到"数据
这里就要展开说明为什么浏览器要对"跨域访问"进行限制,导致(尤其是)Web前端中发送HTTP请求会得不到数据,并在控制台报错。
出于安全性,浏览器会采用同源策略(Same-Origin Policy,SOP)限制脚本内发起的跨源 HTTP 请求,限制一个源的文档或者它加载的脚本如何与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。例如,它可以防止互联网上的恶意网站在浏览器中运行 JavaScript 脚本,从第三方网络邮件服务(用户已登录)或公司内网(因没有公共 IP 地址而受到保护,不会被攻击者直接访问)读取数据,并将这些数据转发给攻击者。
假设在没有同源限制的情况下:
- 用户已登录银行网站
https://bank.com(Cookie 已保存) - 用户同时打开一个恶意网站
https://evil.com evil.com的 JavaScript 可以:- 直接读取
bank.com的接口返回数据 - 发起转账请求
- 窃取用户隐私信息
- 直接读取
这是非常严重的安全灾难。
同源策略将跨源之间的访问(交互)通常分为3种:
- 跨源写操作 (Cross-origin writes)一般是被允许的。例如链接、重定向以及表单提交。特定少数的 HTTP 请求需要添加
预检请求。 - 跨源资源嵌入 (Cross-origin embedding)一般是被允许的,比如
<img src="...">、<script src="...">、<link href="...">。 - 跨源读操作(Cross-origin reads)一般是不被允许的。
再次强调:跨域限制是"浏览器行为",不是后端服务器的限制。后端服务本身是可以接收来自任何来源的 HTTP 请求的。
比如前端访问fetch("https://api.example.com/data"),而当前页面来自http://localhost:8080,请求可以发出去,但浏览器会拦截响应,不让 JavaScript 读取。
要使不同源可以访问(交互),可以使用 CORS来允许跨源访问。CORS 是HTTP的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源。
怎么解决跨域访问的"问题"
CORS机制
跨源资源共享 (Cross-Origin Resource Sharing,CORS,或通俗地译为跨域资源共享 )是一种基于HTTP头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己(服务器)的资源 。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头(Header)。
对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是GET以外的 HTTP 请求,或者搭配某些MIME类型(多用途互联网邮件扩展,是一种标准,用来表示文档、文件或一组数据的性质和格式)的POST请求),浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(例如Cookie和HTTP 认证相关数据)。
一般浏览器要检查的响应头有:
Access-Control-Allow-Origin:指示响应的资源是否可以被给定的来源共享。Access-Control-Allow-Methods:指定对预检请求的响应中,哪些 HTTP 方法允许访问请求的资源。Access-Control-Allow-Headers:用在对预检请求的响应中,指示实际的请求中可以使用哪些 HTTP 标头。Access-Control-Allow-Credentials:指示当请求的凭据标记为 true 时,是否可以暴露对该请求的响应给脚本。Access-Control-Max-Age:指示预检请求的结果能被缓存多久。
如:
http
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
可知,若使用CORS解决跨域访问中的问题要在服务器端(通常是后端)进行设置。以Spring Boot的后端为例:
-
局部的请求:在对应的
Controller类或指定方法上使用@CrossOrigin。如下java@CrossOrigin( origins = "http://localhost:3000", allowCredentials = "true" ) -
全局使用:新建一个配置类并注入Spring框架中。如下:
java@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins( "http://test.example.com" ) .allowedMethods("GET","POST","PUT","DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }
使用CORS 的优点:官方标准;安全、可控;与前后端分离完美匹配。缺点:需要服务端正确配置;初学者容易被预检请求困扰。
通过架构或代理手段
除了使用CORS的方式,还可以通过架构设计或代理的方式让跨域"变成"同源访问。
比如通过Nginx / 网关 代理浏览器(前端)请求,再由Nginx或网关访问服务器获取数据。
浏览器 → 前端域名 → Nginx → 后端服务
这样的话在浏览器(前端)看到将始终是对当前网站(前端域名)的访问(即使打开开发者工具的网络选项,请求的url地址也是前端域名)。
一个Nginx的配置示例:
nginx
server {
listen 443;
server_name www.example.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
前端请求示例:axios.get('/api/user')。
这是通过Nginx或网关这样的中间件实现的,如果在开发阶段想要快速解决跨域访问问题,可以在相应的项目构建的配置中设置代理。这里以Vite为构建工具的Vue项目为例,在vite.config.js中添加如下的配置项:
js
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
然后请求的URL采用这样的方式axios.get('/api/user'),不在使用axios.get('http://localhost:8080/api/user')。
使用代理方式的优点:无跨域;性能好;适合生产环境。缺点:需要额外部署配置。
总结
跨域问题并不是请求被禁止,而是浏览器在同源策略约束下,出于安全考虑,限制前端 JavaScript 对跨源响应数据的访问行为。
跨域问题的根源是 浏览器实现的同源策略(Same-Origin Policy),而不是:
- HTTP 协议限制
- 后端服务器限制
- 前端框架(Vue / React)的问题
浏览器阻止的是JS 获取结果,而不是"阻止请求发送"------跨域请求可以被发出,服务器可以正常返回(比如预检请求响应),浏览器阻止JavaScript访问响应数据。
"跨域问题"只存在于浏览器环境,例如:
- Java / Node / Python 发 HTTP 请求------没有跨域问题
- Postman / curl ------没有跨域问题
- 微服务之间调用------没有跨域问题
因为这些环境不执行浏览器的同源策略 。跨域问题是浏览器安全模型的一部分,本质上是对跨源资源访问的"读权限控制",而非通信能力限制。
使用CORS 并不是"绕过"同源策略 ------浏览器的同源策略始终存在;CORS 是 同源策略的"例外机制" ;本质是:服务器显式授权浏览器放行 。换句话说:没有 CORS,就没有"合法的跨域读取"。
只要不产生跨域,就不会有跨域问题,所以可以使用代理或网关将请求进行转发,而不是由浏览器直接请求服务器端发生跨域问题。