文章目录

JavaScript 跨域:CORS、JSONP、代理服务器

发布于 2026-04-04 10:53:50 · 浏览 4 次 · 评论 0 条

JavaScript 跨域解决方案详解

在日常的前端开发中,你一定遇到过类似的错误提示:Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'https://your-site.com' has been blocked by CORS policy。这个错误正是跨域问题导致的。本文将深入讲解跨域的本质以及三种主流解决方案:CORSJSONP代理服务器


为什么会有跨域限制?

跨域限制并非 JavaScript 语言本身的缺陷,而是浏览器安全策略的产物,这套策略被称为同源策略(Same-Origin Policy)。

什么是"同源"?

当两个 URL 的协议域名端口三者完全相同,它们就是同源的。以下是具体示例:

URL 是否与 https://www.example.com/page 同源
https://www.example.com/other ✅ 同源
https://api.example.com/data ❌ 域名不同
http://www.example.com/page ❌ 协议不同
https://www.example.com:8080/page ❌ 端口不同

同源策略限制了什么?

同源策略主要限制以下三类行为:

  1. Cookie、LocalStorage、SessionStorage 无法跨源读取
  2. DOM 元素 无法跨源操作
  3. Ajax/Fetch 请求 无法跨源发送

前两种限制相对容易理解,而第三种限制正是我们在开发中最常遇到的——浏览器会拦截不符合同源规则的 AJAX 请求,即便服务器成功返回了数据,浏览器也会拒绝将响应交给 JavaScript 处理。

为什么要设计同源策略?

假设没有同源策略,任何网页都可以随意发起请求获取其他网站的数据。恶意网站可以在用户不知情的情况下,冒充用户向银行、邮箱、社交平台发送请求并获取敏感信息。同源策略就是浏览器为保护用户数据而设置的第一道防线。


方案一:CORS

CORS(Cross-Origin Resource Sharing,跨域资源共享)是目前最主流、最推荐的跨域解决方案。它通过服务器端的 HTTP 响应头声明,允许指定来源的网页访问资源。

工作原理

CORS 的核心是服务器配置响应头,告诉浏览器允许哪些来源访问资源。浏览器在发送请求时会携带 Origin 头,服务器根据这个头决定是否允许访问:

# 浏览器发送的请求头
GET /api/data HTTP/1.1
Origin: https://frontend.com
Host: api.example.com

# 服务器的正确响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
Content-Type: application/json

如果服务器没有返回 Access-Control-Allow-Origin 头,或者返回的域名与请求的 Origin 不匹配,浏览器就会拒绝响应。

简单请求与预检请求

CORS 将请求分为两类,理解它们的区别对正确配置至关重要。

简单请求(Simple Request)满足以下条件:

  • 请求方法为 GETPOSTHEAD
  • 请求头仅包含 AcceptAccept-LanguageContent-LanguageContent-Type(且值为 application/x-www-form-urlencodedmultipart/form-datatext/plain

简单请求会直接发送,服务器响应后浏览器检查头信息决定是否放行。

预检请求(Preflight Request)用于复杂请求,会先发送一个 OPTIONS 请求确认服务器是否允许:

# 预检请求
OPTIONS /api/data HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type

# 预检响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

常见的复杂请求包括:PUTDELETE 等方法,或携带自定义头(如 Authorization)。

服务器配置示例

以下是不同后端框架的 CORS 配置示例:

Express(Node.js)

const express = require('express');
const cors = require('cors');

const app = express();

// 允许所有来源(仅限开发环境)
app.use(cors());

// 生产环境指定具体来源
app.use(cors({
  origin: 'https://your-frontend.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true  // 允许携带 Cookie
}));

app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello World' });
});

app.listen(3000);

Spring Boot(Java)

import org.springframework.context.annotation.Bean;
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 CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOriginPatterns("https://*.your-domain.com")
                        .allowedMethods("GET", "POST", "PUT", "DELETE")
                        .allowedHeaders("*")
                        .allowCredentials(true)
                        .maxAge(3600);
            }
        };
    }
}

Flask(Python)

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
# 允许所有来源
CORS(app, resources={"/api/*": {"origins": "*"}})

# 或者指定来源
CORS(app, 
     origins=["https://www.your-site.com"],
     supports_credentials=True)

