同源策略是浏览器最重要的安全机制之一,它规定页面中的脚本只能读取与当前页面同协议、同域名、同端口下的资源。一旦前端页面向不同源的服务端发起请求,浏览器就会对响应进行检查,缺少必要的CORS响应头时直接拦截,控制台通常会报出类似Access to XMLHttpRequest has been blocked by CORS policy的错误。要彻底解决这个问题,可以从两个方向入手:一是让服务端正确返回CORS响应头,二是通过代理让请求变成同源。本文分别展开讲解。

为什么浏览器要拦截跨域请求
首先要明确一点:跨域请求大多数情况下其实已经到达了服务端,服务端也正常返回了数据,拦截发生在浏览器端。浏览器拿到响应后会检查其中的CORS响应头,如果不满足要求,就会拒绝把响应交给页面脚本,这就是为什么有时你在服务端日志里能看到请求成功,而前端却报错的原因。
同源策略的初衷是保护用户安全。想象一下,如果你登录了银行网站,浏览器保存了Cookie,此时又访问了一个恶意网站,如果没有同源策略,恶意网站的脚本就能携带你的Cookie向银行发起请求并读取响应,用户的资产信息将被随意窃取。同源策略正是通过限制脚本的跨源读取能力来堵住这个漏洞。
跨域请求分为简单请求和预检请求两类。简单请求要求请求方法为GET、POST或HEAD,且请求头只包含Accept、Accept-Language、Content-Type(仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)等安全字段。一旦不满足这些条件,比如使用了Content-Type: application/json、请求方法为PUT或DELETE、或者携带了自定义token头,浏览器就会先发送一个OPTIONS方式的预检请求,询问服务端是否允许真正的请求。很多人配置了CORS却仍然失败,问题往往出在预检请求没有被正确处理。
服务端配置CORS响应头
CORS的核心是几个响应头。Access-Control-Allow-Origin指定允许访问的源,值可以是具体的协议加域名加端口,也可以是星号表示允许所有源,但注意与携带Cookie一起使用时不能为星号。Access-Control-Allow-Methods声明允许的HTTP方法,Access-Control-Allow-Headers声明允许的请求头,预检请求通过后服务端必须返回这两项。Access-Control-Allow-Credentials设为true时,浏览器才允许请求携带Cookie。Access-Control-Max-Age可以设置预检结果的缓存时间,减少不必要的OPTIONS请求。
以Nginx为例,一个典型的配置如下:
location /api/ {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Token" always;
# 预检请求直接返回,不转发到后端
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:8080/;
}注意配置中的always参数,它保证错误响应(如404、500)也会带上CORS头。如果不加,正常请求看起来跨域没问题,一旦接口报错,浏览器又会抛出CORS异常,让开发者误判问题根源。
在Node.js的Express框架中,可以使用cors中间件快速处理:
const express = require('express');
const cors = require('cors');
const app = express();
// 允许指定来源携带Cookie访问
app.use(cors({
origin: 'https://www.ipipp.com',
credentials: true,
maxAge: 86400
}));
app.listen(3000, () => console.log('服务已启动'));在Java Spring项目中,则可以通过全局配置类实现:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("https://*.ipipp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}无论哪种语言,配置思路都是一致的:对预检的OPTIONS请求返回204状态码并附带全部许可头,对实际请求的响应附加Access-Control-Allow-Origin等头信息。排查问题时,用浏览器开发者工具的Network面板查看OPTIONS请求的响应头,基本能定位到缺少哪一项配置。
代理方案:绕过跨域限制
如果服务端不在你的控制范围内,比如调用第三方接口,无法修改其响应头,代理就是更合适的方案。代理的原理很直接:让前端请求发给与自己同源的代理服务器,代理服务器在服务端转发请求给目标接口。由于服务端之间通信不受同源策略约束(同源策略只限制浏览器),跨域问题自然不存在。
开发环境下,前端构建工具普遍内置了代理支持。以Vite为例,在配置文件中这样写:
export default {
server: {
proxy: {
'/api': {
target: 'https://target.ipipp.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}这样页面中的请求只需写相对路径/api/user,与页面同源,浏览器不会拦截。Webpack对应的devServer.proxy配置方式几乎相同,Vue CLI创建的项目也可以在配置文件中做同样设置。
生产环境没有构建工具的devServer,一般用Nginx承担代理角色:
server {
listen 80;
location / {
root /var/www/dist; # 前端静态文件
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_set_header Host $http_host;
proxy_pass https://target.ipipp.com/;
}
}这种把前端静态资源和接口代理统一在同一个域名下的部署方式,本质上让所有请求都变成了同源请求,是生产环境最稳妥的做法。
使用代理时要注意两点:一是代理转发可能丢失或改写Host头和Cookie,必要时通过proxy_set_header显式传递;二是开发环境代理只在本地生效,不要以为配置了devServer就万事大吉,上线部署仍需Nginx等网关层的配合。把服务端CORS头和代理方案结合理解,跨域问题就再也没有神秘感了。