文章目录
-
- 前言:那个让我通宵的报错
- 啥是CORS?一句话说不清楚
- 同源策略:浏览器的"被害妄想症"
- CORS到底怎么工作的?两种模式
- 实战配置:我怎么解决那个通宵问题
-
- [Spring Boot后端配置](#Spring Boot后端配置)
- Nginx反向代理方案(生产环境推荐)
- Node.js/Express配置
- 我踩过的那些天坑(血泪史)
- 安全警告:别无脑开*
- 总结与互动
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
前言:那个让我通宵的报错
兄弟们,我先说个真事儿。
去年三月份,我接了个外包项目,甲方爸爸要求前后端分离,前端Vue3,后端Spring Boot。我拍着胸脯说没问题,三天搞定。结果第三天凌晨四点,我还盯着控制台那一行红字发呆:
Access to XMLHttpRequest at 'http://localhost:8080/api/login' from origin 'http://localhost:5173' has been blocked by CORS policy...
当时我就破防了!这什么鬼东西?!我代码写得没毛病啊,axios配置也对,后端接口也能跑,咋就数据拿不到呢?
后来天亮了,我在楼下买了杯美式(别问为什么是美式,因为生活太苦了),终于搞明白了------这特么就是传说中的跨域问题。今天这篇文章,我不跟你讲大道理,就聊聊这个让无数程序员掉头发的CORS,到底是咋回事,以及我怎么解决它的。
啥是CORS?一句话说不清楚
首先啊,CORS全称是Cross-Origin Resource Sharing,翻译过来叫"跨域资源共享"。听着挺高大上的对吧?其实就是浏览器的一种安保机制。
我给你打个比方。想象你现在住在一个高档小区,小区门口有保安大爷,大爷手里拿着个花名册。按照小区规定,只有业主(同源)能随便进出,外来人员(跨域)要进小区,必须得业主事先跟保安打过招呼,登记备案,才能放行。
浏览器就是那个保安大爷。你的网站前端跑在http://localhost:5173,这是你家。后端API跑在http://localhost:8080,这是隔壁老王家。浏览器一看,协议不同?端口不同?域名不同?得了,默认情况下就是不让进。
等等,你可能会问:"那我直接用Postman调用接口咋就没问题?"
问得好!这就是最坑的地方。CORS只针对浏览器!你用Postman、用curl、写Python脚本,随便调,完全没限制。但一旦涉及到浏览器里的JavaScript发请求,保安大爷立马警觉------"站住!有嫌疑!"
同源策略:浏览器的"被害妄想症"
要说清楚CORS,得先聊聊它背后的逻辑------同源策略(Same-Origin Policy)。
这个策略是1995年Netscape浏览器搞出来的,那时候互联网还挺单纯,但网景公司就已经有"被害妄想症"了。他们担心,如果一个恶意网站能通过JavaScript随便访问其他网站的数据,那还得了?比如你在A网站登录了银行账号,然后不小心点开B网站,B网站的JS就能直接调银行接口转钱?这特么不是完犊子了!
所以同源策略规定:协议、域名、端口,三者必须完全一致,才算"同源"。
- http://example.com:80/page1 和 http://example.com:80/page2 → 同源 ✓
- http://example.com:80/page1 和 https://example.com:80/page1 → 不同源!协议不同 ✗
- http://example.com:80/page1 和 http://api.example.com:80/page1 → 不同源!子域名不同 ✗
- http://example.com:80/page1 和 http://example.com:8080/page1 → 不同源!端口不同 ✗
看到没?就这么严格。哪怕只是端口从80变成8080,保安大爷立马翻脸不认人。
但问题来了,现代web开发哪有不分前后端的?微服务架构下,API跟前端部署在不同域名上是常态。这时候如果浏览器死守着同源策略不放,那大家都不用干活了。所以,CORS机制应运而生------它不是取消同源策略,而是给了一个"白名单"机制,告诉浏览器:"这几家是我亲戚,放行吧。"
CORS到底怎么工作的?两种模式
好,现在进入硬核(其实也不硬)部分。CORS请求分成两种类型:简单请求和预检请求(preflight)。浏览器会自动判断走哪种流程,你控制不了,只能被动接受。
简单请求:直接放行
啥算简单请求?得同时满足下面一堆条件:
- 方法只能是GET、HEAD、POST之一
- 头部字段只能有Accept、Accept-Language、Content-Language、Content-Type(但Content-Type只能是application/x-www-form-urlencoded、multipart/form-data或text/plain)
- 不能有你自定义的Header
说实话,现在写代码,谁还不带个Authorization头?谁还不发个JSON(application/json)?所以真实项目里,简单请求几乎不存在!我写了这么多年代码,简单请求就遇到过一次,还是静态资源加载。
简单请求的流程是这样的:浏览器直接发出请求,但会在Header里加一个Origin字段,告诉服务器:"我来自http://localhost:5173,给句话,能不能访问?"
服务器如果愿意放行,就在响应头里加:
Access-Control-Allow-Origin: http://localhost:5173
或者更暴力的:
Access-Control-Allow-Origin: *
浏览器收到响应,一看"哦,允许了",就把数据交给JavaScript。如果服务器没返回这个头,或者返回的Origin不匹配,浏览器就当场拦截,你JavaScript拿到的就是个大红报错。
预检请求:先问再问
这个才是我们日常开发的常态!当你发一个POST请求,Content-Type是application/json,还带了Authorization头,浏览器立马紧张起来:"这请求有点复杂啊,我得先探探路!"
于是,在真正的请求之前,浏览器会自动发一个OPTIONS请求(这叫预检),问服务器:"大哥,我打算用POST方法,带这些头,发JSON数据,你行不行?"
这个OPTIONS请求会带三个关键头:
- Origin: 还是表明来源
- Access-Control-Request-Method: POST(我准备用的方法)
- Access-Control-Request-Headers: content-type,authorization(我准备带的头)
服务器这时候得表态,返回:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
看到没?服务器明确说:我允许这些方法,允许这些头。浏览器一听,妥了,才会发出真正的POST请求。如果服务器没响应,或者响应里没说允许,浏览器就直接掐断,真正的请求压根发不出去!
那个Access-Control-Max-Age是缓存时间,单位秒。意思是这次预检结果我记住了,86400秒内(也就是24小时)不用再问。
实战配置:我怎么解决那个通宵问题
好了,原理讲完了,说说我是怎么解决凌晨四点的绝望的。
Spring Boot后端配置
我那个项目用的是Spring Boot 3.2(2025年的版本,别用2.x了兄弟们,该升级了)。最暴力的方法,在Controller上加注解:
java
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:5173", maxAge = 3600)
public class UserController {
// ... 你的代码
}
但这太麻烦了,每个Controller都加?我懒。所以全局配置:
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有路径
.allowedOrigins("http://localhost:5173", "http://127.0.0.1:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true) // 允许携带cookie
.maxAge(3600);
}
}
注意!这里有个大坑!allowedOrigins("*")和allowCredentials(true)不能同时用!这是Spring Boot的安全限制。如果你需要带Cookie(比如登录态),必须指定具体域名,不能用星号。
Nginx反向代理方案(生产环境推荐)
后来项目上线,我换了种方案,用Nginx做反向代理。这才是正解!
nginx
server {
listen 80;
server_name myapp.com;
location /api {
# 转发到后端服务
proxy_pass http://localhost:8080;
# 关键是这几行!
add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
# 处理预检请求
if ($request_method = 'OPTIONS') {
return 204;
}
}
location / {
proxy_pass http://localhost:5173; # 前端服务
}
}
看到没?Nginx层就把CORS处理了,后端代码完全不用改!而且这样还能隐藏真实后端地址,安全性++。
Node.js/Express配置
如果你写Node,是这样的:
javascript
const express = require('express');
const cors = require('cors'); // 记得npm install cors
const app = express();
// 方案1:允许所有(开发环境凑合用)
app.use(cors());
// 方案2:生产环境严谨点
app.use(cors({
origin: ['http://localhost:5173', 'https://production.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
Express这个cors中间件是2025年1月刚更新的2.8.5版本,修复了几个安全漏洞,记得升级。
我踩过的那些天坑(血泪史)
坑1:缓存导致的诡异现象
有一次我改了后端配置,刷新了浏览器,居然还是报错!我以为是配置没生效,重启了N遍服务,差点把电脑砸了。后来才发现,是Access-Control-Max-Age的锅。浏览器把之前的预检结果缓存了,我改的配置它压根没去问!
解决方案:开发环境把maxAge设成0,或者手动禁用浏览器缓存(Chrome DevTools里有个Disable Cache)。
坑2:Vite热更新的锅
用Vite的兄弟们注意!Vite的热更新(HMR)用WebSocket,有时候CORS配好了API请求,但HMR还是连不上。这时候得在vite.config.js里单独配:
javascript
export default defineConfig({
server: {
hmr: {
overlay: false,
},
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
}
}
}
});
用Vite的代理转发,直接绕过浏览器的CORS!这招在开发环境简直神器,前端代码里请求/api/xxx,Vite帮你转发到8080端口,浏览器看来都是5173同源,完美!
坑3:复杂请求体触发的预检
我有一次发了个DELETE请求,按理说也是简单方法对吧?但浏览器还是发了OPTIONS预检。查了半天,发现是因为我在DELETE请求里带了JSON body。某些浏览器(说的就是Safari)对DELETE带body的情况有特殊处理,会触发预检。
解决方案:DELETE请求尽量把参数放URL里,别放body里。或者老老实实让服务器支持OPTIONS响应。
安全警告:别无脑开*
最后说点严肃的。我看到很多教程教人直接Access-Control-Allow-Origin: ,还配上Access-Control-Allow-Credentials: true。这特么是在玩火!
2025年OWASP发布的API安全Top 10里,CORS配置错误依然排在第6位。如果你把 开放到生产环境,还允许携带Cookie,那攻击者随便做个钓鱼网站就能冒充用户请求你的API,这叫CSRF攻击的变种,防不胜防!
正确的做法:
- 生产环境白名单必须明确指定域名
- 内部系统考虑用IP白名单+VPN,别直接暴露在公网
- 敏感接口(转账、删数据)就算开了CORS,也要额外验证Referer和Origin头
- 定期用curl -H "Origin: https://evil.com"测试你的API,看有没有配置泄露
总结与互动
写到这儿,天又快黑了(我看了一眼时间,下午5点25分)。突然有种回到去年那个凌晨的错觉,只不过现在手里是可乐而不是美式。
CORS这个东西吧,原理其实不复杂,就是浏览器的"门禁系统"。但坑就坑在它涉及前后端、浏览器、服务器、网络层多个环节,报错信息还模棱两可,"blocked by CORS policy"------鬼知道是哪一步blocked的?
今天这篇文章,我从那个让我通宵的报错说起,聊了同源策略的"被害妄想症",拆解了简单请求和预检请求的区别,给了Spring Boot、Nginx、Node.js的现成配置代码,还倒了几盆我踩过的天坑。
但是! 我知道你们肯定还有自己的问题。比如"我的情况更复杂,用了网关+微服务咋配?"或者"WebSocket的CORS怎么处理?"再或者"为啥我配好了Postman能调,浏览器还是不行?"
来,评论区见!把你遇到的CORS奇葩报错贴出来,咱们一起研究。点赞过500的话,我下周出个进阶篇,专门讲复杂架构下的CORS治理方案,还有怎么在微服务网关层(Spring Cloud Gateway 2025新版)统一处理跨域,保证让你再也不怕那个红色报错!
最后,转发这篇文章给你那个还在被CORS折磨的同事,救人一命胜造七级浮屠啊朋友们!
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。