一、跨域问题的本质是什么

跨域问题就像两个邻居隔着围墙聊天,明明说的都是普通话,但就是听不清对方在说什么。浏览器出于安全考虑,默认禁止不同源的脚本交互,这就是著名的"同源策略"。

同源策略要求协议、域名、端口三者完全相同才算同源。举个例子:

  • https://a.comhttp://a.com 不同源(协议不同)
  • https://a.comhttps://b.com 不同源(域名不同)
  • https://a.com:80https://a.com:8080 不同源(端口不同)

在实际开发中,前后端分离架构非常普遍,前端可能运行在localhost:3000,而后端API在api.example.com,这就必然遇到跨域问题。

二、最基础的解决方案:CORS配置

CORS(跨域资源共享)是W3C标准,也是目前最主流的解决方案。它的核心思想是:服务器告诉浏览器"这个请求我允许"。

PHP中实现CORS非常简单,只需在响应头中添加几个字段:

<?php
// 允许来自任意域的请求(生产环境应替换为具体域名)
header("Access-Control-Allow-Origin: *");

// 允许的HTTP方法
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");

// 允许携带的请求头
header("Access-Control-Allow-Headers: Content-Type, Authorization");

// 预检请求缓存时间(单位:秒)
header("Access-Control-Max-Age: 86400");

// 允许浏览器在跨域请求时携带cookie
header("Access-Control-Allow-Credentials: true");

// 如果是OPTIONS请求(预检请求),直接返回
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    exit;
}

// 你的业务逻辑代码...
echo json_encode(['data' => '跨域请求成功']);

这个方案有几个关键点需要注意:

  1. Access-Control-Allow-Origin在生产环境最好不要用*,应该明确指定允许的域名
  2. 带cookie的请求不能使用*,必须明确指定域名
  3. 复杂请求(如Content-Type为application/json)会先发OPTIONS预检请求

三、进阶方案:Nginx反向代理

如果你有服务器配置权限,使用Nginx反向代理是更优雅的方案。它相当于在前后端之间架设一座桥梁,让浏览器以为所有请求都来自同源。

配置示例:

server {
    listen 80;
    server_name frontend.com;
    
    location / {
        root /var/www/html;
        index index.html;
    }
    
    location /api/ {
        proxy_pass http://backend.com/;  # 后端真实地址
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # CORS配置
        add_header 'Access-Control-Allow-Origin' 'http://frontend.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,Content-Type';
        add_header 'Access-Control-Allow-Credentials' 'true';
        
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }
}

这种方案的优点:

  • 前端代码无需任何修改
  • 可以隐藏后端真实地址
  • 配置一次,所有接口自动支持跨域
  • 性能比PHP处理更好

四、特殊场景解决方案

1. JSONP方案(仅限GET请求)

虽然现在不推荐使用,但在某些特殊场景下(如需要支持老旧浏览器),JSONP仍是一种选择。

前端代码:

function handleResponse(data) {
    console.log('收到响应:', data);
}

const script = document.createElement('script');
script.src = 'http://backend.com/api?callback=handleResponse';
document.body.appendChild(script);

后端PHP代码:

<?php
$callback = $_GET['callback'];
$data = ['status' => 'success', 'data' => '这是返回的内容'];
header('Content-Type: application/javascript');
echo $callback . '(' . json_encode($data) . ')';

JSONP的局限性很明显:

  • 只支持GET请求
  • 错误处理困难
  • 存在XSS风险

2. WebSocket跨域

WebSocket协议本身支持跨域,但服务端需要明确允许:

<?php
$server = new \Swoole\WebSocket\Server("0.0.0.0", 9501);

// 处理握手
$server->on('handshake', function ($request, $response) {
    // 检查origin是否允许
    $origin = $request->header['origin'] ?? '';
    $allowedOrigins = ['http://frontend.com', 'http://localhost:3000'];
    
    if (!in_array($origin, $allowedOrigins)) {
        $response->end();
        return false;
    }
    
    // WebSocket握手处理...
    return true;
});

$server->on('message', function ($server, $frame) {
    $server->push($frame->fd, "服务器回复: {$frame->data}");
});

$server->start();

五、实战中的注意事项

  1. 凭证处理:带cookie的跨域请求需要特别注意:

    // 前端
    fetch('http://backend.com/api', {
        credentials: 'include'
    });
    
    // 后端
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Origin: http://frontend.com'); // 不能是*
    
  2. 复杂请求:PUT/DELETE请求或自定义头会触发预检请求,服务器必须正确处理OPTIONS方法。

  3. 缓存问题:预检请求结果可能会被浏览器缓存,修改CORS配置后客户端可能需要清理缓存。

  4. 安全性:宽松的CORS配置可能导致CSRF攻击,建议:

    • 严格限制允许的源
    • 使用CSRF Token
    • 敏感操作要求身份验证

六、现代PHP框架中的最佳实践

以Laravel框架为例,推荐使用专门的CORS中间件:

  1. 首先安装fruitcake/laravel-cors包:
composer require fruitcake/laravel-cors
  1. 创建中间件:
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CorsMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        
        $response->headers->set('Access-Control-Allow-Origin', env('ALLOWED_ORIGINS'));
        $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
        $response->headers->set('Access-Control-Allow-Credentials', 'true');
        
        return $response;
    }
}
  1. .env中配置允许的源:
ALLOWED_ORIGINS=http://localhost:3000,https://frontend.com

七、总结与选型建议

经过以上探讨,我们可以得出以下结论:

  1. 简单项目:直接使用PHP的header函数设置CORS头是最快上手的方案。

  2. 企业级项目:推荐使用Nginx反向代理或框架中间件方案,更易于维护和扩展。

  3. 特殊需求

    • 需要支持老旧浏览器:考虑JSONP
    • 实时通信:WebSocket
    • 文件上传:注意处理带Content-Type的复杂请求
  4. 性能考量:Nginx处理CORS的性能优于PHP,高并发场景应优先考虑。

  5. 安全建议:始终遵循最小权限原则,不要盲目使用Access-Control-Allow-Origin: *

跨域问题看似简单,但实际开发中可能会遇到各种边界情况。理解其底层原理,根据项目需求选择合适的解决方案,才能写出既安全又高效的代码。