08-CORS与前后端联调-WebMvcConfig里的一行配置
系列:AI 伙伴(AI-Partner)——具身智能陪伴机器人 · 数据接口部署与二次开发篇(08/12)
一、先搞懂:为什么浏览器要"多管闲事"
你本地把 AI 伙伴(AI-Partner)后端跑在 8080 端口,又用前端页面开在 5500 端口调试,结果浏览器控制台一抹红:
Access to XMLHttpRequest at 'http://localhost:8080/api/chat' from origin 'http://localhost:5500' has been blocked by CORS policy很多新手的反应是"接口坏了"。其实接口好得很,是浏览器在拦你。这就是同源策略(Same-Origin Policy)。
同源的定义很严格:协议、域名、端口三者全部相同才算同源。http://localhost:8080和http://localhost:5500域名相同但端口不同——不同源。浏览器规定:不同源的情况下,页面里的 JS可以发出请求,但读不到响应(更早一步,某些请求连发都发不出去,后面讲预检)。这是浏览器的安全机制,防止恶意网站拿着你的登录态去偷偷请求别的站点。
关键词:这是浏览器的行为。所以 curl、Postman、服务器之间的 HTTP 调用统统没有跨域问题——它们不经过浏览器。跨域只折磨"浏览器里跑的页面"。谁来解决?服务端声明"我允许谁来访问我",这就是 CORS(跨域资源共享)。
二、项目里的 CORS 配置:逐行解读
AI 伙伴(AI-Partner)的 CORS 配置在config/WebMvcConfig里,短小精悍(项目源码,全文如下):
@ConfigurationpublicclassWebMvcConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddCorsMappings(CorsRegistryregistry){registry.addMapping("/api/**").allowedOriginPatterns("*").allowedMethods("GET","POST","PUT","DELETE","OPTIONS").allowedHeaders("*").maxAge(3600);}}逐项拆解:
| 配置项 | 项目取值 | 含义 |
|---|---|---|
addMapping | /api/** | 只对/api前缀的接口生效,其他路径(如根路径元信息接口)不管 |
allowedOriginPatterns | * | 允许任意来源,且是"模式匹配"式通配 |
allowedMethods | GET/POST/PUT/DELETE/OPTIONS | 放行的 HTTP 方法白名单 |
allowedHeaders | * | 请求头全放行(如 Content-Type、自定义 token 头) |
maxAge | 3600 | 预检结果缓存 1 小时,见后文 |
注意这个项目没有设置allowCredentials(true),所以*在这里不违法。但一旦要带 Cookie(比如以后加会话鉴权),情况就完全变了,往下看。
三、allowedOriginPatterns(““) 与 allowedOrigins(””):关键差异
这两个方法长得像双胞胎,行为却有本质区别,也是 Spring 升级后最常见的报错来源:
| 方法 | 语义 | 与 allowCredentials(true) 组合 |
|---|---|---|
allowedOrigins("*") | 响应头原样回Access-Control-Allow-Origin: * | 非法。浏览器规定带凭证时响应头必须是具体域名,不允许*,Spring 会直接抛异常 |
allowedOriginPatterns("*") | “任意来源都匹配”,但响应头回显请求方的真实 Origin(如http://localhost:5500) | 合法。浏览器拿到具体域名,允许带凭证 |
换句话说:allowedOrigins("*")是"我对所有人说随便来";allowedOriginPatterns("*")是"谁来了我就对谁说你可以来"——效果上都是全放行,但后者能和 Cookie 组合使用。Spring Boot 2.4 之后如果你写allowedOrigins("*")又配了allowCredentials(true),启动就报When allowCredentials is true, allowedOrigins cannot contain the special value "*"。
AI 伙伴(AI-Partner)选 patterns 的写法,等于给未来的 Cookie 鉴权预留了兼容性——虽然当前项目没有拦截器和凭证校验,这个选择依然是对的姿势。
顺带把安全这盆冷水泼了:全开 CORS 只适合开发联调。项目文档里也如实把"收紧 CORS、加鉴权"列进了生产建议。上生产前务必改白名单,文末给写法。
四、预检请求(OPTIONS)流程拆解
为什么前端一个"普通 POST"会变成两个请求?因为浏览器把跨域请求分两类:
- 简单请求:方法为 GET/HEAD/POST,且请求头都在安全列表内、Content-Type 仅限
text/plain、multipart/form-data、application/x-www-form-urlencoded——直接发。 - 非简单请求:比如你的前端习惯性发
Content-Type: application/json的 POST(AI 伙伴(AI-Partner)的/api/chat就是这种)——浏览器会先发一个 OPTIONS 预检请求问服务器"我能发吗",得到许可再发真正的请求。
完整时序(演示):
浏览器 后端(8080) │ ── OPTIONS /api/chat ───────▶│ 预检:不带业务数据 │ Origin: http://localhost:5500 │ Access-Control-Request-Method: POST │ ◀── 200 + CORS 响应头 ───────│ │ Access-Control-Allow-Origin: http://localhost:5500 │ Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS │ Access-Control-Max-Age: 3600 │ ── POST /api/chat (JSON) ───▶│ 真实请求 │ ◀── {"code":0,...} ──────────│两个实战要点:
maxAge(3600)的意义:预检结果缓存 1 小时,期间同一接口的跨域请求不再重复发 OPTIONS,省一半往返。缓存期内你改了 CORS 配置却发现"没生效",先清缓存或换个无痕窗口再排查。- OPTIONS 请求本身不经过你的业务代码,所以不要在 Controller 里试图"接住"它,Spring MVC 会自动应答。
五、小程序与网页调试为什么会遇到跨域
联调场景逐个说:
| 调试方式 | 有跨域问题吗 | 原因 |
|---|---|---|
| curl / Postman | 没有 | 不经过浏览器 |
| 本地网页(file:// 或别的端口) | 有 | 经典跨域场景,服务端配置 CORS 解决 |
| 微信小程序开发者工具 | 通常没有,但体验版/真机可能遇到 | 小程序用wx.request,默认不检查 CORS;但开发工具勾选"不校验合法域名"时走的是浏览器内核模拟,部分场景仍会撞上 |
| Nginx 反代后的前端 | 没有(同域) | 前端和/api都从同一个域名进出,浏览器认为是同源 |
这也是为什么很多团队最终选择 Nginx 把前端静态资源和后端接口收进同一个域名——跨域问题从物理上消失。而开发期最省事的方案就是像 AI 伙伴(AI-Partner)这样后端直接放开。
另外提醒一句:项目后端调用的视觉服务(FastAPI)自己也开了 CORS 全开(allow_origins=["*"]),如果你想让浏览器页面直连 8000 端口的视觉接口调试,也不会被拦——但同理,这是开发态配置。
六、生产环境:把*收紧成白名单(示意)
生产环境的正确写法是把域名写死(示意):
@OverridepublicvoidaddCorsMappings(CorsRegistryregistry){registry.addMapping("/api/**").allowedOriginPatterns("https://www.example.com","https://m.example.com").allowedMethods("GET","POST","PUT","DELETE").allowedHeaders("Content-Type","Authorization").maxAge(3600);}再讲究一点,可以把域名列表放进配置文件或环境变量,按spring.profiles区分 dev/prod——开发全开、生产白名单。记住三条红线:
- 生产不要
allowedOriginPatterns("*"),尤其加了 Cookie/Token 凭证之后; allowedHeaders从*收敛到实际用到的头,少暴露一个是一个;- 若用了 Nginx 统一反代,后端 CORS 甚至可以直接关掉,让网关统一管。
七、跨域排查表:报错 → 原因 → 解决
收好这张表,联调遇到 CORS 报错按图索骥:
| 报错信息(关键词) | 原因 | 解决 |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present | 服务端没配 CORS,或请求路径不在addMapping范围内 | 确认配置覆盖了目标路径;确认后端真的重启了 |
has been blocked by CORS policy: Response to preflight request doesn't pass access control check | 预检(OPTIONS)被拒:方法/头不在白名单,或被拦截器挡了 | 检查allowedMethods/allowedHeaders;本项目暂无拦截器,加了鉴权后要给 OPTIONS 放行 |
The value of 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include' | 前端带 Cookie(credentials: ‘include’),服务端却回* | 用allowedOriginPatterns替代allowedOrigins,并确认allowCredentials配置一致 |
Method PUT is not allowed by Access-Control-Allow-Methods | 方法没进白名单 | 白名单里补上该方法 |
Request header field xxx is not allowed | 自定义请求头没放行 | allowedHeaders里加该头 |
| 改了配置依旧报错 | 预检缓存 1 小时未过期 / 浏览器缓存 / 配置没加载 | 清浏览器缓存或换无痕窗口;启动日志确认 WebMvcConfig 生效 |
| 本地没问题,上生产报跨域 | 生产走了 Nginx/网关,CORS 头被网关拦截或重复添加 | 检查网关层是否也配了 CORS,避免响应头重复 |
八、收尾
CORS 这件事,本质是浏览器和服务端的一次"握手确认":浏览器问"你允许我访问吗",服务端答"允许谁、用什么方法、带什么头、答复有效期多久"。AI 伙伴(AI-Partner)用WebMvcConfig里不到 10 行的配置换来了前后端联调的丝滑,开发期这是甜;但它同时意味着任何网页都能调你的/api/**——生产期这就是裸奔。联调放开,上线收紧,这是纪律。配合下一阶段的鉴权和网关改造,把"门"真正装上门锁。