哈哈,有点标题党,但我保证这篇文章跟别的不太同样。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”,一搜一大堆,或者,等我有空了再写一篇这个主题的“最佳实践”?