@app.route('/api/data')
def get_data():
    return {'message': 'Hello World'}

常见响应头详解

响应头 说明
Access-Control-Allow-Origin 允许访问的来源域名,可设具体值或 *(允许所有)
Access-Control-Allow-Methods 允许的 HTTP 方法列表
Access-Control-Allow-Headers 允许的请求头列表
Access-Control-Allow-Credentials 是否允许携带 Cookie(truefalse
Access-Control-Max-Age 预检结果的缓存时间(秒)
Access-Control-Expose-Headers 允许 JavaScript 读取的响应头

特别注意:当 Access-Control-Allow-Credentialstrue 时,Access-Control-Allow-Origin 不能设置为 *,必须指定具体域名。


方案二:JSONP

JSONP(JSON with Padding)是早期浏览器不支持 CORS 时的一种"hack"方案。它利用 <script> 标签不受同源策略限制的特性来实现跨域数据获取。由于现代浏览器已普遍支持 CORS,JSONP 只在一些遗留系统中还能见到。

工作原理

<script> 标签可以加载任何域名的脚本文件,这个特性被 JSONP 利用来解决跨域问题:

<!-- 1. 在页面中定义回调函数 -->
<script>
  function handleResponse(data) {
    console.log('收到数据:', data);
  }
</script>

<!-- 2. 服务端返回的脚本会调用这个函数 -->
<script src="https://api.example.com/data?callback=handleResponse"></script>

<script> 标签加载完成后,服务端返回的 JavaScript 代码会立即执行:

// 服务端返回的内容
handleResponse({ name: "John", age: 30 });

浏览器会把它当作普通 JavaScript 执行,从而调用 handleResponse 函数并传入数据。

服务端实现示例

Express(Node.js)

const express = require('express');
const app = express();

// 处理 JSONP 请求
app.get('/api/jsonp', (req, res) => {
  const { callback } = req.query;
  const data = { message: '这是 JSONP 返回的数据' };

  // 确保 callback 参数存在
  if (callback) {
    // 返回包装了回调函数的 JavaScript 代码
    res.type('application/javascript');
    res.send(`${callback}(${JSON.stringify(data)})`);
  } else {
    res.status(400).json({ error: 'Missing callback parameter' });
  }
});

app.listen(3000);

客户端调用

// 封装 JSONP 请求函数
function jsonp(url, callbackName = 'jsonpCallback') {
  return new Promise((resolve, reject) => {
    // 定义全局回调函数
    window[callbackName] = (data) => {
      resolve(data);
      // 清理
      delete window[callbackName];
      const script = document.querySelector(`script[src*="${url}"]`);
      if (script) script.remove();
    };
    
    // 创建 script 标签
    const script = document.createElement('script');
    const separator = url.includes('?') ? '&' : '?';
    script.src = `${url}${separator}callback=${callbackName}`;
    script.onerror = reject;
    document.body.appendChild(script);
  });
}

// 使用示例
jsonp('https://api.example.com/data')
  .then(data => console.log(data))
  .catch(err => console.error(err));

JSONP 的局限性

JSONP 存在明显的缺陷,使其在现代开发中逐渐被淘汰:

  1. 仅支持 GET 请求:JSONP 通过 <script> 加载实现,无法发送 POST、PUT 等请求
  2. 安全风险:JSONP 返回的是直接执行的 JavaScript 代码,如果服务端被攻击,可能注入恶意脚本
  3. 错误处理困难:JSONP 没有状态码,无法区分请求失败的具体原因
  4. 没有超时控制:无法设置请求超时时间

方案三:代理服务器

代理服务器方案的思路是"曲线救国":浏览器直接请求同源的代理服务器,代理服务器再向目标服务器转发请求。由于服务器之间的通信不受同源策略限制,问题迎刃而解。

方案对比

代理方案可分为前端代理后端代理两种:

类型 优点 缺点
前端代理(如 Vite、Webpack Dev Server) 配置简单,开发环境即开即用 仅限于开发环境,生产环境需额外配置
后端代理(Nginx、Node.js、Python 等) 开发、生产环境通用 需要额外服务器配置和维护成本

开发环境:使用 Vite 代理

如果你的项目使用 Vite 作为构建工具,配置代理只需几步:

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',  // 目标服务器
        changeOrigin: true,                 // 修改 Origin 头
        secure: false,                      // 允许代理到 HTTPS
        rewrite: (path) => path.replace(/^\/api/, '')  // 路径重写
      }
    }
  }
});

