CORS 跨域深入
大约 13 分钟约 3890 字
CORS 跨域深入
简介
CORS(Cross-Origin Resource Sharing)跨域资源共享是浏览器同源策略的扩展机制。深入理解 CORS 的预检请求流程、凭证策略、通配符限制、中间件管道位置以及与 CSRF 的关系,有助于在生产环境中正确配置跨域,避免安全漏洞和调试困扰。
特点
同源策略与 CORS 原理
什么是同源
同源判定:协议 + 域名 + 端口 三者完全一致
http://example.com/api ← 源 A
https://example.com/api ← 不同源(协议不同)
http://example.com:8080 ← 不同源(端口不同)
http://api.example.com ← 不同源(域名不同)
http://example.com/api ← 同源
简单请求(不需要预检):
GET、HEAD、POST
Content-Type 仅限:text/plain、multipart/form-data、application/x-www-form-urlencoded
自定义头仅限:Accept、Accept-Language、Content-Language、Content-Type
预检请求(需要 OPTIONS):
PUT、DELETE、PATCH
Content-Type 为 application/json
自定义请求头(如 Authorization、X-Custom-Header)预检请求流程
浏览器 服务器
│ │
│── OPTIONS /api/data ────────→│ Access-Control-Allow-Origin: https://app.com
│ Origin: https://app.com │ Access-Control-Allow-Methods: GET,POST,PUT,DELETE
│ Access-Control-Request- │ Access-Control-Allow-Headers: Content-Type,Authorization
│ Method: PUT │ Access-Control-Max-Age: 3600
│ Access-Control-Request- │ Access-Control-Allow-Credentials: true
│ Headers: Authorization │
│ │
│←── 204 No Content ──────────│
│ │
│── PUT /api/data ────────────→│ 正常响应(带 CORS 头)
│ Origin: https://app.com │
│ Authorization: Bearer xxx │
│←── 200 OK ─────────────────│ Access-Control-Allow-Origin: https://app.com
│ │ Access-Control-Allow-Credentials: trueCORS 中间件深入
中间件源码级执行流程
// CORS 中间件的核心处理逻辑(简化版)
// 源码路径:Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs
public class CorsMiddleware
{
// 1. 检查是否是预检请求
// 2. 根据策略评估 Origin
// 3. 写入 CORS 响应头
// 4. 预检请求直接返回 204
// 关键:CORS 中间件必须放在 UseRouting 之后
// 因为终结点元数据中的 [EnableCors]/[DisableCors] 需要路由匹配后才能读取
}
// 正确的中间件顺序
var app = builder.Build();
app.UseExceptionHandler(); // 1. 异常处理(最先)
app.UseHsts(); // 2. HTTPS 重定向
app.UseHttpsRedirection(); // 3. HTTPS
app.UseCors(); // 4. CORS(必须在 UseRouting 之后)
app.UseAuthentication(); // 5. 认证
app.UseAuthorization(); // 6. 授权
app.MapControllers(); // 7. 路由映射策略优先级规则
// 优先级从低到高:
// 1. 默认策略 (AddDefaultPolicy) → 最低
// 2. 命名策略 (AddPolicy) → 通过 app.UseCors("name") 启用
// 3. [EnableCors("name")] → 控制器/Action 级别
// 4. .RequireCors("name") → 终结点级别(Minimal API)
// 5. [DisableCors] → 最高优先级,直接跳过 CORS
// 关键规则:
// - [EnableCors] 必须指定策略名,不能为空
// - 一旦在 Action 上使用了 [EnableCors],控制器的 [EnableCors] 被覆盖
// - [DisableCors] 可以在任何级别禁用 CORS
[ApiController]
[Route("api/[controller]")]
[EnableCors("ProdPolicy")]
public class OrdersController : ControllerBase
{
[HttpGet] // 使用 ProdPolicy
public IActionResult GetAll() => Ok();
[HttpGet("public")]
[EnableCors("DevPolicy")] // 覆盖为 DevPolicy
public IActionResult GetPublic() => Ok();
[HttpPost("internal")]
[DisableCors] // 完全禁用 CORS
public IActionResult Internal() => Ok();
}凭证与通配符深入
为什么 AllowAnyOrigin + AllowCredentials 会失败
// 浏览器规范明确规定:
// 当 Access-Control-Allow-Credentials: true 时
// Access-Control-Allow-Origin 不能为 *
// 因为 * 意味着"任何网站都可以携带用户凭证访问"
// 以下代码会抛出 InvalidOperationException:
builder.Services.AddCors(options =>
{
options.AddPolicy("Invalid", policy =>
policy.AllowAnyOrigin().AllowCredentials()); // 抛异常!
});
// 正确做法 1:明确指定 Origin
options.AddPolicy("Valid1", policy =>
policy.WithOrigins("https://app.example.com", "https://admin.example.com")
.AllowCredentials());
// 正确做法 2:动态验证 Origin
options.AddPolicy("Valid2", policy =>
policy.SetIsOriginAllowed(origin =>
{
var uri = new Uri(origin);
// 只允许 *.example.com 的子域名
return uri.Host.EndsWith(".example.com") && uri.Scheme == "https";
})
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
// 正确做法 3:通配符子域名(.NET 6+)
options.AddPolicy("Valid3", policy =>
policy.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins("https://*.example.com")
.AllowCredentials());凭证模式下的响应头限制
// 当 AllowCredentials = true 时:
// Access-Control-Allow-Origin 必须是具体的源(不能是 *)
// Access-Control-Expose-Headers 不能用通配符(某些旧浏览器)
// Vary: Origin 头必须存在(缓存隔离)
// 暴露自定义响应头给前端
builder.Services.AddCors(options =>
{
options.AddPolicy("WithExposedHeaders", policy =>
policy.WithOrigins("https://app.example.com")
.AllowCredentials()
.WithExposedHeaders("X-Total-Count", "X-Request-Id", "X-Page-Token"));
});
// 前端读取自定义头
// fetch('/api/data').then(res => {
// const totalCount = res.headers.get('X-Total-Count');
// });动态 CORS 策略
基于配置的动态源
// appsettings.json
// {
// "Cors": {
// "AllowedOrigins": [
// "https://app.example.com",
// "https://admin.example.com",
// "https://partner.example.com"
// ]
// }
// }
builder.Services.AddCors(options =>
{
options.AddPolicy("DynamicFromConfig", policy =>
{
var origins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>() ?? Array.Empty<string>();
policy.WithOrigins(origins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
});基于数据库的动态源
// 从数据库/缓存动态加载允许的 Origin
builder.Services.AddCors(options =>
{
options.AddPolicy("DynamicFromDb", policy =>
{
policy.SetIsOriginAllowed(async origin =>
{
var uri = new Uri(origin);
if (uri.Scheme != "https") return false;
// 从缓存/数据库查询
var allowedOrigins = await _originCache.GetAllowedOriginsAsync();
return allowedOrigins.Contains(uri.Host);
})
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(5));
});
});
// 自定义 CORS 中间件(适用于更复杂的场景)
app.Use(async (context, next) =>
{
var origin = context.Request.Headers.Origin.ToString();
if (!string.IsNullOrEmpty(origin))
{
var isValidOrigin = await _originService.IsValidOriginAsync(origin);
if (isValidOrigin)
{
context.Response.Headers["Access-Control-Allow-Origin"] = origin;
context.Response.Headers["Access-Control-Allow-Credentials"] = "true";
context.Response.Headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,PATCH";
context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-Request-Id";
context.Response.Headers["Access-Control-Max-Age"] = "600";
context.Response.Headers["Vary"] = "Origin"; // 缓存隔离
}
// 处理预检请求
if (context.Request.Method == HttpMethods.Options)
{
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}
}
await next();
});预检缓存与性能
PreflightMaxAge 配置
// 预检请求缓存时间
// - 太短:频繁发送 OPTIONS 请求,增加延迟
// - 太长:策略变更后客户端长时间不更新
// 不同场景的推荐值:
builder.Services.AddCors(options =>
{
// 内部 API(策略很少变化)
options.AddPolicy("Internal", policy =>
policy.WithOrigins("https://internal.example.com")
.SetPreflightMaxAge(TimeSpan.FromHours(1))); // 1 小时
// 外部 API(策略可能调整)
options.AddPolicy("External", policy =>
policy.WithOrigins("https://partner.example.com")
.SetPreflightMaxAge(TimeSpan.FromMinutes(10))); // 10 分钟
// 开发环境
options.AddPolicy("Dev", policy =>
policy.AllowAnyOrigin()
.SetPreflightMaxAge(TimeSpan.FromMinutes(1))); // 1 分钟
});
// 预检请求的性能影响
// 1. 每个新 Origin + 新 Method + 新 Header 组合都会触发 OPTIONS
// 2. 浏览器缓存按 Origin + Method + Headers 三元组隔离
// 3. 预检结果不跨页面共享(不同标签页独立缓存)常见问题与排错
CORS 调试清单
// 问题 1:响应头缺失
// 原因:中间件顺序错误,CORS 放在了 UseRouting 之前
// 解决:确保 app.UseCors() 在 app.UseRouting() 之后
// 问题 2:预检请求 404
// 原因:路由没有匹配 OPTIONS 方法
// 解决:确保控制器有 [HttpOptions] 或全局允许
// 问题 3:Origin 不在允许列表
// 排查:查看浏览器控制台 Network → Preflight Response
// 确认请求的 Origin 与 WithOrigins 中的值完全一致(含协议和端口)
// 问题 4:Credentials 下仍然报错
// 原因:AllowAnyOrigin 和 AllowCredentials 冲突
// 解决:改用 WithOrigins 或 SetIsOriginAllowed
// 问题 5:自定义头无法读取
// 原因:未调用 WithExposedHeaders
// 解决:
policy.WithExposedHeaders("X-Custom-Header", "X-Total-Count");
// 调试中间件(开发环境)
app.Use(async (context, next) =>
{
if (context.Request.Method == HttpMethods.Options)
{
Console.WriteLine($"[CORS Debug] Preflight from: {context.Request.Headers.Origin}");
}
await next();
// 记录 CORS 响应头
var corsHeaders = new[] {
"Access-Control-Allow-Origin",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Allow-Credentials",
"Access-Control-Max-Age",
"Access-Control-Expose-Headers"
};
foreach (var header in corsHeaders)
{
if (context.Response.Headers.TryGetValue(header, out var value))
Console.WriteLine($"[CORS Debug] {header}: {value}");
}
});CORS 与 CSRF 的区别
CORS(跨域资源共享):
- 控制跨域请求是否被允许
- 服务器通过响应头声明策略
- 解决的是"浏览器是否发送请求"的问题
CSRF(跨站请求伪造):
- 利用用户的已认证身份发起恶意请求
- 浏览器会自动携带 Cookie
- 即使没有 CORS,简单请求(GET/POST form)也会被发送
防护建议:
1. CORS 限制 Origin → 防止第三方页面读取响应
2. CSRF Token → 防止恶意请求被服务器接受
3. SameSite Cookie → 阻止跨站请求携带 Cookie
4. 两者配合使用才能完整防护CORS 与反向代理
Nginx 层面的 CORS 处理
// 在生产环境中,CORS 可能在反向代理层(Nginx)处理
// 如果 Nginx 已经处理了 CORS,ASP.NET Core 就不需要再配置
// 否则可能出现重复的 CORS 响应头
// Nginx CORS 配置示例:
// add_header Access-Control-Allow-Origin $http_origin always;
// add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
// add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
// add_header Access-Control-Allow-Credentials true always;
// add_header Vary Origin always;
// 注意:如果 Nginx 和 ASP.NET Core 同时处理 CORS
// 响应头中会出现两个 Access-Control-Allow-Origin
// 浏览器会拒绝这种响应
// 解决方案:在 Nginx 层统一处理 CORS,或者在 ASP.NET Core 层处理
// 不要两层同时处理反向代理下的 Origin 透传
// 当 ASP.NET Core 部署在反向代理后面时
// Origin 头由浏览器发送,不受反向代理影响
// 但 Host 头可能被修改,需要使用 ForwardedHeaders 中间件
// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
// 在 UseCors 之前调用
app.UseForwardedHeaders();Minimal API 中的 CORS
终结点级别的 CORS 配置
// Minimal API 中使用 RequireCors
var app = builder.Build();
app.MapGet("/api/public", () => "公开数据")
.RequireCors("AllowAll");
app.MapGet("/api/secure", () => "安全数据")
.RequireCors("ProdPolicy")
.RequireAuthorization();
// 禁用特定终结点的 CORS
app.MapGet("/api/internal", () => "内部数据")
.DisableCors();
// 带名称的策略组
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
options.AddPolicy("ProdPolicy", policy =>
policy.WithOrigins("https://app.example.com")
.AllowCredentials());
});跨域携带 Cookie 的完整示例
前后端联调配置
// 后端配置
builder.Services.AddCors(options =>
{
options.AddPolicy("CookiePolicy", policy =>
{
policy.WithOrigins("https://www.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithExposedHeaders("X-Request-Id")
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
});
// Cookie 配置 — SameSite 属性很关键
builder.Services.Configure<CookiePolicyOptions>(options =>
{
// 跨域场景必须使用 SameSite=None
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always; // SameSite=None 要求 Secure
});
// 前端 fetch 配置
// fetch('https://api.example.com/data', {
// credentials: 'include', // 携带 Cookie
// headers: { 'Content-Type': 'application/json' }
// })
// 前端 axios 配置
// axios.defaults.withCredentials = true;
// axios.defaults.baseURL = 'https://api.example.com';CORS 与 OAuth2/OIDC 的关系
Token 交换流程中的 CORS
// OAuth2 授权码流程中 CORS 的角色:
// 1. 浏览器 → 授权服务器(重定向):不受 CORS 限制(导航不是 XHR)
// 2. 浏览器 → 资源服务器(API 调用):需要 CORS 配置
// 3. 浏览器 → Token 端点(PKCE 场景):需要 Token 端点配置 CORS
// BFF(Backend For Frontend)模式可以完全避免 CORS
// 前端 → BFF(同源)→ 资源服务器(服务器到服务器,无 CORS)
// 如果使用 SPA 直接调用 API
builder.Services.AddCors(options =>
{
options.AddPolicy("SpaPolicy", policy =>
{
// SPA 的地址
policy.WithOrigins("https://spa.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});测试中的 CORS 验证
集成测试验证 CORS 头
// 使用 WebApplicationFactory 测试 CORS 配置
public class CorsTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public CorsTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task Preflight_ReturnsCorrectHeaders()
{
// Arrange
var client = _factory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Options, "/api/data");
request.Headers.Add("Origin", "https://app.example.com");
request.Headers.Add("Access-Control-Request-Method", "PUT");
request.Headers.Add("Access-Control-Request-Headers", "Authorization");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Equal("https://app.example.com",
response.Headers.GetValues("Access-Control-Allow-Origin").First());
Assert.Contains("PUT",
response.Headers.GetValues("Access-Control-Allow-Methods").First());
Assert.Contains("Authorization",
response.Headers.GetValues("Access-Control-Allow-Headers").First());
}
[Fact]
public async Task ActualRequest_ReturnsCorsHeaders()
{
var client = _factory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/api/data");
request.Headers.Add("Origin", "https://app.example.com");
var response = await client.SendAsync(request);
response.Headers.Contains("Access-Control-Allow-Origin").Should().BeTrue();
}
[Fact]
public async Task DisallowedOrigin_ReturnsNoCorsHeaders()
{
var client = _factory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/api/data");
request.Headers.Add("Origin", "https://evil.example.com");
var response = await client.SendAsync(request);
response.Headers.Contains("Access-Control-Allow-Origin").Should().BeFalse();
}
}生产环境 CORS 最佳实践
安全配置模板
// 生产环境推荐的 CORS 配置
builder.Services.AddCors(options =>
{
// 默认策略 — 拒绝所有跨域
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(
builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
?? Array.Empty<string>())
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithExposedHeaders("X-Total-Count", "X-Request-Id", "X-Page-Token")
.SetPreflightMaxAge(TimeSpan.FromMinutes(5));
});
// 开发策略 — 仅在开发环境使用
if (builder.Environment.IsDevelopment())
{
options.AddPolicy("DevPolicy", policy =>
{
policy.SetIsOriginAllowed(origin =>
{
var uri = new Uri(origin);
return uri.Host == "localhost" || uri.Host == "127.0.0.1";
})
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
}
});
// 生产环境中间件顺序
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(); // CORS 在认证之前
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();优点
缺点
总结
CORS 深入要点:浏览器同源策略要求协议+域名+端口完全一致。简单请求(GET/POST 表单)直接发送,复杂请求(PUT/DELETE/JSON Content-Type/自定义头)先发 OPTIONS 预检。AllowAnyOrigin 与 AllowCredentials 不能同时使用,必须用 WithOrigins 或 SetIsOriginAllowed 替代。中间件顺序:UseCors 必须在 UseRouting 之后。预检结果按 Origin+Method+Headers 三元组缓存,生产环境建议设置合理的 PreflightMaxAge。CORS 只控制"是否允许跨域",不能替代 CSRF Token 和 SameSite Cookie 防护。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 安全类主题的关键不只在认证成功,而在于权限边界、证书信任链和审计链路是否完整。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确令牌生命周期、刷新策略、作用域、Claims 和失败返回模型。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 只验证登录成功,不验证权限收敛和令牌失效场景。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续深入零信任、细粒度授权、证书自动化和密钥轮换。
适用场景
- 当你准备把《CORS 跨域深入》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《CORS 跨域深入》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《CORS 跨域深入》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《CORS 跨域深入》最大的收益和代价分别是什么?
