跨域资源共享(CORS)
大约 12 分钟约 3656 字
跨域资源共享(CORS)
简介
浏览器的同源策略(Same-Origin Policy)限制了不同源之间的 HTTP 请求。同源的定义是协议(Protocol)、域名(Domain)和端口(Port)三者完全相同。CORS(Cross-Origin Resource Sharing)是一种 W3C 标准机制,允许服务器声明哪些外部源可以访问其资源。ASP.NET Core 提供内置的 CORS 中间件来处理跨域请求。
同源策略与 CORS 的关系
同源判断:协议 + 域名 + 端口 完全一致
http://example.com/api/data <- 源
https://example.com/api/data <- 不同源(协议不同)
http://api.example.com/data <- 不同源(域名不同)
http://example.com:8080/data <- 不同源(端口不同)
http://example.com/api/data <- 同源
CORS 工作流程:
1. 浏览器发起跨域请求
2. 浏览器自动添加 Origin 请求头
3. 服务器检查 Origin,决定是否允许
4. 服务器返回 CORS 响应头
5. 浏览器根据响应头决定是否将响应交给 JavaScript简单请求 vs 预检请求
简单请求(Simple Request)— 直接发送:
- 方法:GET、HEAD、POST
- Content-Type:text/plain、multipart/form-data、application/x-www-form-urlencoded
- 无自定义头
- 直接发送请求,浏览器在响应中检查 CORS 头
预检请求(Preflight Request)— 先发 OPTIONS:
- 方法:PUT、DELETE、PATCH
- Content-Type:application/json
- 有自定义头(Authorization、X-Custom-Header 等)
- 先发送 OPTIONS 请求询问服务器是否允许
- 服务器返回允许的方法、头、源等信息
- 浏览器缓存预检结果(Access-Control-Max-Age)
- 然后才发送实际请求特点
基本配置
全局 CORS
var builder = WebApplication.CreateBuilder(args);
// ============================================
// 添加 CORS 服务
// ============================================
builder.Services.AddCors(options =>
{
// 默认策略 — 所有未指定策略的端点都使用此策略
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(
"https://www.example.com",
"https://admin.example.com"
)
.WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.WithHeaders("Content-Type", "Authorization", "X-Request-Id")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10)); // 预检缓存 10 分钟
});
});
var app = builder.Build();
// 必须在 UseRouting 之后、UseAuthorization 之前
app.UseCors();
app.MapControllers();
app.Run();从配置文件读取 Origin
// appsettings.json
/*
{
"Cors": {
"AllowedOrigins": [
"https://www.example.com",
"https://admin.example.com",
"https://app.example.com"
],
"AllowedMethods": ["GET", "POST", "PUT", "DELETE"],
"AllowedHeaders": ["Content-Type", "Authorization"],
"AllowCredentials": true,
"PreflightMaxAgeMinutes": 10
}
}
*/
// 读取配置并构建 CORS 策略
builder.Services.AddCors(options =>
{
var corsSection = builder.Configuration.GetSection("Cors");
var allowedOrigins = corsSection.GetSection("AllowedOrigins").Get<string[]>()
?? Array.Empty<string>();
var allowedMethods = corsSection.GetSection("AllowedMethods").Get<string[]>()
?? new[] { "GET", "POST" };
var allowedHeaders = corsSection.GetSection("AllowedHeaders").Get<string[]>()
?? new[] { "Content-Type" };
var allowCredentials = corsSection.GetValue<bool>("AllowCredentials");
var preflightMaxAge = corsSection.GetValue<int>("PreflightMaxAgeMinutes");
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(allowedOrigins)
.WithMethods(allowedMethods)
.WithHeaders(allowedHeaders);
if (allowCredentials)
policy.AllowCredentials();
if (preflightMaxAge > 0)
policy.SetPreflightMaxAge(TimeSpan.FromMinutes(preflightMaxAge));
});
});命名策略
builder.Services.AddCors(options =>
{
// ============================================
// 开发环境策略(宽松 — 方便本地调试)
// ============================================
options.AddPolicy("DevPolicy", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
// ============================================
// 生产环境策略(严格 — 明确指定允许的源)
// ============================================
options.AddPolicy("ProdPolicy", policy =>
{
policy.WithOrigins(
"https://www.example.com",
"https://admin.example.com",
"https://app.example.com"
)
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization", "X-Request-Id")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
// ============================================
// 第三方合作策略(只允许特定方法和头)
// ============================================
options.AddPolicy("PartnerPolicy", policy =>
{
policy.WithOrigins("https://partner.com")
.WithMethods("GET")
.WithHeaders("X-Partner-Key", "X-Partner-Id")
.SetPreflightMaxAge(TimeSpan.FromHours(1));
});
// ============================================
// WebSocket 策略
// ============================================
options.AddPolicy("WebSocketPolicy", policy =>
{
policy.WithOrigins("wss://example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
// ============================================
// API 公开策略(不需要凭证)
// ============================================
options.AddPolicy("PublicApiPolicy", policy =>
{
policy.AllowAnyOrigin()
.WithMethods("GET")
.WithHeaders("Content-Type")
.SetPreflightMaxAge(TimeSpan.FromHours(24));
});
});
var app = builder.Build();
// 根据环境选择策略
if (app.Environment.IsDevelopment())
{
app.UseCors("DevPolicy");
}
else
{
app.UseCors("ProdPolicy");
}控制器/Action 级别
EnableCors 特性
[ApiController]
[Route("api/[controller]")]
[EnableCors("ProdPolicy")] // 控制器级别 — 应用于所有 Action
public class ProductsController : ControllerBase
{
// 使用控制器级别的 ProdPolicy
[HttpGet]
public IActionResult GetAll() => Ok("产品列表");
// 覆盖控制器级别策略 — 使用 DevPolicy
[HttpGet("public")]
[EnableCors("DevPolicy")]
public IActionResult GetPublic() => Ok("公开数据");
// 完全禁用 CORS — 即使有全局/控制器级别策略
[HttpGet("internal")]
[DisableCors]
public IActionResult GetInternal() => Ok("内部接口");
// 仅允许特定源
[HttpPost("upload")]
[EnableCors("PartnerPolicy")]
public IActionResult Upload() => Ok("上传成功");
}自定义 CORS 特性
/// <summary>
/// 自定义 CORS 特性 — 根据条件动态选择策略
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ConditionalCorsAttribute : Attribute, ICorsPolicyProvider
{
private readonly CorsPolicy _policy;
public ConditionalCorsAttribute()
{
_policy = new CorsPolicy
{
AllowAnyHeader = true,
AllowAnyMethod = true
};
}
public Task<CorsPolicy?> GetPolicyAsync(HttpContext context, string policyName)
{
// 根据请求头动态决定是否允许
var origin = context.Request.Headers.Origin.ToString();
if (origin.EndsWith(".example.com") || origin.EndsWith(".partner.com"))
{
_policy.Origins.Add(origin);
_policy.AllowCredentials = true;
return Task.FromResult<CorsPolicy?>(_policy);
}
// 不允许
return Task.FromResult<CorsPolicy?>(null);
}
}
// 使用自定义特性
[ConditionalCors]
[HttpGet("conditional")]
public IActionResult ConditionalEndpoint() => Ok("动态 CORS");端点路由 CORS
Minimal API
var app = builder.Build();
// 全局 CORS 中间件
app.UseCors();
// 使用命名策略
app.MapGet("/api/public", () => "公开接口")
.RequireCors("DevPolicy");
// 使用内联策略
app.MapGet("/api/data", () => "数据接口")
.RequireCors(policy => policy
.WithOrigins("https://www.example.com")
.WithMethods("GET", "POST")
.AllowCredentials());
// 禁用 CORS
app.MapGet("/api/internal", () => "内部接口")
.DisableCors();
// 允许所有 CORS(仅用于开发/测试)
if (app.Environment.IsDevelopment())
{
app.MapGet("/api/open", () => "开放接口")
.RequireCors(policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
}
// WebSocket 端点
app.MapHub<ChatHub>("/chat-hub")
.RequireCors("WebSocketPolicy");常见问题
凭证和通配符
// ============================================
// 错误:AllowAnyOrigin 和 AllowCredentials 不能同时使用
// ============================================
// policy.AllowAnyOrigin().AllowCredentials();
// 抛出异常:NotSupportedException: The origin '*' is not allowed when
// credentials mode is 'include'. Use specific origins instead.
// ============================================
// 正确方式 1:明确指定源
// ============================================
policy.WithOrigins("https://www.example.com", "https://admin.example.com")
.AllowCredentials();
// ============================================
// 正确方式 2:使用动态源检查(子域名通配)
// ============================================
policy.SetIsOriginAllowed(origin =>
{
var uri = new Uri(origin);
// 允许所有 *.example.com 子域名
return uri.Host.EndsWith(".example.com", StringComparison.OrdinalIgnoreCase);
})
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
// ============================================
// 正确方式 3:启用子域名通配
// ============================================
policy.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins("https://*.example.com", "https://*.partner.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
// ============================================
// 正确方式 4:不需要凭证时使用 AllowAnyOrigin
// ============================================
// 对于不需要 Cookie/Authorization 的公开 API
policy.AllowAnyOrigin()
.WithMethods("GET")
.WithHeaders("Content-Type");动态 CORS
// ============================================
// 方式 A:从数据库读取允许的 Origin
// ============================================
builder.Services.AddCors(options =>
{
options.AddPolicy("DynamicPolicy", policy =>
{
policy.SetIsOriginAllowed(async origin =>
{
// 从配置/数据库/缓存检查
var serviceProvider = builder.Services.BuildServiceProvider();
var corsService = serviceProvider.GetRequiredService<ICorsOriginService>();
return await corsService.IsOriginAllowedAsync(origin);
})
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
// ============================================
// 方式 B:使用自定义中间件处理复杂 CORS 逻辑
// ============================================
app.Use(async (context, next) =>
{
var origin = context.Request.Headers.Origin.ToString();
var isCorsRequest = !string.IsNullOrEmpty(origin);
if (isCorsRequest)
{
var allowed = await IsOriginAllowedAsync(origin);
if (allowed)
{
context.Response.Headers.AccessControlAllowOrigin = origin;
context.Response.Headers.AccessControlAllowCredentials = "true";
context.Response.Headers.AccessControlAllowMethods = "GET,POST,PUT,DELETE";
context.Response.Headers.AccessControlAllowHeaders = "Content-Type,Authorization,X-Request-Id";
context.Response.Headers.AccessControlMaxAge = "600";
context.Response.Headers.AccessControlExposeHeaders = "X-Request-Id,X-Total-Count";
}
// 处理预检请求 — 直接返回,不进入后续管道
if (context.Request.Method == "OPTIONS")
{
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}
}
await next();
});
// ============================================
// 动态 Origin 检查服务
// ============================================
public interface ICorsOriginService
{
Task<bool> IsOriginAllowedAsync(string origin);
}
public class CorsOriginService : ICorsOriginService
{
private readonly IMemoryCache _cache;
private readonly IConfiguration _configuration;
public CorsOriginService(IMemoryCache cache, IConfiguration configuration)
{
_cache = cache;
_configuration = configuration;
}
public async Task<bool> IsOriginAllowedAsync(string origin)
{
// 使用缓存避免每次请求都查询
var cacheKey = $"cors:origin:{origin}";
if (_cache.TryGetValue(cacheKey, out bool allowed))
return allowed;
// 从配置中检查
var allowedOrigins = _configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
?? Array.Empty<string>();
allowed = allowedOrigins.Contains(origin, StringComparer.OrdinalIgnoreCase);
// 也可以从数据库检查
// var dbAllowed = await CheckDatabaseAsync(origin);
// allowed = allowed || dbAllowed;
// 缓存结果 5 分钟
_cache.Set(cacheKey, allowed, TimeSpan.FromMinutes(5));
return allowed;
}
}CORS 与认证的配合
// ============================================
// CORS + JWT Bearer 认证的完整配置
// ============================================
builder.Services.AddCors(options =>
{
options.AddPolicy("JwtPolicy", policy =>
{
policy.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
var app = builder.Build();
// 中间件顺序至关重要
app.UseExceptionHandler("/error");
app.UseCors("JwtPolicy"); // CORS 必须在认证之前
app.UseAuthentication(); // 认证
app.UseAuthorization(); // 授权
app.MapControllers();
// 前端请求示例:
// fetch('https://api.example.com/api/data', {
// method: 'GET',
// credentials: 'include', // 跨域发送 Cookie
// headers: {
// 'Authorization': `Bearer ${token}`,
// 'Content-Type': 'application/json'
// }
// });CORS 与 SignalR
// ============================================
// CORS + SignalR WebSocket
// ============================================
builder.Services.AddCors(options =>
{
options.AddPolicy("SignalRPoly", policy =>
{
policy.WithOrigins("https://app.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
builder.Services.AddSignalR();
var app = builder.Build();
app.UseCors("SignalRPoly");
app.MapHub<ChatHub>("/chat-hub");
// 前端连接示例:
// const connection = new HubConnectionBuilder()
// .withUrl("https://api.example.com/chat-hub", {
// accessTokenFactory: () => token
// })
// .build();CORS 中间件顺序
正确的中间件顺序:
app.UseExceptionHandler("/error"); // 1. 异常处理
app.UseHsts(); // 2. HSTS
app.UseHttpsRedirection(); // 3. HTTPS 重定向
app.UseCors("ProdPolicy"); // 4. CORS(必须在路由和认证之前)
app.UseAuthentication(); // 5. 认证
app.UseAuthorization(); // 6. 授权
app.UseOutputCache(); // 7. 输出缓存
app.MapControllers(); // 8. 路由映射
关键规则:
- UseCors 必须在 UseRouting/MapControllers 之前
- UseCors 必须在 UseAuthentication 之前(否则预检请求不会到达 CORS 中间件)
- UseCors 必须在 UseAuthorization 之前中间件顺序错误导致的问题
// ❌ 错误顺序 1:CORS 在认证之后
app.UseAuthentication();
app.UseCors(); // 预检请求可能被认证中间件拦截
app.UseAuthorization();
// ❌ 错误顺序 2:CORS 在路由之后
app.MapControllers();
app.UseCors(); // 路由已经匹配,CORS 头不会添加
// ❌ 错误顺序 3:忘记添加 UseCors
// app.UseCors(); // 没有中间件处理 CORS
app.MapControllers(); // 所有跨域请求都会失败CORS 日志与调试
CORS 调试中间件
/// <summary>
/// CORS 调试中间件 — 记录 CORS 相关信息
/// </summary>
public class CorsDebugMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CorsDebugMiddleware> _logger;
public CorsDebugMiddleware(RequestDelegate next, ILogger<CorsDebugMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var origin = context.Request.Headers.Origin.ToString();
var method = context.Request.Method;
// 记录跨域请求信息
if (!string.IsNullOrEmpty(origin))
{
_logger.LogDebug(
"CORS 请求: Method={Method}, Origin={Origin}, Path={Path}",
method, origin, context.Request.Path);
}
await _next(context);
// 记录 CORS 响应头
if (!string.IsNullOrEmpty(origin))
{
var allowOrigin = context.Response.Headers.AccessControlAllowOrigin.ToString();
_logger.LogDebug(
"CORS 响应: Origin={Origin}, AllowOrigin={AllowOrigin}, Status={Status}",
origin, allowOrigin, context.Response.StatusCode);
}
}
}
// 注册
app.UseMiddleware<CorsDebugMiddleware>();常见 CORS 错误及解决
错误 1: Access to XMLHttpRequest has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
原因: 服务器没有返回 Access-Control-Allow-Origin 头
解决: 确认 UseCors() 已注册且在正确的位置
错误 2: Access to XMLHttpRequest has been blocked by CORS policy:
Response to preflight request doesn't pass access control check
原因: 预检请求(OPTIONS)被服务器拒绝
解决: 确认 WithMethods 包含实际请求的方法
错误 3: The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'
原因: AllowAnyOrigin() 和 AllowCredentials() 同时使用
解决: 使用 WithOrigins() 明确指定源
错误 4: CORS policy: No 'Access-Control-Allow-Headers' header is present
原因: 请求头包含自定义头,但服务器未声明允许
解决: 在 WithHeaders() 中添加自定义头名称
错误 5: CORS policy: Response to preflight request doesn't pass access control check:
It does not have HTTP ok status
原因: 预检请求返回非 200 状态码
解决: 检查中间件顺序,确保 CORS 在认证/授权之前优点
缺点
总结
CORS 配置核心:services.AddCors 注册策略、app.UseCors 启用中间件。命名策略适合不同环境(DevPolicy/ProdPolicy)。控制器级别用 [EnableCors] 和 [DisableCors]。AllowAnyOrigin 和 AllowCredentials 不能同时使用。中间件顺序:UseCors 必须在 UseRouting 之后、UseAuthentication 和 UseAuthorization 之前。生产环境务必指定明确的 Origin 列表,避免使用 AllowAnyOrigin。预检缓存(SetPreflightMaxAge)可以减少 OPTIONS 请求次数。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 安全类主题的关键不只在认证成功,而在于权限边界、证书信任链和审计链路是否完整。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确令牌生命周期、刷新策略、作用域、Claims 和失败返回模型。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 只验证登录成功,不验证权限收敛和令牌失效场景。
- 在生产环境使用
AllowAnyOrigin(),造成安全漏洞。 - 忘记
SetPreflightMaxAge,导致每次跨域请求都发送 OPTIONS 预检。 - CORS 中间件放在认证之后,导致预检请求被拒绝。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续深入零信任、细粒度授权、证书自动化和密钥轮换。
适用场景
- 当你准备把《跨域资源共享(CORS)》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
- CORS 配置应从配置文件读取,不同环境使用不同策略。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
- 检查浏览器控制台的 CORS 错误信息,确定具体原因。
- 检查中间件注册顺序是否正确。
- 检查预检请求(OPTIONS)是否返回 200 状态码。
复盘问题
- 如果把《跨域资源共享(CORS)》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《跨域资源共享(CORS)》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《跨域资源共享(CORS)》最大的收益和代价分别是什么?