配置完成后,前端代码可以这样请求:

// 浏览器请求 /api/user,实际转发到 https://api.example.com/user
fetch('/api/user')
  .then(response => response.json())
  .then(data => console.log(data));

开发环境:使用 Webpack Dev Server

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },  // 移除 /api 前缀
        onProxyReq: (proxyReq, req, res) => {
          // 可以在这里修改请求头
        }
      }
    }
  }
};

生产环境:Nginx 反向代理

在生产环境中,使用 Nginx 作为反向代理是常见的选择:

server {
    listen 80;
    server_name your-frontend.com;

    # 前端静态资源
    location / {
        root /var/www/your-frontend/dist;
        try_files $uri $uri/ /index.html;
    }

    # API 代理
    location /api/ {
        proxy_pass https://api.example.com/;
        proxy_set_header Host api.example.com;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # WebSocket 支持(如果需要)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

上述配置的关键点说明:

  • proxy_pass 指定目标服务器地址,注意末尾的 / 会去掉匹配到的路径前缀
  • proxy_set_header Host 转发原始 Host 头,避免服务端基于 Host 的校验失败
  • proxy_set_header X-Real-IP 传递客户端真实 IP 给后端

生产环境:Node.js 代理

如果不想配置 Nginx,可以使用 Node.js 自己实现代理服务:

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 静态文件服务
app.use(express.static('./dist'));

// API 代理中间件
app.use('/api', createProxyMiddleware({
  target: 'https://api.example.com',
  changeOrigin: true,
  pathRewrite: { '^/api': '' },
  onProxyReq: (proxyReq, req, res) => {
    // 转发 Cookie
    if (req.headers.cookie) {
      proxyReq.setHeader('cookie', req.headers.cookie);
    }
  },
  onError: (err, req, res) => {
    console.error('代理请求失败:', err.message);
    res.status(502).json({ error: 'Bad Gateway' });
  }
}));

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

前端代码无需改动

使用代理后,前端代码可以完全按照同源方式编写:

// 开发时请求 /api,实际走代理
// 生产时可能也是 /api,指向 Nginx 或 Node.js 代理
// 总之对前端来说都是"同源"请求

async function fetchData() {
  const response = await fetch('/api/users');
  const data = await response.json();
  return data;
}

// 支持所有 HTTP 方法
async function submitData(data) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  return response.json();
}

三种方案对比

特性 CORS JSONP 代理服务器
支持请求方法 所有方法(GET、POST 等) 仅 GET 所有方法
浏览器支持 现代浏览器均支持 所有浏览器 所有浏览器
服务端改动 需要配置响应头 需要返回特殊格式 需要额外服务器
安全性 高(服务器完全控制) 低(可能注入脚本) 高(服务器可控)
复杂度 中等
适用场景 服务端可控时 遗留系统兼容 服务端不可控或需要统一入口

如何选择?

选择跨域解决方案时,考虑以下因素:

优先选择 CORS——当你能够控制服务器配置时,CORS 是最佳选择。它简洁、安全、支持所有 HTTP 方法,是现代 Web 开发的标准做法。

使用代理方案——当你无法修改服务端配置,或者需要同时代理多个后端服务时,代理服务器是可靠的解决方案。开发环境推荐使用 Vite/Webpack 的代理功能,生产环境推荐 Nginx 或 Node.js。

避免使用 JSONP——除非维护必须兼容 JSONP 的遗留系统,否则不要在新项目中使用 JSONP。它不仅功能受限,还存在安全风险。


总结

跨域问题是前端开发者必须理解的基础概念。CORS 通过服务端配置响应头实现精确控制,是现代开发的首选方案。JSONP 依赖 <script> 标签的漏洞,仅适用于 GET 请求的遗留场景。代理服务器通过同源服务器转发请求,绕过浏览器的同源策略限制,适用于服务端不可控的情况。根据你的实际场景选择合适的方案,就能有效解决跨域问题。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文