本文3.0版本文章
更新反饋
1、博友@落幕殘情童鞋說到了,Nginx反向代理實現跨域,因為我目前還沒有使用到,給忽略了,這次記錄下,為下次補充。此坑已填
2、提示:跨域的姊妹篇——《三十三║ ⅖ 種方法實現完美跨域》
代碼已上傳Github+Gitee,文末有地址
今天忙着給小伙伴們提出的問題解答,時間上沒把握好,都快下班了,趕緊發布:書說上文《從壹開始前后端分離【 .NET Core2.0 +Vue2.0 】框架之十一 || AOP自定義篩選,Redis入門 11.1》,昨天咱們說到了分布式緩存鍵值數據庫,主要講解了如何安裝,使用,最后遺留了一個問題,同步+Redis緩存還是比較簡單,如何使用異步泛型存取Redis,還是一直我的心結,希望大家有會的,可以不吝賜教,本系列教程已經基本到了尾聲,今天就說兩個小的知識點,既然本系列是講解前后端分離的,那一定會遇到跨域的問題,沒錯,今天將說下跨域!然后順便說一下DTOs(數據傳輸對象),這些東西大家都用過,比如,在MVC中定義一個ViewModel,是基於Model實體類的,然后做了相應的變化,以適應前端需求,沒錯,就是這個,如果大型的實體類,一個個復雜的話會稍顯費力,今天就是用一個自動映射工具——AutoMapper。
零、今天完成左下角的深紫色部分
一、為什么會出現跨域的問題
跨域問題由來已久,主要是來源於瀏覽器的”同源策略”。
何為同源?只有當協議、端口、和域名都相同的頁面,則兩個頁面具有相同的源。只要網站的 協議名protocol、 主機host、 端口號port 這三個中的任意一個不同,網站間的數據請求與傳輸便構成了跨域調用,會受到同源策略的限制。 同源策略限制從一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的關鍵的安全機制。瀏覽器的同源策略,出於防范跨站腳本的攻擊,禁止客戶端腳本(如 JavaScript)對不同域的服務進行跨站調用(通常指使用XMLHttpRequest請求)。
所以說我們在web中,我們無法去獲取跨域的請求,常見的就是無法通過js獲取接口(這里要說下我的以前使用的經驗:在同源系統下,前端js去調用后端接口,然后后端C#去調取跨域接口,這是我以前采用的辦法,但是前后端分離,這個辦法肯定就是不行了,因為那時候已經沒有了前后端之分,是兩個項目),所以我們只要合理使用同源策略,就可以達到跨域訪問的目的。
二、三種跨域方式 之JsonP
我自己建立了一個靜態頁面,用來模擬前端訪問,具體如下步驟:
1、模擬前端訪問頁面
新建一個Html頁面,使用Jquery來發送請求(文件在項目的WWW文件夾下,大家可以自己下載,或者Copy下邊代碼)。
一共三種跨域方法:
<html> <head> <meta charset="utf-8"> <title>Blog.Core</title> <script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script> <style> div { margin: 10px; word-wrap: break-word; } </style> <script> $(document).ready(function () { $("#jsonp").click(function () { $.getJSON("/api/Login/jsonp?callBack=?", function (data) { $("#data-jsonp").html("數據: " + data.value); }); }); $("#cors").click(function () { $.get("/api/Login/Token", function (data, status) { console.log(data); $("#status-cors").html("狀態: " + status); $("#data-cors").html("數據: " + data? data.token:"失敗"); }); }); $("#cors-post").click(function () { let postdata = { "bID": 10, "bsubmitter": "222", "btitle": "33333", "bcategory": "4444", "bcontent": "5555", "btraffic": 0, "bcommentNum": 0, "bUpdateTime": "2018-11-08T02:36:26.557Z", "bCreateTime": "2018-11-08T02:36:26.557Z", "bRemark": "string" }; $.ajax({ type: 'post', url: '/api/Values', contentType: 'application/json', data: JSON.stringify(postdata), success: function (data, status) { console.log(data); $("#status-cors-post").html("狀態: " + status); $("#data-cors-post").html("數據: " + JSON.stringify(data)); } }); //$.ajax({ // type: "POST", // url: "/api/Values", // success: function (data, status) { // console.log(data); // $("#status-cors-post").html("狀態: " + status); // $("#data-cors-post").html("數據: " + data); // } //}); }); }); </script> </head> <body> <h3>通過JsonP實現跨域請求</h3> <button id="jsonp">發送一個 GET </button> <div id="status-jsonp"></div> <div id="data-jsonp"></div> <hr /> <h3>添加請求頭實現跨域</h3> 無 <hr /> <h3>通過CORS實現跨域請求,另需要在服務器端配置CORE</h3> <button id="cors">發送一個 GET </button> <div id="status-cors"></div> <div id="data-cors"></div> <hr /> <button id="cors-post">發送一個 POST </button> <div id="status-cors-post"></div> <div id="data-cors-post"></div> <hr /> </body> </html>
注意:這里一定要注意jsonp的前端頁面請求寫法,要求很嚴謹
2、請求頁面部署
1、其實只需要當前Blog.Core 項目配置了靜態文件中間件,直接訪問就可以
比如我的在線地址:查看右側公告欄
2、單獨部署:將這個頁面部署到自己的IIS中(拷貝到文件里,直接在iis添加該文件,訪問剛剛的Html文件目錄就行)
3、設計后台接口
在我們的項目 LoginController 中,設計Jsonp接口,Core調用的接口我們已經有了,就是之前獲取Token的接口GetJWTStr
[HttpGet] [Route("jsonp")] public void Getjsonp(string callBack, long id = 1, string sub = "Admin", int expiresSliding = 30, int expiresAbsoulute = 30) { TokenModel tokenModel = new TokenModel(); tokenModel.Uid = id; tokenModel.Sub = sub; DateTime d1 = DateTime.Now; DateTime d2 = d1.AddMinutes(expiresSliding); DateTime d3 = d1.AddDays(expiresAbsoulute); TimeSpan sliding = d2 - d1; TimeSpan absoulute = d3 - d1; string jwtStr = BlogCoreToken.IssueJWT(tokenModel, sliding, absoulute);
//重要,一定要這么寫 string response = string.Format("\"value\":\"{0}\"", jwtStr); string call = callBack + "({"+response+"})"; Response.WriteAsync(call); }
注意:這里一定要注意jsonp的接口寫法,要求很嚴謹
4、點擊”通過JsonP實現跨域請求“按鈕,發現已經有數據了,證明Jsonp跨域已經成功,你可以換成自己的域名試一試,但是Cors的還不行
三、三種跨域方式 之添加請求頭實現跨域
這里我沒有寫到代碼里,是在一般處理程序里之前用到的
1、后端
public void ProcessRequest(HttpContext context) { //接收參數 string uName = context.Request["name"]; string data = "{\"name\":\"" + uName + "\",\"age\":\"18\"}"; //只需在服務端添加以下兩句 context.Response.AddHeader("Access-Control-Allow-Origin", "*"); //跨域可以請求的方式 context.Response.AddHeader("Access-Control-Allow-Methods", "POST,GET"); context.Response.Write(data); }
2、前端
function ashxRequest() { $.post("http://localhost:5551/ashxRequest.ashx", { name: "halo" }, function (data) { for (var i in data) { alert(data[i]); } }, "json") }
大家感興趣可以自己實驗下。有問題請留言
四、三種跨域方式 之 高效CORS
1、前端ajax調用
前端的代碼在jsonp的時候已經寫好,請往上看第二節,后端接口也是Token接口
剩下的就是配置跨域了,很簡單!
2、配置 CORS 跨域
在ConfigureServices中添加
#region CORS //跨域第一種方法,先注入服務,聲明策略,然后再下邊app中配置開啟中間件 services.AddCors(c => { //↓↓↓↓↓↓↓注意正式環境不要使用這種全開放的處理↓↓↓↓↓↓↓↓↓↓ c.AddPolicy("AllRequests", policy => { policy .AllowAnyOrigin()//允許任何源 .AllowAnyMethod()//允許任何方式 .AllowAnyHeader()//允許任何頭 .AllowCredentials();//允許cookie }); //↑↑↑↑↑↑↑注意正式環境不要使用這種全開放的處理↑↑↑↑↑↑↑↑↑↑ //一般采用這種方法 c.AddPolicy("LimitRequests", policy => { policy .WithOrigins("http://127.0.0.1:1818", "http://localhost:8080", "http://localhost:8021", "http://localhost:8081", "http://localhost:1818")//支持多個域名端口,注意端口號后不要帶/斜桿:比如localhost:8000/,是錯的 .AllowAnyHeader()//Ensures that the policy allows any header. .AllowAnyMethod(); }); }); // 這是第二種注入跨域服務的方法,這里有歧義,部分讀者可能沒看懂,請看下邊解釋 //services.AddCors(); #endregion
歧義解釋:
可能有些讀者會說,你這里寫錯了,應該是 app.UseCors() ,我肯定是知道的,那為啥還要這么寫呢,是因為這里我提供了兩套 Cors 跨域寫法:
1、配置在 configureServices 中,然后再在管道中開啟中間件,就是上邊的寫法;
2、還有一個是,只在 configureServices 中,開啟服務,然后在中間件中,具體的配置:
//跨域第一種版本,請要ConfigureService中配置服務 services.AddCors(); // app.UseCors(options => options.WithOrigins("http://localhost:8021").AllowAnyHeader() //.AllowAnyMethod());
基本注釋都有,大家都能看的懂,就這么簡單!
注意:在定義策略 LimitRequests 的時候,源域名應該是客戶端請求的端口域名,不是當前API的域名端口。
感謝博友 @學弱 提醒:CORS的配置一定要放在AutoFac前面,否則builder.Populate(services);后,你再進行配置會沒有效果。
3、啟動中間件
在啟動文件 的Configure 配置方法里,添加啟用Cors中間件服務
感謝博友@kiritio_ooo的提醒,Git已更新
注意:如果你使用了 app.UserMvc() 或者 app.UseHttpsRedirection()這類的中間件,一定要把 app.UseCors() 寫在它們的上邊,先進行跨域,再進行 Http 請求,否則會提示跨域失敗。
因為這兩個都是涉及到 Http請求的,如果你不跨域就直接轉發或者mvc,那肯定報錯。
4、運行調試,一切正常
至此,跨域的問題已經完成辣
重要:如果你想查看效果,我的最新的Github上代碼已經給大家寫好了,大家clone以后,只需要執行 http://localhost:8081/corspost.html ,就能看到各種效果了。當然如果懶得下載,可以看我的在線效果:查看右側公告欄
注意:這里要說下,如果遇到了跨域失敗的提示,比如這樣:
這個並不一定是沒有配置好導致的跨域失敗,還有可能是接口有錯誤,比如 500,或者是 404 了,導致的接口異常,所以就提示訪問有錯誤。
五、其他跨域方法補充
請參考我的文章:
nginx是一個高性能的web服務器,常用作反向代理服務器。nginx作為反向代理服務器,就是把http請求轉發到另一個或者一些服務器上。
通過把本地一個url前綴映射到要跨域訪問的web服務器上,就可以實現跨域訪問。
對於瀏覽器來說,訪問的就是同源服務器上的一個url。而nginx通過檢測url前綴,把http請求轉發到后面真實的物理服務器。並通過rewrite命令把前綴再去掉。這樣真實的服務器就可以正確處理請求,並且並不知道這個請求是來自代理服務器的。
簡單說,nginx服務器欺騙了瀏覽器,讓它認為這是同源調用,從而解決了瀏覽器的跨域問題。又通過重寫url,欺騙了真實的服務器,讓它以為這個http請求是直接來自與用戶瀏覽器的。
這樣,為了解決跨域問題,只需要動一下nginx配置文件即可。
六、結語
三種辦法其實都能達到目的,但是優缺點也很明顯
1、手動創建JSONP跨域
優點:無瀏覽器要求,可以在任何瀏覽器中使用此方式
缺點:格式要求很嚴格,只支持get請求方式,請求的后端出錯不會有提示,造成不能處理異常
2、添加請求頭實現跨域
優點:支持任意請求方式,並且后端出錯會像非跨域那樣有報錯,可以對異常進行處理
缺點:兼容性不是很好,IE的話 <IE10 都不支持此方式
雖然CORS的方法有點兒類似請求頭,但是封裝,兼容性,靈活性都要好的很多,強烈推薦。
七、初探DTOs
請看以下實體類
//數據庫實體類 public class Author { public string Name { get; set; } } public class Book { public string Title { get; set; } public Author Author { get; set; } } //頁面實體類 public class BookViewModel { public string Title { get; set; } public string Author { get; set; } } //api調用 BookViewModel model = new BookViewModel { Title = book.Title, Author = book.Author.Name }
上面的例子相當的直觀了,我們平時也是這么用的基本,但是問題也隨之而來了,我們可以看到在上面的代碼中,如果一旦在Book對象里添加了一個額外的字段,而后想在前台頁面輸出這個字段,那么就需要去在項目里找到每一處有這樣BookViewModel轉換字段的地方,這是非常繁瑣的。另外,BookViewModel.Author是一個string類型的字段,但是Book.Author屬性卻是Author對象類型的,我們用的解決方法是通過Book.Auther對象來取得Author的Name屬性值,然后再賦值給BookViewModel的Author屬性,這樣看起行的通,但是想一想,如果打算在以后的開發中把Name拆分成兩個-FisrtName和LastName,我的天吶!我們得去把原來的ViewModel對象也拆分成對應的兩個字段,然后在項目中找到所有的轉換,然后替換。
那么有什么辦法或者工具來幫助我們能夠避免這樣的情況發生呢?AutoMapper正是符合要求的一款插件。只需一鍵操作,就能一勞永逸,解決所有問題,然后通過依賴注入,快速使用:
//AutoMapper自動映射 //Mapper.Initialize(cfg => cfg.CreateMap<BlogArticle, BlogViewModels>()); //BlogViewModels models = Mapper.Map<BlogArticle, BlogViewModels>(blogArticle); BlogViewModels models = IMapper.Map<BlogViewModels>(blogArticle);//就這一句話完全搞定所有轉換
今天因為時間的關系,沒有說到Automapper,明天再見吧~
八、CODE