一. 整體說明
1. 說在前面的話
早在前面的章節中,就詳細介紹了.Net FrameWork版本下MVC和WebApi的跨域解決方案,詳見:https://www.cnblogs.com/yaopengfei/p/10340434.html ,由於在Core版本中,MVC和WebApi已經合並,所以在該章節中介紹Asp.Net Core中的跨域解決方案。
2. 背景
瀏覽器出於安全性考慮,禁止在網頁上發出請求到不同的域的web頁面進行請求,此限制稱為同域策略。 同域策略可阻止惡意站點讀取另一個站點中的敏感數據, 但有時候,你可能想要允許其他站點對您的應用程序進行跨域請求,所以有兩套解決方案,分別是:CORS(跨域資源共享) 、 jsonp。
PS:有時候為了方便測試,可以直接配置一下Chrome瀏覽器,使其支持跨域請求。
3. 跨域資源共享(CORS)
它是一項 W3C 標准,主流瀏覽器都支持,原理是讓服務器放寬同域策略,它很靈活,可以顯式的控制允許哪些地址、哪種請求方式來進行跨域訪問。
4. JSONP
簡單的來說,它就是在json數據外面包了一層,它有一個很大的局限性,就是僅支持Get請求,如下JSON和JSONP的區別:
(1) json格式:
{
"id":123,
"name":"ypf"
}
(2) jsonp格式:在json外面包了一層
callback({
"id":123,
"name":"ypf"
})
其中callback取決於url傳到后台是什么,它就叫什么。
5. 瀏覽器配置
有時候為了測試方便,我們可以直接配置一下Chrome瀏覽器,使其支持跨域。
二. 跨域資源共享
1. 說明
使用的程序集是【Microsoft.AspNetCore.Cors】,在Core Mvc中,默認已經包含了,無須再引入;他有兩種作用形式,可以在Configure管道中UseMvc前進行全局攔截,也可以在以特性的形式作用於控制器 或 Action。
2. Cors策略詳解
A. 設置允許的來源:WithOrigins 或 AllowAnyOrigin 如:WithOrigins("http://localhost:29492","http://localhost:5000")
B. 設置允許的方法:WithMethods 或 AllowAnyMethod 如:WithMethods("GET","PUT")
C. 設置允許的請求標頭:WithHeaders 或 AllowAnyHeader
D. 設置公開響應標頭:WithExposedHeaders
E. 允許請求中的憑據:AllowCredentials
PS:AddCors注冊服務,AddDefaultPolicy注冊默認策略、AddPolicy注冊命名策略
3. 使用流程
流程一: 在ConfigureService先注冊策略(默認策略或命名策略),然后可以在Configure管道中進行全局攔截 或者 以特性的形式作用於Controller或action。
流程二: 直接在Configure中配置相應的策略進行全局攔截,不需要在ConfigureService中注冊任何代碼。
4. 默認策略測試
(1). 前提
在Configure中通過AddDefultPolicy注冊默認策略,策略內容:允許所有來源、所有方法、所有請求標頭、允許請求憑據。代碼如下:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.Configure<CookiePolicyOptions>(options => 4 { 5 // This lambda determines whether user consent for non-essential cookies is needed for a given request. 6 options.CheckConsentNeeded = context => true; 7 options.MinimumSameSitePolicy = SameSiteMode.None; 8 }); 9 10 //注冊跨域請求服務 11 services.AddCors(options => 12 { 13 //注冊默認策略 14 options.AddDefaultPolicy(builder => 15 { 16 builder.AllowAnyOrigin() 17 .AllowAnyMethod() 18 .AllowAnyHeader() 19 .AllowCredentials(); 20 }); 21 }); 22 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 23 }
(2). 測試1-全局攔截
在ConfigureService中(app.UseMvc前)通過app.UseCors();注冊默認策略,然后在前端訪問Test1方法,正常訪問。
PS:Test1方法要注釋掉cors特性后再測試。
1 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 2 { 3 if (env.IsDevelopment()) 4 { 5 app.UseDeveloperExceptionPage(); 6 } 7 else 8 { 9 app.UseExceptionHandler("/Home/Error"); 10 } 11 12 app.UseStaticFiles(); 13 app.UseCookiePolicy(); 14 15 //全局配置跨域(一定要配置在 app.UseMvc前) 16 //1. 默認策略 17 app.UseCors(); 18 19 app.UseMvc(routes => 20 { 21 routes.MapRoute( 22 name: "default", 23 template: "{controller=Home}/{action=Index}/{id?}"); 24 }); 25 }
1 /// <summary> 2 /// 默認策略 3 /// </summary> 4 /// <returns></returns> 5 [HttpPost] 6 public string Test1() 7 { 8 return "ypf001"; 9 }
1 //1. 測試cors默認策略 2 $('#j_btn1').click(function () { 3 $.post("http://localhost:29492/Home/Test1", {}, function (data) { 4 alert(data); 5 }); 6 });
(3). 測試2-特性作用
在Test1方法上增加特性[EnableCors],然后進行測試,正常訪問。
PS:ConfigureService中全局攔截要注釋掉再測試
1 /// <summary> 2 /// 默認策略 3 /// </summary> 4 /// <returns></returns> 5 [EnableCors] 6 [HttpPost] 7 public string Test1() 8 { 9 return "ypf001"; 10 }
5. 命名策略測試
(1). ConfigureService中策略的注冊代碼
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.Configure<CookiePolicyOptions>(options => 4 { 5 // This lambda determines whether user consent for non-essential cookies is needed for a given request. 6 options.CheckConsentNeeded = context => true; 7 options.MinimumSameSitePolicy = SameSiteMode.None; 8 }); 9 10 //注冊跨域請求服務 11 services.AddCors(options => 12 { 13 //注冊一個名字為“AnotherPolicy”新策略 14 options.AddPolicy("AnotherPolicy", builder => 15 { 16 builder.WithOrigins("http://localhost:29553", "http://localhost:5001") //多個地址通過"逗號"隔開 17 .WithMethods("GET","PUT") 18 .WithHeaders(Microsoft.Net.Http.Headers.HeaderNames.ContentType, "Authorization") 19 .AllowCredentials(); 20 21 }); 22 23 }); 24 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 25 }
(2). Configure全局攔截代碼 或者 作用於Action的代碼 (二者選其一即可)
全局攔截:
1 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 2 { 3 if (env.IsDevelopment()) 4 { 5 app.UseDeveloperExceptionPage(); 6 } 7 else 8 { 9 app.UseExceptionHandler("/Home/Error"); 10 } 11 app.UseStaticFiles(); 12 app.UseCookiePolicy(); 13 14 //全局配置跨域(一定要配置在 app.UseMvc前) 15 //2. 命名策略 16 app.UseCors("AnotherPolicy"); 17 18 app.UseMvc(routes => 19 { 20 routes.MapRoute( 21 name: "default", 22 template: "{controller=Home}/{action=Index}/{id?}"); 23 }); 24 }
作用於action
1 /// <summary> 2 /// 命名策略 3 /// </summary> 4 /// <returns></returns> 5 [EnableCors("AnotherPolicy")] 6 [HttpPost] 7 public string Test2() 8 { 9 return "ypf002"; 10 }
(3). 前端測試代碼
1 //2. 測試cors命名策略 2 $('#j_btn2').click(function () { 3 $.ajax({ 4 url: 'http://localhost:29492/Home/Test2', 5 type: "Post", 6 xhrFields: { 7 //發送跨域請求憑據 8 withCredentials: true 9 }, 10 cache: false, 11 data: {}, 12 beforeSend: function (request) { 13 //在請求報文頭中加入Authorization 目的是讓請求為非簡單請求 14 request.setRequestHeader("Authorization", "Bearer 071899A00D4D4C5B1C41A6B0211B9399"); 15 }, 16 success: function (data) { 17 alert(data); 18 } 19 }); 20 });
6. 直接Configure全局配置測試
直接在Configure管道中配置策略,默認允許所有,注釋掉ConfigureService中的所有策略和Test1、Test2方法上的特性, 仍然可以正常訪問,測試通過。
1 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 2 { 3 if (env.IsDevelopment()) 4 { 5 app.UseDeveloperExceptionPage(); 6 } 7 else 8 { 9 app.UseExceptionHandler("/Home/Error"); 10 } 11 12 app.UseStaticFiles(); 13 app.UseCookiePolicy(); 14 15 //全局配置跨域(一定要配置在 app.UseMvc前) 16 //3. 直接配置策略,不需在Configure中注冊任何代碼了 17 app.UseCors(options => 18 { 19 options.AllowAnyOrigin() 20 .AllowAnyMethod() 21 .AllowAnyHeader() 22 .AllowCredentials(); 23 }); 24 25 app.UseMvc(routes => 26 { 27 routes.MapRoute( 28 name: "default", 29 template: "{controller=Home}/{action=Index}/{id?}"); 30 }); 31 }
特別注意:
在core 2.x版本,AllowAnyOrigin()和AllowCredentials()可以共存,在3.x版本,不能共存。
三. JSONP
1. 原始的jsonp模式
在Asp.Net Core中支持,在.Net版本的webapi中是不支持,即在方法中聲明一個接受參數與前端JSONP位置傳遞過來的進行對應,然后將數據進行包裹返回,$"{myCallBack}({xjs})"。以JQuery的Ajax請求為例,可以指定JSONP的名稱,如下面代碼指定為:myCallBack,如果省略,JSONP的名稱則為:callback。
特別注意:凡是JSONP請求,GET請求地址的第一個參數則為JSONP位置配置的名稱,如:http://localhost:29492/Home/GetInfor2?myCallBack=jQuery341001790888637311383_1564124488832&userName=ypf&_=1564124488833
服務器端代碼:
1 /// <summary> 2 /// 原始的JSONP模式,支持 3 /// </summary> 4 /// <param name="callBack"></param> 5 /// <param name="userName"></param> 6 /// <returns></returns> 7 [HttpGet] 8 9 public dynamic GetInfor1(string myCallBack, string userName) 10 { 11 var data = new 12 { 13 id = userName + "001", 14 userName = userName 15 }; 16 string xjs = JsonConvert.SerializeObject(data); 17 return $"{myCallBack}({xjs})"; 18 }
前端代碼:
1 $('#j_jsonp1').click(function () { 2 $.ajax({ 3 url: 'http://localhost:29492/Home/GetInfor1', 4 type: "get", 5 dataType: "jsonp", 6 //需要和服務端回掉方法中的參數名相對應 7 //注釋掉這句話默認傳的名稱叫callback 8 jsonp: "myCallBack", 9 cache: false, 10 data: { userName: "ypf" }, 11 success: function (data) { 12 console.log(data); 13 alert(data.id + "," + data.userName); 14 } 15 }); 16 });
2. 利用過濾器判斷請求來決定是否跨域
(1) 背景
想實現服務器端方法正常寫,該返回什么就返什么,如果是JSONP請求,則返回JSONP格式,如果是普通請求,則返回不同格式。
(2) 原理
如果是jsonp請求的,如同下面這個地址:http://localhost:29492/Home/GetInfor2?myCallBack=jQuery341001790888637311383_1564124488832&userName=ypf&_=1564124488833, 第一個參數為myCallBack,如果有這個參數則證明是jsonp請求,需要拿到它的值;反之如果沒有這個參數,則是普通請求,需要配置跨域策略了,才能拿到值。
在過濾器中判斷,如果是jsonp請求,則將數據進行包裹進行返回,如果是普通請求,則直接返回,然后把過濾器以特性[IsJsonpCallback]形式作用在GetInfor2方法上。
過濾器代碼:
1 public class IsJsonpCallbackAttribute: ActionFilterAttribute 2 { 3 private const string CallbackQueryParameter = "myCallBack"; 4 public override void OnActionExecuted(ActionExecutedContext context) 5 { 6 7 string text = context.HttpContext.Request.QueryString.Value; 8 string[] arrys = text.Split('&'); 9 if (arrys[0].Contains(CallbackQueryParameter)) 10 { 11 var myCallBackValue = arrys[0].Split('=')[1]; 12 var result = $"{myCallBackValue}({((ObjectResult)context.Result).Value})"; 13 context.HttpContext.Response.WriteAsync(result); 14 } 15 16 base.OnActionExecuted(context); 17 } 18 }
服務器端代碼:
1 /// <summary> 2 /// 改造后的Jsonp(請求是Jsonp格式,則返回jsonp格式,反之普通格式) 3 /// </summary> 4 /// <param name="callBack"></param> 5 /// <param name="userName"></param> 6 /// <returns></returns> 7 [HttpGet] 8 [IsJsonpCallback] 9 10 public dynamic GetInfor2(string userName) 11 { 12 var data = new 13 { 14 id = userName + "001", 15 userName = userName 16 }; 17 string xjs = JsonConvert.SerializeObject(data); 18 return $"{xjs}"; 19 }
前端代碼:
1 //2. 利用過濾器改造-jsonp請求 2 $('#j_jsonp2').click(function () { 3 $.ajax({ 4 url: 'http://localhost:29492/Home/GetInfor2', 5 type: "get", 6 dataType: "jsonp", 7 //需要和服務端回掉方法中的參數名相對應 8 //注釋掉這句話默認傳的名稱叫callback 9 jsonp: "myCallBack", 10 cache: false, 11 data: { userName: "ypf" }, 12 success: function (data) { 13 console.log(data); 14 alert(data.id + "," + data.userName); 15 } 16 }); 17 }); 18 //3. 利用過濾器改造-普通請求 19 $('#j_jsonp21').click(function () { 20 $.ajax({ 21 url: 'http://localhost:29492/Home/GetInfor2', 22 type: "get", 23 dataType: "json", 24 cache: false, 25 data: { userName: "ypf" }, 26 success: function (data) { 27 console.log(data); 28 alert(data.id + "," + data.userName); 29 } 30 }); 31 });
3. 利用程序集【WebApiContrib.Core.Formatter.Jsonp】改造
GitHub地址:https://github.com/WebApiContrib/WebAPIContrib.Core/tree/master/src/WebApiContrib.Core.Formatter.Jsonp
通過Nuget引入上面程序集,在ConfigureServices中的AddMvc中,書寫代碼 option => option.AddJsonpOutputFormatter(),即可實現JSONP請求返回JSONP格式,普通請求返回不同格式, 默認情況下回調參數為“callback”,也可以手動配置:option =>option.AddJsonpOutputFormatter(mvcOption.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault(), "myCallBack")
默認回調參數配置的代碼:
1 services.AddMvc( 2 //不寫參數的話,走的是默認回調參數 callback 3 option => option.AddJsonpOutputFormatter() 4 ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
命名回調參數配置的代碼:
1 MvcOptions mvcOption = new MvcOptions(); 2 services.AddMvc( 3 //配置JSONP的方案二,回調參數配置為myCallBack 4 option =>option.AddJsonpOutputFormatter(mvcOption.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault(), "myCallBack") 6 ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
通過上面的代碼配置,即可實現JSONP請求返回JSONP格式,普通請求返回不同格式。(原理去github上看代碼)
四. Chrome瀏覽器配置
1. 前提
注釋掉所有策略、特性,通過下面的步驟設置Chrome瀏覽器的屬性,使其支持跨域。
2. 配置步驟
(1).在電腦上新建一個目錄,例如:C:\MyChromeDevUserData
(2).在Chrome根目錄中找到exe程序,在其屬性頁面中的目標輸入框里加上 --disable-web-security --user-data-dir=C:\MyChromeDevUserData,其中--user-data-dir的值就是剛才新建的目錄。
如下圖:
配置內容為:"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir=C:\MyChromeDevUserData,--user-data-dir
(3).點擊應用和確定后關閉屬性頁面,並打開chrome瀏覽器。再次打開chrome,發現有“--disable-web-security”相關的提示,如下圖:,說明chrome可以正常跨域工作了。
3. 測試
(1). 服務器端方法 和 前端請求代碼 如下:
1 /// <summary> 2 /// 不進行任何配置,通過配置瀏覽器進行跨域 3 /// </summary> 4 /// <returns></returns> 5 public string TestNoConfig() 6 { 7 return "ypfTestNoConfig"; 8 }
1 //1. 配置瀏覽器進行跨域 2 $('#j_brower').click(function () { 3 $.post("http://localhost:29492/Home/TestNoConfig", {}, function (data) { 4 alert(data); 5 }); 6 });
(2). 先用普通瀏覽器進行請求,請求不通,如下圖:
(3). 再用配置好跨域的chrome進行請求,請求通了,如下圖:
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 本人才疏學淺,用郭德綱的話說“我是一個小學生”,如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。