
1. 项目概述从“同源”到“跨域”的必然之路如果你写过前端代码大概率在控制台见过这个红色的错误“Access to fetch at ‘http://api.example.com/data’ from origin ‘http://localhost:3000’ has been blocked by CORS policy”。这行字背后就是今天要聊的“跨域请求”。它不是什么高深莫测的黑科技而是现代Web开发中一个绕不开的基础安全机制。简单说跨域请求就是一个网页的JavaScript代码试图去请求一个与当前页面域名、协议或端口不同的服务器资源时浏览器出于安全考虑而施加的限制。为什么要有这个限制这得从浏览器的“同源策略”说起。同源策略是浏览器安全的基石它规定一个源的文档或脚本在没有明确授权的情况下不能与另一个源的资源进行交互。这里的“源”由协议、域名、端口三者共同定义。比如https://www.a.com和https://api.a.com就是不同源域名不同http://localhost:8080和http://localhost:3000也是不同源端口不同。这个策略有效地防止了恶意网站通过脚本窃取用户在其他网站如银行、邮箱的会话信息是防御“跨站脚本攻击”等安全威胁的第一道防线。然而现代Web应用往往是前后端分离的架构。前端应用可能部署在https://app.mycompany.com而后端API服务则独立运行在https://api.mycompany.com。为了让前端能安全地调用后端API我们就需要一种机制来“有控制地”打破同源策略的严格限制。这就是“跨源资源共享”机制诞生的背景。它不是一个漏洞而是一个标准化的安全解决方案允许服务器声明哪些外部源有权访问自己的资源。理解CORS不仅是解决那个红色报错更是理解现代Web应用如何安全地进行数据通信的关键。无论是前端开发者、后端工程师还是安全测试人员这都是必须掌握的核心知识。2. CORS机制深度解析不只是几个响应头那么简单很多人对CORS的理解停留在“后端加个Access-Control-Allow-Origin: *头就行了”。这种认知非常危险它可能为你的应用打开安全后门。CORS是一套完整的握手协议其核心在于浏览器与服务器之间通过一系列HTTP头进行协商服务器始终掌握着访问控制的最终决定权。2.1 核心流程简单请求与预检请求CORS将请求分为两类简单请求和需预检的请求。区分它们的关键在于请求本身是否“安全”浏览器会根据一套规则自动判断。简单请求必须同时满足以下所有条件方法限制只能是GET、HEAD、POST之一。头部限制除了用户代理自动设置的头部如Connection、User-Agent只能手动设置以下“安全”的请求头AcceptAccept-LanguageContent-LanguageContent-Type且值仅限于application/x-www-form-urlencoded、multipart/form-data、text/plain三者之一请求中不能使用ReadableStream对象。对于简单请求浏览器会直接发出请求并在请求头中自动添加Origin字段标明请求来源。服务器收到后如果同意跨域访问就在响应头中包含Access-Control-Allow-Origin其值可以是具体的源如https://app.example.com或通配符*。浏览器看到这个响应头与自己的源匹配才会将响应内容交给前端JavaScript处理。需预检的请求则复杂得多。当你的请求不满足简单请求的条件时例如使用了PUT、DELETE方法或者Content-Type是application/json又或者你自定义了一个X-Auth-Token请求头浏览器就会认为这是一个“非简单”的、可能对服务器数据产生副作用的请求。出于安全考虑浏览器不会直接发出这个实际请求而是先发起一个OPTIONS方法的“预检请求”。这个预检请求就像一次“事前请示”它通过两个特殊的请求头告诉服务器后续真实请求的细节Access-Control-Request-Method: 告知服务器接下来的真实请求打算用什么HTTP方法如POST。Access-Control-Request-Headers: 告知服务器接下来的真实请求会携带哪些自定义头部如X-PINGOTHER, Content-Type。服务器收到预检请求后需要检查这些信息并通过响应头明确告知浏览器自己的策略Access-Control-Allow-Origin: 允许哪个源访问。Access-Control-Allow-Methods: 允许哪些HTTP方法。Access-Control-Allow-Headers: 允许哪些自定义请求头。Access-Control-Max-Age: 可选本次预检结果的有效期秒在此期间内同一请求无需再次预检。只有预检请求的响应通过了浏览器的检查浏览器才会接着发出真实的请求。否则真实请求根本不会被发出你在前端代码里看到的直接就是一个CORS错误。注意预检请求的响应状态码通常是204 No Content或200 OK但它不携带任何业务数据它的唯一目的就是完成这次“安全握手”。所有业务逻辑都在随后的真实请求中处理。2.2 关键HTTP头部详解理解CORS本质是理解这些HTTP头部的作用。下面这个表格梳理了最核心的几个头部头部名称所属方作用示例与说明Origin请求头浏览器自动添加表明请求来自哪个源协议域名端口。Origin: https://app.example.comAccess-Control-Allow-Origin响应头最核心的头部。服务器用它来声明允许访问该资源的源。可以是具体的源或通配符*。Access-Control-Allow-Origin: https://app.example.com或Access-Control-Allow-Origin: *Access-Control-Allow-Methods响应头用于预检请求的响应列出服务器允许的HTTP方法。Access-Control-Allow-Methods: POST, GET, OPTIONSAccess-Control-Allow-Headers响应头用于预检请求的响应列出服务器允许的自定义请求头。Access-Control-Allow-Headers: X-Custom-Header, Content-TypeAccess-Control-Max-Age响应头指定预检请求的结果可以被缓存多久秒。Access-Control-Max-Age: 86400缓存24小时Access-Control-Allow-Credentials响应头当值为true时表示服务器允许浏览器在跨域请求中携带凭据如Cookies、HTTP认证信息。重要如果设置此头为true则Access-Control-Allow-Origin不能为通配符*必须是具体的源。Access-Control-Allow-Credentials: trueAccess-Control-Request-Method请求头预检请求中使用告知服务器真实请求将使用的方法。Access-Control-Request-Method: POSTAccess-Control-Request-Headers请求头预检请求中使用告知服务器真实请求将携带的自定义头部列表。Access-Control-Request-Headers: X-PINGOTHER, Content-Type2.3 带凭据的请求Cookie与认证信息默认情况下跨域请求不会携带像Cookies、HTTP认证信息这类用户凭据。这是同源策略的另一个重要体现。如果你的应用需要维持跨域会话比如前端域名和API域名不同但需要共享登录状态就必须显式地开启这个功能。在前端无论是使用原生的XMLHttpRequest还是现代的Fetch API都需要进行配置Fetch API: 在fetch()的初始化参数中设置credentials: include。XMLHttpRequest: 设置xhr.withCredentials true;。在服务器端响应必须包含两个头部Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin必须是一个明确的、具体的源例如https://app.example.com绝对不能是通配符*。实操心得这是最容易出错的地方之一。很多开发者在本地测试时前后端都在localhost但端口不同设置了withCredentials却看到请求失败。请检查你的后端响应头Access-Control-Allow-Origin必须是像http://localhost:3000这样的精确值而不是*。同时确保后端响应的Set-Cookie头设置了正确的SameSite和Secure属性如果使用HTTPS。3. 后端实战如何正确配置CORS理解了原理我们来看如何在后端实现。不同的语言和框架配置方式不同但核心思想一致在HTTP响应中添加正确的CORS头部。这里以几种常见的技术栈为例。3.1 Node.js (Express框架) 配置在Express中你可以使用官方的cors中间件这是最推荐的方式因为它处理了各种边缘情况。npm install corsconst express require(express); const cors require(cors); const app express(); // 1. 最简单的配置允许所有来源访问仅适用于开发或公开API app.use(cors()); // 等价于 Access-Control-Allow-Origin: * // 2. 配置允许的特定来源生产环境推荐 const corsOptions { origin: https://your-frontend-app.com, // 允许单个源 // 或者动态配置多个允许的源 // origin: function (origin, callback) { // const allowedOrigins [https://app-a.com, https://app-b.com]; // if (!origin || allowedOrigins.indexOf(origin) ! -1) { // callback(null, true); // } else { // callback(new Error(Not allowed by CORS)); // } // }, methods: [GET, POST, PUT, DELETE, OPTIONS], // 允许的方法 allowedHeaders: [Content-Type, Authorization, X-Custom-Header], // 允许的头部 exposedHeaders: [Content-Range, X-Content-Range], // 允许前端JS访问的额外响应头 credentials: true, // 允许携带凭据Cookies等 maxAge: 86400 // 预检请求缓存时间秒 }; app.use(cors(corsOptions)); // 你的路由 app.get(/api/data, (req, res) { res.json({ message: Hello CORS! }); }); app.listen(3000);关键参数解释origin: 可以是一个字符串、一个数组多个源或一个函数。使用函数是处理动态源列表的最佳实践比如根据环境变量或数据库配置来动态判断。credentials: true: 如果你需要跨域传递Cookie或认证头这个必须设置为true并且origin不能是*。exposedHeaders: 默认情况下前端JS只能访问CORS安全响应头如Cache-Control,Content-Language等。如果你需要让前端读取像X-Total-Count常用于分页这样的自定义头必须在这里声明。3.2 Spring Boot (Java) 配置在Spring Boot中配置CORS非常灵活可以在全局配置也可以在单个控制器或方法上配置。全局配置推荐 创建一个配置类实现WebMvcConfigurer接口。import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) // 匹配的路径模式 .allowedOrigins(https://your-frontend-app.com) // 允许的源 // .allowedOriginPatterns(*) // Spring Boot 2.4 支持的通配符模式比allowedOrigins(*)更灵活 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) // 允许所有请求头或指定列表 Authorization, Content-Type .exposedHeaders(X-Total-Count) // 暴露给前端的响应头 .allowCredentials(true) // 允许凭据 .maxAge(3600); // 预检请求缓存时间 } }局部配置使用CrossOrigin注解 你可以将注解用在控制器类或具体方法上。RestController RequestMapping(/api) CrossOrigin(origins https://your-frontend-app.com, maxAge 3600) public class MyController { GetMapping(/items) CrossOrigin // 继承类级别的配置或单独覆盖 public ListItem getItems() { // ... } PostMapping(/items) CrossOrigin(origins https://admin-app.com, credentials true) public Item createItem(RequestBody Item item) { // 这个方法允许来自 admin-app.com 的带凭据请求 // ... } }注意事项在Spring Security项目中如果同时使用了Spring Security的过滤器链仅配置WebMvcConfigurer可能不够因为Security的过滤器会在MVC之前执行并拦截OPTIONS请求。此时需要在Security配置中显式允许OPTIONS请求和CORS相关头部。通常添加http.cors()即可它会自动集成你的CORS配置。3.3 Nginx反向代理配置有时你不想修改后端代码或者后端是遗留系统难以改动。这时在反向代理层如Nginx配置CORS是一个干净利落的方案。server { listen 80; server_name api.yourdomain.com; location / { # 你的后端服务地址 proxy_pass http://backend-server; # 核心CORS配置 if ($request_method OPTIONS) { # 处理预检请求 add_header Access-Control-Allow-Origin https://your-frontend-app.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; add_header Access-Control-Max-Age 1728000; # 20天缓存 add_header Content-Type text/plain; charsetutf-8; add_header Content-Length 0; return 204; # 预检请求返回204 No Content } # 处理真实请求 if ($request_method ~* (GET|POST|PUT|DELETE)) { add_header Access-Control-Allow-Origin https://your-frontend-app.com always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Expose-Headers Content-Length,Content-Range,X-Total-Count always; } } }Nginx配置要点分离处理用$request_method变量区分OPTIONS预检请求和真实请求GET/POST等。always参数Nginx的add_header指令默认只在响应码为200, 201, 204, 206, 301, 302, 303, 304, 307, 308时添加头部。使用always确保在所有响应包括4xx, 5xx错误中都添加CORS头否则前端在收到错误响应时依然会触发CORS错误。凭据与通配符如果使用了Access-Control-Allow-Credentials: true那么Access-Control-Allow-Origin必须是一个具体的源不能是*。上面的配置中我们写死了前端地址。动态源处理如果需要根据请求的Origin头动态设置允许的源Nginx配置会变得复杂通常需要借助Lua模块或Map指令不如在应用层处理方便。4. 前端实战发起跨域请求的正确姿势后端配置好了前端发起请求时也有不少细节需要注意。现代前端主要使用Fetch API或axios这类库。4.1 使用原生Fetch API// 1. 简单GET请求默认不带凭据 fetch(https://api.example.com/data) .then(response { if (!response.ok) { throw new Error(HTTP error! status: ${response.status}); } return response.json(); }) .then(data console.log(data)) .catch(error console.error(Fetch error:, error)); // 2. 带自定义头和凭据的POST请求会触发预检 fetch(https://api.example.com/data, { method: POST, mode: cors, // 明确指定CORS模式这是默认值但显式声明更好 credentials: include, // 关键如果需要发送Cookies或HTTP认证信息必须设置 headers: { Content-Type: application/json, // 这个Content-Type会触发预检 X-Custom-Header: myvalue }, body: JSON.stringify({ key: value }) }) .then(response response.json()) .then(data console.log(data)); // 3. 处理预检请求缓存 // 浏览器会自动根据服务器返回的 Access-Control-Max-Age 缓存预检结果。 // 前端无需特殊处理但了解其原理有助于调试。Fetch API 关键参数mode: cors: 这是默认值表示进行CORS跨域请求。其他值如no-cors限制很多响应不透明、same-origin严格同源用于特殊场景。credentials: 这个选项至关重要。omit(默认): 从不发送或接收Cookies。same-origin: 仅在同源请求中发送凭据。include:总是发送凭据即使跨域。当设置为include时后端必须响应Access-Control-Allow-Credentials: true且Access-Control-Allow-Origin不能为*。4.2 使用Axios库Axios提供了更简洁的语法和更好的错误处理是很多项目的首选。import axios from axios; // 创建一个配置了基础URL和默认凭据的实例 const apiClient axios.create({ baseURL: https://api.example.com, withCredentials: true, // 相当于Fetch的 credentials: include timeout: 10000, }); // 发起请求 apiClient.get(/data) .then(response console.log(response.data)) .catch(error { // Axios会将HTTP错误码如404, 500放入error.response if (error.response) { console.error(Server responded with error:, error.response.status, error.response.data); // 注意即使服务器返回4xx/5xx只要CORS头正确这里也能收到error.response } else if (error.request) { // 请求已发出但无响应通常是网络问题或CORS拦截 console.error(No response received:, error.request); // 典型的CORS错误会在这里被捕获因为浏览器阻止了请求所以没有响应。 } else { console.error(Error setting up request:, error.message); } }); // 带自定义头的POST请求 apiClient.post(/items, { name: New Item }, { headers: { X-Requested-With: XMLHttpRequest, Custom-Header: value } });Axios的withCredentials这个配置项和Fetch的credentials: include作用相同。一旦全局设置所有通过该实例发出的请求都会携带凭据。4.3 处理CORS错误与调试当CORS配置不正确时浏览器控制台会给出明确的错误信息。学会解读这些信息是快速定位问题的关键。Access to fetch at ‘…’ from origin ‘…’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.问题服务器没有返回Access-Control-Allow-Origin响应头。解决检查后端配置确保对跨域请求的响应中添加了这个头。…blocked by CORS policy: The ‘Access-Control-Allow-Origin’ header has a value ‘…’ that is not equal to the supplied origin.问题服务器返回的Access-Control-Allow-Origin值与当前页面的源不匹配。例如页面来自http://localhost:3000但服务器只允许https://production.com。解决调整服务器配置将当前页面的源加入允许列表或使用动态匹配逻辑。…blocked by CORS policy: Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.问题预检请求OPTIONS失败了可能返回了非2xx状态码如404, 500。解决确保你的后端服务器正确处理了OPTIONS方法的请求并返回了正确的CORS头部和2xx状态码通常是200或204。…blocked by CORS policy: Request header field x-custom-header is not allowed by Access-Control-Allow-Headers in preflight response.问题你请求中携带的自定义头x-custom-header不在服务器Access-Control-Allow-Headers的允许列表中。解决在后端配置的allowedHeaders中添加这个头。…blocked by CORS policy: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’.问题前端请求设置了credentials: ‘include’或Axios的withCredentials: true但服务器响应头Access-Control-Allow-Origin是通配符*。解决这是安全性强制要求。要么前端去掉withCredentials如果不需传Cookie要么后端将Access-Control-Allow-Origin改为具体的、请求来源的源。调试技巧打开浏览器开发者工具的Network网络面板。发起你的请求仔细查看发出的请求和收到的响应。重点关注OPTIONS请求如果看到浏览器先发了一个OPTIONS请求然后才是你的真实请求如POST说明这是一个“需预检的请求”。检查这个OPTIONS请求的响应头和状态码。查看请求头确认Origin头是否正确发送。查看响应头确认服务器返回了正确的Access-Control-Allow-*系列头部。5. 高级话题与常见陷阱掌握了基础配置后还有一些高级场景和容易踩的坑需要了解。5.1 预检请求的重定向问题根据CORS规范一个预检请求OPTIONS不应该被重定向。如果服务器对OPTIONS请求返回了3xx重定向状态码一部分浏览器会直接报错“The request was redirected to ‘…’, which is disallowed for cross-origin requests that require preflight.”解决方案最佳实践确保你的API端点URL是最终的避免对OPTIONS请求进行重定向。在Nginx或应用服务器配置中确保OPTIONS请求直接由处理CORS的模块或逻辑处理而不进入可能触发重定向的业务路由。变通方案如果无法避免重定向前端可以采取“两步走”策略先发一个简单的HEAD或GET请求不触发预检到初始URL从响应头如Location或响应URLResponse.url中获取重定向后的最终URL然后再用这个最终URL发起真正的带预检的请求。但这通常很麻烦且不优雅。5.2 通配符*的限制与动态源处理使用Access-Control-Allow-Origin: *非常方便但它有两个重大限制不能与Access-Control-Allow-Credentials: true同时使用。它允许任何来源这在很多需要限制访问的场景下是不安全的。生产环境的最佳实践是动态设置允许的源。具体做法是维护一个允许的源列表白名单可以存储在环境变量、配置文件或数据库中。在CORS中间件或处理逻辑中读取请求头中的Origin值。检查该Origin是否在白名单内。如果在则将Access-Control-Allow-Origin设置为这个具体的Origin值如果不在则要么不设置该头导致浏览器拒绝要么返回一个错误。Node.js/Express 动态源示例const allowedOrigins [https://app.example.com, https://staging.example.com, http://localhost:3000]; app.use(cors({ origin: function (origin, callback) { // 允许没有Origin头的请求例如来自curl、Postman的请求 if (!origin) return callback(null, true); if (allowedOrigins.indexOf(origin) -1) { const msg The CORS policy for this site does not allow access from the specified Origin: ${origin}; return callback(new Error(msg), false); } return callback(null, origin); // 将允许的源动态设置为请求的Origin }, credentials: true // 现在可以安全地设置凭据支持了 }));5.3 非简单请求的Content-Type陷阱一个常见的坑是认为POST请求都是简单请求。实际上只有当Content-Type是application/x-www-form-urlencoded、multipart/form-data或text/plain时POST才是简单请求。一旦你使用现在更常见的application/json它就会变成一个需预检的请求。这意味着如果你的前端用fetch或axios发送一个JSON body的POST请求而你的后端没有正确响应OPTIONS预检请求那么即使你配置了Access-Control-Allow-Origin请求也会失败。你必须确保后端能正确处理OPTIONS方法并返回包含Content-Type在Access-Control-Allow-Headers中的响应。5.4 缓存预检请求为了性能浏览器会缓存预检请求的结果。服务器通过Access-Control-Max-Age响应头来控制缓存时间单位秒。例如Access-Control-Max-Age: 3600表示这个预检结果在1小时内有效。这意味着在缓存有效期内对同一URL、使用相同方法和头部的后续请求浏览器将不再发送OPTIONS预检请求而是直接发出真实请求。这能显著提升频繁跨域请求的性能。注意事项如果你在开发过程中频繁修改后端CORS配置可能会因为浏览器缓存了旧的预检结果而导致新配置不生效。此时你需要强制刷新页面CtrlF5或在开发者工具的Network面板中勾选“Disable cache”来清除缓存进行测试。5.5 与Cookie/Session的结合当需要跨域共享登录状态时流程如下用户在前端域名如app.com登录登录请求POST到api.com/login需要设置withCredentials: true。后端api.com验证成功后在响应中设置Set-Cookie例如一个Session ID。由于CORS凭据设置正确这个Cookie会被浏览器保存并且其域属性是api.com。此后前端向api.com发起的任何请求只要设置了withCredentials: true浏览器都会自动附上这个Cookie。后端通过读取这个Cookie来识别用户会话。关键点跨域Cookie同样受到SameSite属性的约束。在现代浏览器中默认的SameSiteLax可能会阻止某些跨站请求携带Cookie。为了确保跨域场景正常工作后端在设置Cookie时可能需要明确指定SameSiteNone; Secure注意Secure要求必须使用HTTPS。6. 替代方案与CORS的边界虽然CORS是解决跨域问题的标准方案但在某些特定场景下也有其他备选或补充方案。6.1 JSONP一个过时的“ Hack”JSONP利用script标签没有跨域限制的特性来实现跨域数据获取。它要求服务器返回一段包裹在回调函数名中的JavaScript代码。// 前端 function handleResponse(data) { console.log(data); } const script document.createElement(script); script.src https://api.example.com/data?callbackhandleResponse; document.body.appendChild(script); // 服务器响应类似handleResponse({name: value});JSONP的严重局限性仅支持GET请求无法进行POST、PUT等操作。错误处理困难难以捕获HTTP错误状态。存在安全风险因为返回的是可执行脚本如果服务器被攻破返回恶意代码前端会直接执行。不符合现代Web开发规范。结论除非必须支持非常古老的浏览器如IE9以下否则绝对不要在新项目中使用JSONP。CORS是更安全、更强大、更标准化的解决方案。6.2 反向代理开发环境的利器在开发阶段你可能会遇到前端在localhost:3000后端API在localhost:8080的情况。为了避免CORS配置的麻烦一个非常流行的做法是使用反向代理。前端开发服务器如Vite、Webpack Dev Server、Create React App都内置了代理功能。你只需简单配置将特定API路径的请求转发到后端服务器。Vite配置示例 (vite.config.js)export default defineConfig({ server: { proxy: { // 将 /api 开头的请求代理到后端服务器 /api: { target: http://localhost:8080, changeOrigin: true, // 修改请求头中的Host为目标地址 rewrite: (path) path.replace(/^\/api/, ) // 可选重写路径 } } } })配置后前端代码中请求/api/users实际上会被转发到http://localhost:8080/users。由于请求是从开发服务器同源发出的浏览器不会触发CORS限制。这极大简化了本地联调。6.3 修改浏览器安全策略仅限开发强烈警告此方法仅用于本地开发调试绝对禁止用于生产环境或普通用户。在开发时为了快速测试可以启动禁用了同源策略的浏览器。Chrome(Mac/Linux):google-chrome --disable-web-security --user-data-dir/tmp/chrome-testChrome(Windows): 创建快捷方式在目标后添加--disable-web-security --user-data-dirC:\chrome_temp风险这会让你访问的任何网站都处于不安全状态切勿用于浏览日常网页。理解跨域请求远不止于在代码里加上几个响应头。它是一套完整的安全协商协议贯穿于现代Web应用架构的通信链路中。从简单的同源策略到复杂的预检握手从后端的动态源配置到前端的凭据管理每一个细节都关乎应用的安全性与可用性。处理CORS问题最佳路径永远是首先在Network面板中仔细查看请求和响应的每一个头部理解浏览器为何阻止然后根据错误信息对照CORS规则在后端进行精确的配置调整。记住通配符*是便捷的但也是危险的带凭据的请求必须与具体的源配对而预检请求的存在则是浏览器为我们数据安全设置的一道重要保险。