برنامههای امروزی، ممکن است به چندین Web API مستقل، تبدیل شده و سپس برنامههایی (Front-ends) جدای از آنها برای کار با آنها ایجاد شوند. بنابراین این وظیفهی برنامههای Web API است که مطمئن شوند کلاینتها قادر به تعامل با آنها هستند. CORS استانداردی است که یک چنین امکانی را مهیا میکند.
CORS چیست؟
CORS و یا cross origin resource sharing، یک مکانیزم امنیتی است که در تمام مرورگرهای جدید جهت جلوگیری از ارسال اطلاعات یک وب سایت، به وب سایتی دیگر، درنظر گرفته شدهاست. درخواست دسترسی به یک منبع (Resource) مانند تصویر، داده و غیره، خارج از سرآغاز آن، یک درخواست cross-origin نامیده میشود. برای مدیریت یک چنین درخواستهایی، استانداردی به نام CORS طراحی شدهاست. میتوان به آن مانند نگهبانی یک ساختمان نگاه کرد که تا مجوز خاصی به آنها ارائه نشود، امکان دسترسی به منابع ساختمان را صادر نخواهند کرد.
Origin چیست؟
سرآغاز یک درخواست از سه قسمت تشکیل میشود:
- Protocol/Scheme مانند HTTP/HTTPS
- Host مانند نام دومین
- شماره پورت مانند 443 برای پروتکل HTTPS یا پورت 80 برای HTTP که عموما هر دو مورد به علت پیشفرض بودن، ذکر نمیشوند
بنابراین URLای مانند https://www.dntips.ir یک Origin را مشخص میکند. در اینجا به تمام منابعی که از این سرآغاز شروع میشوند و سه قسمت یاد شدهی آنها یکی است، same-origin گفته میشود.
در این حالت اگر منابعی به URLهایی مانند https://www.dntips.ir (پروتکل متفاوت) و یا https://github.com (با host متفاوت) اشاره کنند، به آنها Different-Origin گفته خواهد شد.
سیاست امنیتی Same-Origin چیست؟
سیاست امنیتی Same-Origin که به صورت توکار در تمام مرورگرهای امروزی تعبیه شدهاست، مانع تعامل سرآغازهایی متفاوت، جهت جلوگیری از حملات امنیتی مانند Cross-Site Request Forgery یا CSRF میشود. این سیاست امنیتی بسیار محدود کنندهاست و برای مثال مانع این میشود که بتوان توسط کدهای JavaScript ای، درخواستی را به سرآغاز دیگری ارسال کرد؛ حتی اگر بدانیم درخواست رسیده از منبعی مورد اطمینان صادر شدهاست. برای مثال اگر سایت جاری یک درخواست Ajax ای را به https://anotherwebsite.com/api/users ارسال کند، چون قسمت host مربوط به origin آنها یکی نیست، این عملیات توسط مرورگر غیرمجاز شمرده شده و مسدود میشود.
چگونه میتوان از تنظیمات CORS، برای رفع محدودیتهای سیاستهای دسترسی Same-Origin استفاده کرد؟
استاندارد CORS تعدادی header ویژه را جهت تبادل اطلاعات بین سرور و مرورگر مشخص کردهاست تا توسط آنها بتوان منابع را بین سرآغازهای متفاوتی به اشتراک گذاشت. در این حالت ابتدا کلاینت درخواستی را به سمت سرور ارسال میکند. سپس سرور پاسخی را به همراه هدرهای ویژهای که مشخص میکنند به چه منابعی و چگونه میتوان دسترسی یافت، به سمت کلاینت ارسال خواهد کرد. اکثر این هدرها با Access-Control-Allow شروع میشوند:
• Access-Control-Allow-Origin
در آن لیست سرآغازهایی را که سرور مایل است منابع خود را با آنها به اشتراک بگذارد، مشخص میشوند. در حالت توسعهی برنامه میتوان از مقدار * نیز برای آن استفاده کرد تا هر سرآغازی بتواند به منابع سرور دسترسی پیدا کند. اما از استفادهی این مقدار سراسری و کلی، در حالت انتشار برنامه خودداری کنید.
• Access-Control-Allow-Headers
• Access-Control-Allow-Methods
• Access-Control-Allow-Credentials
درخواستهای ویژهی Pre-Flight
در بسیاری از موارد، مرورگر زمانیکه تشخیص میدهد درخواست صادر شدهی از طرف برنامه، قرار است به یک Origin دیگر ارسال شود، خودش یک درخواست ویژه را به نام Pre-Flight و از نوع OPTIONS به سمت سرور ارسال میکند. علت آن این است که سرورهای قدیمی، مفهوم CORS را درک نمیکنند و در برابر درخواستهای ویژهای مانند Delete که از سرور جاری صادر نشده باشند، مقاومت میکنند (صرفا درخواستهای Same-Origin را پردازش میکنند). به همین جهت است که اگر به برگهی network ابزارهای توسعه دهندگان مرورگر خود دقت کنید، درخواستهای cross-origin به نظر دوبار ارسال شدهاند. بار اول درخواستی که method آن، options است و در خواست دوم که درخواست اصلی است و برای مثال method آن put است. این درخواست اول، Pre-Flight Request نام دارد. هدف آن این است که بررسی کند آیا سرور مقصد، استاندارد CORS را متوجه میشود یا خیر و آیا اینقدر قدیمی است که صرفا درخواستهای Same-Origin را پردازش میکند و مابقی را مسدود خواهد کرد. اگر سرور درخواست Pre-Flight را درک نکند، مرورگر درخواست اصلی را صادر نخواهد کرد.
البته درخواستهای pre-flight بیشتر در حالتهای زیر توسط مرورگر صادر میشوند:
- اگر متد اجرایی مدنظر، متدی بجز GET/POST/HEAD باشد.
- اگر content type درخواستهای از نوع POST، چیزی بجز application/x-www-form-urlencoded, multipart/form-data و یا text/plain باشند.
- اگر درخواست، به همراه یک هدر سفارشی باشد نیز سبب صدور یک pre-flight request خواهد شد.
بنابراین در برنامههای SPA خود که اطلاعات را با فرمت JSON به سمت یک Web API ارسال میکنند (و Origin آنها یکی نیست)، حتما شاهد درخواستهای Pre-Flight نیز خواهید بود.
تنظیمات CORS در ASP.NET Core
اکنون که با مفهوم CORS آشنا شدیم، در ادامه به نحوهی انجام تنظیمات آن در برنامههای ASP.NET Core خواهیم پرداخت.
تنظیمات ابتدایی CORS در فایل Startup.cs و متد ConfigureServices آن انجام میشوند:
public void ConfigureServices(IServiceCollection services)
{
// Add service and create Policy with options
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials() );
});
// Make sure MVC is added AFTER CORS
services.AddMvc();
}
ابتدا توسط متد AddCors، سرویسهای مرتبط، به سیستم تزریق وابستگیهای ASP.NET Core اضافه خواهند شد و سپس در قسمت تنظیمات آن میتوان همان هدرهای Access-Control-Allow را که پیشتر در مورد آنها بحث کردیم، تعریف و مقدار دهی کرد. برای مثال تنظیم AllowAnyOrigin، همان استفادهی از مقدار * در هدر Access-Control-Allow-Origin تولیدی است. باید دقت داشت که تنظیمات این سرویسها باید پیش از افزودن سرویسهای MVC انجام شوند.
در اینجا CorsPolicy که به عنوان پارامتر مشخص شدهاست، نام این سیاست دسترسی سفارشی است و در قسمتهای مختلف برنامه میتوان ارجاعاتی را به این نام تعریف کرد.
و برای تعریف سرآغازی خاص و یا متدی مشخص، میتوان به صورت زیر عمل کرد (ذکر صریح WithOrigins برای حالت توزیع برنامه مناسب است و از دیدگاه امنیتی، استفادهی از AllowAnyOrigin
کار صحیحی نیست ):
services.AddCors(options =>
{
options.AddPolicy("AllowOrigin",
builder => builder.WithOrigins("http://localhost:55294")
.WithMethods("GET", "POST", "HEAD"));
});
پس از تنظیم سیاست دسترسی مدنظر، اکنون نوبت به اعمال آن است. اینکار در متد Configure فایل Startup.cs انجام میشود:
public void Configure(IApplicationBuilder app)
{
// ...
app.UseCors("CorsPolicy");
// ...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
در اینجا متد UseCors که میانافزار CORS را فعال میکند، باید پیشاز میانافزار Mvc تعریف شود تا بتواند هدرها را اعمال کند. همچنین نامی که در اینجا به عنوان پارامتر آن استفاده شده، دقیقا به همان نامی که برای تنظیمات سیاست دسترسی درنظر گرفته شد، اشاره میکند.
متد app.UseCors، یک متد سراسری است و کل سیستم را داخل این سیاست دسترسی CORS قرار میدهد. اگر میخواهید صرفا به کنترلر خاصی این تنظیمات اعمال شوند، میتوان از فیلتر EnableCors با ذکر نام سیاست دسترسی تعریف شده، استفاده کرد (در این حالت ذکر services.AddCors ضروری است و از ذکر app.UseCors صرفنظر میشود):
[EnableCors("CorsPolicy")]
public class MyApiController : Controller
و یا اگر میخواهید از app.UseCors سراسری استفاده کنید، اما آنرا برای کنترلر و یا حتی اکشن متد خاصی غیرفعال نمائید، میتوان از فیلتر [DisableCors] استفاده کرد.
چند نکته:
- به نام رشتهای سیاست دسترسی تعریف شده، دقت داشته باشید. اگر این نام را درست ذکر نکنید، هدری تولید نخواهد شد.
- میانافزار CORS، برای درخواستهای same-origin نیز هدری را تولید نمیکند.
امکان تعریف سیاستهای دسترسی بدون نام نیز وجود دارد
در هر دو روش فوق که یکی بر اساس app.UseCors سراسری و دیگری بر اساس فیلتر EnableCors اختصاصی کار میکنند، ذکر نام سیاست دسترسی options.AddPolicy ضروری است. اگر علاقهای به ذکر این نام ندارید، میتوان به طور کامل از تنظیم services.AddCors صرفنظر کرد (قسمت پارامتر و تنظیمات آنرا ذکر نکرد) و متد app.UseCors را به صورت زیر تنظیم نمود که قابلیت داشتن تنظیمات خاص خود را نیز دارا است:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(policyBuilder =>
{
string[] origins = new string[] { "http://localhost:2000", "http://localhost:2001" };
policyBuilder
.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins(origins);
});
app.UseMvc();
}
روش دیگر انجام اینکار، تعریف یک DefaultPolicy است:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
options.AddPolicy("MyCORSPolicy",
builder =>
{
builder.WithOrigins("http://localhost:49373")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvc().AddNewtonsoftJson();
}
در اینجا در متد AddCors میتوان چندین AddPolicy نامدار را داشت، به همراه یک AddDefaultPolicy بدون نام. استفادهی از سیاست نامدار تعریف شده، یا توسط متد سراسری app.UseCors("MyCORSPolicy") انجام میشود و یا توسط فیلتر [EnableCors("MyCORSPolicy")]. اما اگر فیلتر [EnableCors] را بدون ذکر نامی قید کنیم، از همان تنظیمات AddDefaultPolicy استفاده خواهد کرد.
خطاهای متداول حین کار با CORS
خطای کار با SignalR و ارسال اطلاعات اعتبارسنجی کاربر Access to XMLHttpRequest at 'http://localhost:22135/chat/negotiate?jwt=<removed the jwt key>'
from origin 'http://localhost:53150' has been blocked by CORS policy: Response to preflight request doesn't pass access control check:
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'.
The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
برای رفع این مشکل کافی است متد AllowCredentials نیز قید شود:
services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins",
builder =>
{
builder
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
تمام اعتبارسنجیها و اطلاعات کوکیها در سمت سرور قابل دسترسی نیستند
CORS به صورت پیشفرض اطلاعات کوکیها را به دومینی دیگر ارسال نمیکند. در این حالت اگر حتما نیاز به انجام اینکار است، باید در درخواست Ajax ای ارسالی، خاصیت withCredentials به true تنظیم شود. همچنین سمت سرور نیز هدر Access-Control-Allow-Credentials را تنظیم کند (همان متد AllowCredentials فوق). در اینجا Credentials به کوکیها، هدرهای اعتبارسنجی (مانند هدرهای JWT) کاربران و یا مجوزهای TLS کلاینتها اشاره میکند.
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;
//In jQuery:
$.ajax({
type: 'get',
url: 'http://www.example.com/home',
xhrFields: {
withCredentials: true
}
//ES6 fetch API:
fetch(
'http://remote-url.com/users',
{
'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
mode: 'cors',
credentials: 'include',
body: JSON.stringify(body)
}
)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((error) => console.error(error));
CORS حساس به حروف کوچک و بزرگ است
روش دیگر تنظیم فیلتر EnableCors را در اینجا مشاهده میکنید:
[EnableCors(origins: "http://MyWebsite.com", headers: "*", methods: "*")]
//is not the same as this:
[EnableCors(origins:"http://mywebsite.com", headers: "*", methods: "*")]
اگر بجای * دقیقا origins را مشخص کردید، بخاطر داشته باشید که این تنظیم به کوچکی و بزرگی حروف حساس است و در صورت اشتباهی (مانند ذکر MyWebSite بجای mywebsite متداول با حروف کوچک)، درخواستهای رسیده مسدود خواهند شد. همچنین همانطور که ذکر شد، بین HTTP و HTTPS نیز تفاوت وجود دارد و origin اینها یکی درنظر گرفته نمیشود و حتی اگر تمام حروف را هم کوچک وارد کرده باشید، باز هم تنظیمات جداگانهای را نیاز خواهند داشت.
در حالت بروز خطای مدیریت نشدهای در سمت سرور، ASP.NET Core هدرهای CORS را ارسال نمیکند اطلاعات بیشتر