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

每次看到前端小朋友和后端开发因为接口调不通吵架,我就想起自己刚入行时被跨域支配的恐惧。其实跨域问题就像两个小区之间的围墙 - 前端住在A小区,后端住在B小区,浏览器这个尽职的保安严格执行着"同源策略"的规定。

所谓同源策略,就是要求协议、域名、端口三者必须完全相同。比如:

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

这三种情况都会触发跨域限制。而在现代前后端分离架构中,前端框架(如Vue/React)运行在开发服务器的3000端口,后端API运行在5000端口,跨域问题就不可避免了。

二、DotNetCore的解决方案全家桶

1. CORS中间件 - 官方推荐方案

DotNetCore提供了非常优雅的CORS支持,只需要几行代码就能搞定。我们来看一个完整的示例:

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // 添加CORS服务
    services.AddCors(options =>
    {
        options.AddPolicy("MyPolicy", builder =>
        {
            builder.WithOrigins("http://localhost:3000") // 允许的前端地址
                   .AllowAnyHeader()                    // 允许所有头
                   .AllowAnyMethod()                   // 允许所有HTTP方法
                   .AllowCredentials();                // 允许携带凭证
        });
    });
    
    services.AddControllers();
}

public void Configure(IApplicationBuilder app)
{
    // 使用CORS中间件
    app.UseCors("MyPolicy");
    
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

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

  • WithOrigins可以配置多个域名,用逗号分隔
  • 生产环境一定要替换掉localhost为真实域名
  • 如果前端需要发送cookie,必须设置AllowCredentials

2. 代理方案 - 开发环境的最佳实践

有时候我们不想在前端代码里写完整URL,这时候可以配置开发服务器代理。以Vue CLI为例:

// vue.config.js
module.exports = {
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:5000', // 后端地址
                changeOrigin: true,
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
}

这样前端代码里只需要写/api/users,请求会被自动转发到后端,完全避免了跨域问题。这个方案特别适合开发阶段使用。

三、那些年我们踩过的坑

1. 预检请求(OPTIONS)的坑

当请求满足以下条件时,浏览器会先发送OPTIONS预检请求:

  • 使用了PUT、DELETE等非简单方法
  • 自定义了请求头
  • Content-Type不是application/x-www-form-urlencoded、multipart/form-data或text/plain

很多同学配置了CORS但还是报错,往往是因为没处理好OPTIONS请求。DotNetCore的CORS中间件已经帮我们处理了这个问题,但如果用Nginx做反向代理就需要额外配置:

location / {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        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-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
    }
}

2. 带凭证的请求要特别注意

当请求需要携带cookie或认证头时,必须满足三个条件:

  1. 后端必须设置AllowCredentials
  2. Access-Control-Allow-Origin不能是通配符*
  3. 前端需要设置withCredentials
// 前端axios示例
axios.get('http://api.example.com/data', {
    withCredentials: true
});
// 后端DotNetCore配置
builder.WithOrigins("http://frontend.example.com")
       .AllowCredentials();

四、进阶场景与最佳实践

1. 动态允许源

有时候我们需要根据请求动态判断是否允许跨域,比如多租户系统。这时可以自定义一个策略:

services.AddCors(options =>
{
    options.AddPolicy("DynamicPolicy", builder =>
    {
        builder.AllowAnyHeader()
               .AllowAnyMethod()
               .SetIsOriginAllowed(origin => 
               {
                   // 这里可以写自定义逻辑
                   return origin.EndsWith(".example.com") 
                       || origin == "http://localhost:3000";
               })
               .AllowCredentials();
    });
});

2. 生产环境配置建议

生产环境的CORS配置应该更加严格:

  • 明确指定允许的域名,不要使用通配符
  • 限制允许的HTTP方法
  • 设置适当的缓存时间
  • 配合HTTPS使用
services.AddCors(options =>
{
    options.AddPolicy("ProductionPolicy", builder =>
    {
        builder.WithOrigins("https://www.myapp.com")
               .WithMethods("GET", "POST", "PUT")
               .WithHeaders("Content-Type", "Authorization")
               .SetPreflightMaxAge(TimeSpan.FromHours(1));
    });
});

五、总结与选型建议

经过这么多年的实践,我的建议是:

  1. 开发环境使用代理方案最方便
  2. 生产环境一定要用CORS中间件
  3. 简单项目用基本配置即可
  4. 复杂项目考虑动态源配置

记住,跨域安全策略是为了保护用户,不要为了方便而完全禁用安全限制。正确的做法是根据业务需求找到最合适的平衡点。