zoukankan      html  css  js  c++  java
  • 【第三篇】ASP.NET MVC快速入门之安全策略(MVC5+EF6)

    目录

    【第一篇】ASP.NET MVC快速入门之数据库操作(MVC5+EF6)

    【第二篇】ASP.NET MVC快速入门之数据注解(MVC5+EF6)

    【第三篇】ASP.NET MVC快速入门之安全策略(MVC5+EF6)

    【第四篇】ASP.NET MVC快速入门之完整示例(MVC5+EF6)

    【番外篇】ASP.NET MVC快速入门之免费jQuery控件库(MVC5+EF6)

     

    请关注三石的博客:http://cnblogs.com/sanshi

     

    表单身份验证(Forms Authentication)

    WebForms中的表单身份验证

    在讲解MVC提供的安全策略之前,还是先看下WebForms中常见的表单身份验证(Forms Authentication),这种身份验证的过程也很简单:

    1. 用户提供登录信息(比如用户名和密码)。
    2. 登录信息验证通过后,会创建一个包含用户名的FormsAuthenticationTicket对象。
    3. 对此Ticket对象进行加密,并将加密结果以字符串的形式保存到浏览器Cookie中。

     

    后会的所有HTTP请求,都会带上这个Cookie并由WebForms进行比对,同时对外公开如下两个属性:

    1. HttpContext.User.Identity.IsAuthenticated
    2. HttpContext.User.Identity.Name

     

    在Web.config中,我们一般需要配置登录页面(loginUrl)、登录后的跳转页面(defaultUrl),

    登录后的保持时间(timeout)等信息:

     
    <system.web>
    <authentication mode="Forms">
          <forms loginUrl="~/default.aspx" timeout="120"
    defaultUrl="~/main.aspx" protection="All" path="/" />
    </authentication>
    <authorization>
          <deny users="?" />
    </authorization>
    </system.web>
     

     

    上面这个配置拒绝了所有用户的匿名访问,当然我们在<system.web>节的外面更改指定目录的访问权限,比如:

     
    <location path="res">
           <system.web>
             <authorization>
                  <allow users="*" />
             </authorization>
           </system.web>
    </location>
     

     

    这个配置允许匿名用户对res目录的访问(一般是静态资源)。

     

    MVC中的表单身份验证

    MVC对验证模型进行了重写,但是基本的原理没有变化,我们更关注的是不同点:

    1. WebForms中基于目录进行权限控制。
    2. MVC中对控制器或者控制器的方法进行权限控制。

     

    理解这一点也不难,因为MVC中没有和物理目录对应的URL,并且同一个控制器方法可能会对应多个访问URL,这一过程是由路由引擎配置的,在第一篇文章中有简单介绍。

     

    Authorize注解

    在MVC中,我们要保护的资源不是文件夹目录,而是控制器和控制器方法,所以MVC提供了授权过滤器(Authorize Filter)对此进行保护,它是以数据注解的形式提供的。

    [Authorize]
    public class StudentsController : Controller
    {
           ...
    }

     

    这里是对整个控制器进行了保护,防止匿名用户访问,这时访问会得到一个错误的页面:

     

     

    配置表单身份验证

    现在添加配置信息:

    <system.web>
    <authentication mode="Forms">
      <forms loginUrl="~/Home/Login" defaultUrl="~/Students" timeout="120" protection="All" path="/" />
    </authentication>
    </system.web>

     

     

    指定了登录页面~/Home/Login,登录后的页面是~/Students,现在再来浏览页面:

    http://localhost:55654/Students

     

     

     

    这次访问有两个HTTP请求,并且浏览器地址栏的URL改变了:

    http://localhost:55654/Home/Login?ReturnUrl=%2fStudents

     

    这样的地方我们很熟悉,ReturnUrl参数指定了登录成功后需要调整的页面,而~/Home/Login则是我们刚刚在Web.config中配置的登录页面。

     

    两个HTTP请求中的第一个,响应码是302,这是一个重定向响应,浏览器会自动识别302响应并跳转到响应头中Location指定的网址。所以第二个请求是由浏览器发起的,但是我们尚未定义Login页面,所以返回404未找到。

     

    创建登录页面

    定义Home/Login控制器方法:

     
    public class HomeController : Controller
    {
           public ActionResult Login()
           {
                  return View();
           }
    }
     

     

     

    在操作方法内部点击右键,选择[添加视图…]菜单项:

     

     

    在弹出的向导对话框中,选择[Empty(without Model)],我们来手工创建视图内容:

     

     

    完成的视图页面:

     
    @{
        ViewBag.Title = "Login";
    }
    
    <h2>Login</h2>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
    <input type="text" name="UserName" /> <input type="password" name="Password" /> <input type="submit" value="登录" /> }
     

     

     

    点击[登录]按钮,表单会通过POST请求提交到Login方法:

     
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Login(string UserName, string Password)
    {
           if(UserName == "sanshi" && Password == "pass")
           {
                  FormsAuthentication.RedirectFromLoginPage("sanshi", false);
           }
           return View();
    }
     

     

     

    这里硬编码了管理员的用户名和密码,在实际应用中可能需要从数据库中读取。

     

    在布局中显示登录状态

    接下来,我们需要在布局页面(Shared/_Layout.cshtml)中放置登录后的信息以及[退出系统]按钮:

     
    @if (User.Identity.IsAuthenticated)
    {
           using (Html.BeginForm("Logout", "Home", FormMethod.Post, new { id = "logoutForm" }))
           {
                  @Html.AntiForgeryToken()
                  <ul class="nav navbar-nav navbar-right">
                         <li><a href="javascript:;">Hello, @User.Identity.Name</a></li>
                         <li><a href="javascript:;" id="logout">退出系统</a></li>
                  </ul>
           }
    }
    else
    {
           <ul class="nav navbar-nav navbar-right">
                  <li>@Html.ActionLink("登录", "Login", "Home")</li>
           </ul>
    }
     

     

    这段代码有两层逻辑:

    1. 如果用户已经验证过身份,则显示一个表单,里面放置[Hello, sanshi]以及一个登录按钮。受限于Bootstrap的内置样式,这里只能通过a标签来取代input标签,在页面底部还会注册脚本来处理按钮点击事件。
    2. 如果是匿名用户,则显示[登录]的超链接。

     

    实现[退出系统]功能

    注册[退出系统]按钮的客户端处理脚本,由于在生成表单标签时(Html.BeginForm),我们设置了表单标签的id属性,所以点击[退出系统]按钮时简单提交表单即可:

     
    <script>
           $(function () {
    
                  $('#logout').click(function () {
                         $('#logoutForm').submit();
                  });
           });
    </script>
     

     

     

    [退出系统]按钮的后台逻辑,需要先清空客户端Cookie,然后执行客户端跳转:

     
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Logout()
    {
           FormsAuthentication.SignOut();
           return RedirectToAction("Index", "Home");
    }
     

     

    运行效果

    来看下页面运行效果,首先是登录页面:

     

    登录成功后,直接跳转到~/Students页面:

     

     

    跨站请求伪造(CSRF)

    在前面的HTTP POST请求中,我们多次在View和Controller中看下如下代码:

    1. View中调用了Html.AntiForgeryToken()。
    2. Controller中的方法添加了[ValidateAntiForgeryToken]注解。

     

    这样看似一对的写法其实是为了避免引入跨站请求伪造(CSRF)攻击。

     

    这种攻击形式大概在2001年才为人们所认知,2006年美国在线影片租赁网站Netflix爆出多个CSRF漏洞,2008年流行的视频网址YouTube受到CSRF攻击,同年墨西哥一家银行客户受到CSRF攻击,杀毒厂商McAfee也曾爆出CSRF攻击(引自wikipedia)。

     

    之所以很多大型网址也遭遇CSRF攻击,是因为CSRF攻击本身的流程就比较长,很多开发人员可能在几年的时间都没遇到CSRF攻击,因此对CSRF的认知比较模糊,没有引起足够的重视。

     

    CSRF攻击的模拟示例

    我们这里将通过一个模拟的示例,讲解CSRF的攻击原理,然后再回过头来看下MVC提供的安全策略。

     

    看似安全的银行转账页面

    假设我们是银行的Web开发人员,现在需要编写一个转账页面,客户登录后在此输入对方的账号和转出的金额,即可实现转账:

     

     
    [Authorize]
    public ActionResult TransferMoney()
    {
           return View();
    }
    
    [HttpPost]
    [Authorize]
    public ActionResult TransferMoney(string ToAccount, int Money)
    {
           // 这里放置转账业务代码
    
           ViewBag.ToAccount = ToAccount;
           ViewBag.Money = Money;
           return View();
    }
     

     

     

    由于这个过程需要身份验证,所以我们为TransferMoney的两个操作方法都加上了注解[Authorize],以阻止匿名用户的访问。

     

    如果直接访问http://localhost:55654/Home/TransferMoney,会跳转到登录页面:

     

     

    登录后,来到转账页面,我们看下转账的视图代码:

     
    @{
        ViewBag.Title = "Transfer Money";
    }
     
    <h2>Transfer Money</h2>
     
    @if (ViewBag.ToAccount == null)
    {
        using (Html.BeginForm())
        {
            <input type="text" name="ToAccount" />
            <input type="text" name="Money" />
            <input type="submit" value="转账" />
        }
    }
    else
    {
        @:您已经向账号 [@ViewBag.ToAccount] 转入 [@ViewBag.Money] 元!
    }
     

     

     

    视图代码中有一个逻辑判断,根据ViewBag.ToAccount是否为空来显示不同内容:

    1. ViewBag.ToAccount为空,则表明是页面访问。
    2. ViewBag.ToAccount不为空,则为转账成功,需要显示转账成功的提示信息。

     

    来看下页面运行效果:

     

     

     

    功能完成!看起来没有任何问题,但是这里却又一个CSRF漏洞,隐蔽而难于发现。

     

    我是黑客,Show me the money

    这里就有两个角色,银行的某个客户A,黑客B。

     

    黑客B发现了银行的这个漏洞,就写了两个简单的页面,页面一(click_me_please.html):

     
    <!DOCTYPE html>
    <html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    </head>
    <body>
          
           哈哈,逗你玩的!
          
           <iframe frameborder="0"
    style="display:none;" src="./click_me_please_iframe.html"></iframe>
     
    </body>
    </html>
     

     

     

    第一个页面仅包含了一个隐藏的iframe标签,指向第二个页面(click_me_please_iframe.html):

     
    <!DOCTYPE html>
    <html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    </head>
    <body onload="document.getElementById('myform1').submit();">
     
          
           <form method="POST" id="myform1"
    action="http://localhost:55654/Home/TransferMoney">
                  <input type="hidden" name="ToAccount" value="999999999">
                  <input type="hidden" name="Money" value="3000">
           </form>
     
    </body>
    </html>
     

     

     

    第二个页面放置了一个form标签,并在里面放置了黑客自己的银行账号和转账金额,在页面打开时提交表单(body的onload属性)。

     

    现在黑客把这两个页面放到公网:

    http://fineui.com/demo_mvc/csrf/click_me_please.html

     

    然后批量向用户发送带有攻击链接的邮件,而银行的客户A刚好登录了银行系统,并且手贱点击了这个链接:

     

     

    然后你将看到这个页面:

     

     

    你可能会在心里想,谁这么无聊,然后郁闷的关闭了这个页面。之后客户A会更加郁闷,因为黑客B的银行账号[999999999]已经成功多了3000块钱!

     

    到底怎么转账的,不是有身份验证吗

    是的。转账的确是需要身份验证,现在的问题是你登录了银行系统,已经完成了身份验证,并且在浏览器新的Tab中打开了黑客的链接,我们来看下到底发生了什么:

     

     

    这里有三个HTTP请求,第一个就是[逗你玩]页面,第二个是里面的IFrame页面,第三个是IFrame加载完毕后发起的POST请求,也就是具体的转账页面。因为IFrame是隐藏的,所以用户并不知道发生了什么。

     

    我们来具体看下第三个请求:

     

     

    明显这次转账是成功的,并且Cookie中带上了用户身份验证信息,所有后台根本不知道这次请求是来自黑客的页面,转账成功的返回内容:

     

     

    如何阻止CSRF攻击

    从上面的实例我们可以看出,CSRF源于表单身份验证的实现机制。

     

    由于HTTP本身是无状态的,也就是说每一次请求对于Web服务器来说都是全新的,服务器不知道之前请求的任何状态,而身份验证需要我们在第二次访问时知道是否登录的状态(不可能每次请求都验证账号密码),这本身就是一种矛盾!

     

    解决这个矛盾的办法就是Cookie,Cookie可以在浏览器中保存少量信息,所以Forms Authentication就用Cookie来保存加密过的身份信息。而Cookie中保存的全部值在每次HTTP请求中(不管是GET还是POST,也不管是静态资源还是动态资源)都会被发送到服务器,这也就给CSRF以可乘之机。

     

    所以,CSRF的根源在于服务器可以从Cookie中获知身份验证信息,而无法得知本次HTTP请求是否真的是用户发起的。

     

    Referer验证

    Referer是HTTP请求头信息中的一部分,每当浏览器向服务器发送请求时,都会附带上Referer信息,表明当前发起请求的页面地址。

     

    一个正常的转账请求,我们可以看到Referer和浏览器地址栏是一致的:

     

     

    我们再来看下刚才的黑客页面:

     

     

    可以看到Referer的内容和当前发起请求的页面地址一样,注意对比:

    1. 浏览器网址:click_me_please.html
    2. HTTP请求地址:Home/TransferMoney
    3. Referer:click_me_please_iframe.html,注意这个是发起请求的页面,而不一定就是浏览器地址栏显示的网址。

     

    基于这个原理,我们可以简单的对转账的POST请求进行Referer验证:

     
    [HttpPost]
    [Authorize]
    public ActionResult TransferMoney(string ToAccount, int Money)
    {
           if(Request.Url.Host != Request.UrlReferrer.Host)
           {
                  throw new Exception("Referrer validate fail!");
           }
     
           // 这里放置转账业务代码
     
           ViewBag.ToAccount = ToAccount;
           ViewBag.Money = Money;
           return View();
    }
     

     

     

    此时访问http://fineui.com/demo_mvc/csrf/click_me_please.html,恶意转账失败:

     

     

    MVC默认支持的CSRF验证

    MVC默认提供的CSRF验证方式更加彻底,它通过验证当前请求是否真的来自用户的操作。

     

    在视图页面,表单内部增加对Html.AntiForgeryToken函数的调用:

     
    @if (ViewBag.ToAccount == null)
    {
        using (Html.BeginForm())
        {
            @Html.AntiForgeryToken()
    
            <input type="text" name="ToAccount" />
            <input type="text" name="Money" />
            <input type="submit" value="转账" />
        }
    }
    else
    {
        @:您已经向账号 [@ViewBag.ToAccount] 转入 [@ViewBag.Money] 元!
    }
     

     

     

    这会在表单标签里面和Cookie中分别生成一个名为__RequestVerificationToken 的Token:

     

     

     

    然后添加[ValidateAntiForgeryToken]注解到控制器方法中:

     
    [HttpPost]
    [Authorize]
    [ValidateAntiForgeryToken]
    public ActionResult TransferMoney(string ToAccount, int Money)
    {
           // 这里放置转账业务代码
    
           ViewBag.ToAccount = ToAccount;
           ViewBag.Money = Money;
           return View();
    }
     

     

     

    在服务器端,会验证这两个Token是否一致(不是相等),如果不一致就会报错。

     

    下面手工修改表单中这个隐藏字段的值,来看下错误提示:

     

     

    类似的道理,运行黑客页面http://fineui.com/demo_mvc/csrf/click_me_please.html,恶意转账失败:

     

     

    此时,虽然Cookie中的__RequestVerificationToken提交到了后台,但是黑客无法得知表单字段中的__RequestVerificationToken值,所以转账失败。

     

    过多提交攻击(Over-Posting)

    在编辑Student的控制器方法中,有一个Bind特性注解,我们来回顾一下:

     
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit([Bind(Include = "ID,Name,Gender,Major,EntranceDate")] Student student)
    {
           if (ModelState.IsValid)
           {
                  db.Entry(student).State = EntityState.Modified;
                  db.SaveChanges();
                  return RedirectToAction("Index");
           }
           return View(student);
    }
     

     

     

    这是为了防止Over-Posting攻击,这个理解起来相对简单一点,Bind特性的Include属性用来指定一个白名单,所有在白名单中的属性都会参与模型绑定。

     

    假设在Student模型中增加一个[职务]的字段:

    public string Job {get; set;}

     

     

    如果没有Bind特性,那么在更新Student信息时,恶意用户可以通过模拟POST请求(第二篇文章有介绍)来提交Job的值,从而导致数据库中用户的Job改变。而Bind特性就是为了避免这种情况的发生。

     

    Bind特性还提供了黑名单的设置方式,类似如下所示:

    [Bind(Exclude = "Job")]

     

     

    但是,一般我们推荐使用白名单,这样即使模型发生改变,也不会影响到现有的功能。

     


    =========【2017-01-07】更新==========================================

    上面模型绑定时,通过Bind属性指定了需要绑定的属性列表,没有指定Job属性,所以模型绑定后Job=NULL

    如果之前设置过Job="工程师",那么通过如下代码:

    db.Entry(student).State = EntityState.Modified;
    db.SaveChanges();

    之后,这个Job就会被设为NULL,执行的SQL语句:

    exec sp_executesql N'UPDATE [dbo].[Students]
    SET [Name] = @0, [Gender] = @1, [Major] = @2, [Job] = NULL, [EntranceDate] = @3
    WHERE ([ID] = @4)
    ',N'@0 nvarchar(200),@1 int,@2 nvarchar(200),@3 datetime2(7),@4 int',@0=N'张三石8',@1=1,@2=N'材料科学与工程系',@3='2000-09-01 00:00:00',@4=1
    go

    可见,虽然我仅仅更改了Name字段,但是全部字段都会被更新到数据,并且Job被覆盖为NULL。

     

    这是我们不希望看到的结果。

     

    解决方法一:

    我们可以通过设置Job属性未改变,来不更新Job字段:

    db.Entry(student).State = EntityState.Modified;
    db.Entry(student).Property(s => s.Job).IsModified = false;
    db.SaveChanges();

    此时的SQL语句:

    exec sp_executesql N'UPDATE [dbo].[Students]
    SET [Name] = @0, [Gender] = @1, [Major] = @2, [EntranceDate] = @3
    WHERE ([ID] = @4)
    ',N'@0 nvarchar(200),@1 int,@2 nvarchar(200),@3 datetime2(7),@4 int',@0=N'张三石9',@1=1,@2=N'材料科学与工程系',@3='2000-09-01 00:00:00',@4=1
    go

     

    解决方法二:

    我们也可以先从数据库获取Student对象,然后更新部分字段:

     
    var _student = db.Students.Find(student.ID);
    _student.Name = student.Name;
    _student.Gender = student.Gender;
    _student.Major = student.Major;
    _student.EntranceDate = student.EntranceDate;

    db.SaveChanges();
     

     

    此时会有两个SQL查询,第一个是按照ID检索,第二个是更新:

     
    exec sp_executesql N'SELECT TOP (2) 
        [Extent1].[ID] AS [ID], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[Gender] AS [Gender], 
        [Extent1].[Major] AS [Major], 
        [Extent1].[Job] AS [Job], 
        [Extent1].[EntranceDate] AS [EntranceDate]
        FROM [dbo].[Students] AS [Extent1]
        WHERE [Extent1].[ID] = @p0',N'@p0 int',@p0=1
    go
    
    
    exec sp_executesql N'UPDATE [dbo].[Students]
    SET [Name] = @0
    WHERE ([ID] = @1)
    ',N'@0 nvarchar(200),@1 int',@0=N'张三石10',@1=1
    go
     

     

    特别注意:此时的SQL更新语句,不再是全部更新,而是仅仅更新变化的数据(因为通过第一次的查询,EF知道数据库的字段值,从而可以得知那些需要更新)。

     

     =========【2017-01-07】更新==========================================


     

     

    小结

    本篇文章首先介绍了MVC下Forms Authentication的实现方式以及与WebForms下表单身份验证的区别。然后重点讲解了跨站请求伪造攻击(CSRF),由于这种攻击流程比较长,理解起来比较晦涩,我们特地制作了一个攻击案例,希望能够引起开发人员的重视。Over-Posting攻击相对比较简单,但是需要我们在实际编码中严格遵守安全指引,不能存在侥幸心里。当然还有其他类型的攻击,比如跨站脚本攻击(XSS),Cookie盗取,开放重定向攻击等等,限于篇幅原因就不一一介绍。

    从下一篇文章开始,我们将逐渐丰富示例的功能,先为表格页面增加一个搜索表单,可以根据不同的查询条件显示表格数据。

    下载示例源代码

    文章出处:http://www.cnblogs.com/sanshi/p/6211226.html

  • 相关阅读:
    A1066 Root of AVL Tree (25 分)
    A1099 Build A Binary Search Tree (30 分)
    A1043 Is It a Binary Search Tree (25 分) ——PA, 24/25, 先记录思路
    A1079; A1090; A1004:一般树遍历
    A1053 Path of Equal Weight (30 分)
    A1086 Tree Traversals Again (25 分)
    A1020 Tree Traversals (25 分)
    A1091 Acute Stroke (30 分)
    A1103 Integer Factorization (30 分)
    A1032 Sharing (25 分)
  • 原文地址:https://www.cnblogs.com/net-sky/p/9235964.html
Copyright © 2011-2022 走看看