哈哈,有點標題黨,但我保證這篇文章跟別的不太同樣。html
我認爲,網站安全的基礎有三塊:web
注意,我講的是基礎,若是更高級點的話能夠考慮防範機器人刷單,再高級點就防範DDoS攻擊,不過咱們仍是回到「基礎」這個話題上吧,對於中間人攻擊,使用HTTPS是正確且惟一的作法,其它都是歪門邪道,最好還要購買各個瀏覽器都認可的SSL證書;防範XSS,關鍵點在於將用戶提交數據呈如今頁面上的時候,須要使用Html Encode,或在處理帶HTML格式的用戶表單數據時,進行「消毒」(Sanitize)處理,關於這個,我前一篇文章《讓ASP.NET接受有「潛在危險」的提交》已經講述了應該怎麼作;剩下了這個CSRF是本文要講的,我認爲防範CSRF的前提是必須先作好XSS的防範工做,由於:CSRF防的是別的網站,若是本身的網站自己有XSS漏洞,被別人注入了有害腳本,那麼就變成了「家賊難防」了。ajax
至於什麼是CSRF,如何讓ASP.NET防範CSRF,這種文章不少的,好比博客園裏這篇就能夠:《ASP.NET MVC 防止 CSRF 的方法》,我總結一下通常的作法,也就這兩點:瀏覽器
Done!安全
爲啥這樣就好了呢?我簡單說說原理:@Html.AntiForgeryToken()的做用是在頁面上插入一個type爲hidden的input標籤,它的name固定是__RequestVerificationToken,value則是一長串密文,這是真的密文,是通過加密的,那明文是什麼?是隨機生成的128位數字,跟GUID差很少的東西,咱們叫它「隨機明文」吧,再附帶一點額外的信息(忽略這個吧),而後加密,加密器是這個玩意兒:System.Web.Security.MachineKey,我這裏寫個簡單的Example,你們能夠弄個HelloWorld程序看看運行效果。框架
byte[] plainText = Encoding.UTF8.GetBytes("123456"); string[] purposes = { "blah blah blah" }; byte[] cypherText = MachineKey.Protect(plainText, purposes); Console.WriteLine(Convert.ToBase64String(cypherText)); plainText = MachineKey.Unprotect(cypherText, purposes); Console.WriteLine(Encoding.UTF8.GetString(plainText));
多運行幾回,每次都能解出正確的明文,可是密文每次都不同,因此你們在不斷刷新頁面的時候,發現每次生成的value也不同,但沒事,它們的「隨機明文」是同樣的。除了生成這個input標籤以外,@Html.AntiForgeryToken()還作了個額外的動做,那就是生成一個一樣名字(也叫__RequestVerificationToken)的Cookie,內容差很少,在咱們看來也是一長串密文。因此總結回來@Html.AntiForgeryToken()就作了這兩件事:post
接下來輪到ValidateAntiForgeryToken過濾器,它收到了請求,就嘗試解出請求中的Cookie的「一長串密文」和請求中的Form的「一長串密文」,解密後比對二者的「隨機明文」,若是一致,則經過,(其實做爲高級用法你還能夠自定義一些額外的規則,不過這不在本文講述範圍內)不然拋出HttpAntiForgeryException異常。優化
爲何只須要檢驗下Cookie和Form的「隨機明文」就能夠防範CSRF了呢?其實理解起來並不難,前面說了CSRF防的是別的網站,別的網站僞造了請求,利用訪問者的瀏覽器對目標網站發送了這個請求,但僞造者並不清楚目標網站的訪問者的__RequestVerificationToken這個Cookie的值,所以表單中的__RequestVerificationToken的值也就沒法僞造,這個請求會被ValidateAntiForgeryToken過濾器攔截下來。網站
知道了原理,就來分析下它的特色與侷限性,首先很容易想到的就是:this
另外思考一下若是一個頁面中調用了屢次@Html.AntiForgeryToken(),生成了多個input標籤,會怎樣呢?會不會生成了兩個不一樣的Token,最後比對出錯?其實沒必要擔憂,正兒八經的那個隨機明文只會生成一次,Html.AntiForgeryToken()方法會檢查你提交的Cookie,若是已存在__RequestVerificationToken,那麼它就不會再生成一個新的隨機數明文了。不然若是每次都生成一個隨機數明文,你的頁面上若是有兩個Form的話,其中一個確定無法正常提交,更不用說AJAX提交的狀況。
若是不是直接提交頁面上的表單,而是AJAX POST,像這樣:
$.ajax({ type: "post", url: "/testurl", data: {test:'abc'}, success: function (data) { //done! } });
這可咋辦?你必須千方百計在data中帶上正確的__RequestVerificationToken啊!StackOverflow上有個解決方案,挺不錯,你們參考下:Go to StackOverflow,簡單地說就是寫一個ASP.NET MVC的HTML生成幫助方法,用於生成那「一長串密文」,交給這個ajax的data。
但這可不是我想說的「最佳實踐」 ,再考慮一種狀況:用js動態生成Form,而後Submit。嗯,我認可這個有點奇葩,但在個人項目中確實有很多地方是這麼幹的,你別問爲何了,反正就是有,遇到這種狀況,咋辦?看來仍是得求助於js。下面我分享下個人作法:
首先我沒有把@Html.AntiForgeryToken()放到每個Form中,我只在一處地方用到了@Html.AntiForgeryToken(),那就是母版頁!接下來把下面這段js放到common.js中(common.js是母版頁引用的js,也就是說每一個頁面會引用到):
//處理form的submit事件,添加AntiForgeryToken到表單裏 $("body").on("submit", "form", function () { var theForm = $(this); if (theForm.find("input[name='__RequestVerificationToken']").length === 0) { var antiForgery = $("input[name='__RequestVerificationToken']:first").val(); if (antiForgery) { var theAntiForgeryTokenInput = $('<input />').attr('type', 'hidden') .attr('name', '__RequestVerificationToken') .attr('value', antiForgery); $(this).prepend(theAntiForgeryTokenInput); } } });
這樣一來,全部的form的submit動做就會在這裏被處理一下,添加上了__RequestVerificationToken這個字段。接下來是AJAX POST的處理:
data= {test:'abc'}; var antiForgery = $("input[name='__RequestVerificationToken']:first").val(); if (antiForgery) { if (!data.__RequestVerificationToken) { data.__RequestVerificationToken = antiForgery; } } $.ajax({ type: "post", url: "/testurl", data: data, success: function (data) { //done! } });
嗯?你也許要問,每一個用到AJAX POST的地方都加上這麼一段代碼豈不是很繁瑣?是的,但在個人項目中,我用了幾個公共的方法對AJAX POST進行了一些封裝,因此只須要改好這幾個地方便可,你能夠根據本身的項目的實際狀況進行優化處理。
還有一種狀況是用AJAX來提交Form,而不是像上面這樣的data:
var form = $("#the-form-id"); var dataToSubmit = form.serializeArray(); var antiForgery = $("input[name='__RequestVerificationToken']:first").val(); if (antiForgery) { var found = false; for (var i = 0; i < dataToSubmit.length; i++) { if (dataToSubmit[i].name === '__RequestVerificationToken') { found = true; break; } } if (!found) { dataToSubmit.push({ name: "__RequestVerificationToken", value: antiForgery }); } } $.ajax({ type: method, url: urlToSubmit, data: dataToSubmit, success: function (data) { //done! } });
照舊,根據你的項目的實際狀況封裝一下,其實真正要改動的地方很少,只要你框架搭好了。
總結一下,框架搭好了的前提下,爲了防範CSRF,你所須要作的事情就僅剩下:給帶[HttpPost]註解的Action添加[ValidateAntiForgeryToken]。
至於驗證失敗拋出HttpAntiForgeryException異常致使默認錯誤頁面(我又叫它「死黃頁」,該死的黃頁的意思)出現的問題,你能夠在Application_Error中處理一下啊,Google關鍵字「Application_Error」,一搜一大堆,或者,等我有空了再寫一篇這個主題的「最佳實踐」?