问题

通过 CSRF(Cross-Site Request Forgery)防护,保护从 MVC 页面提交到ASP.NET Web API 的数据。

解决方案

ASP.NET 已经加入了 CSRF 防护功能,只要通过 System.web.Helpers.AntiForgery 类(System.Web.WebPages 的一部分)就可以。

他会生成两个 Token:

  • Cookie Token

  • 基于字符串的 Token

基于字符串的 Token 是可以嵌入到表单或者请求头(使用 Ajax 的情况下)。为了防止 CSRF 攻击,表单提交和Ajax 请求到 API 的数据必须包含这些Token,服务器将会验证这两个 Token。

在 ASP.NET Web API,anti-CSRF Token 验证是一个典型的实现了横切关系的 MessageHandler。

工作原理

为了能在 MVC 应用程序的上下文中生成 Token,我们必须在表单中调用一个叫做 AntiForgeryToken 的HtmlHelper 的扩展方法。

1
2
3
4
<form id="myForm">
    @Html.AntiForgeryToken()
    @* 其他标签 *@
</form>

这个帮助方法在AntiForgery 类中。他会写一个 Token 到响应的 Cookie 中,同时生成一个名字叫做_RequestVerificationToken 的字段,也会随着表单数据同时被提交。

为能在服务器端验证 Token,我们可以通过调用AntiForgery 类的静态方法 Validate 来验证。如果调用的时候没有传递参数的话,就会从 HttpContext.Current 中试着获取相关的 Cookie 和请求体中提取 Token,在这里,我们假设确实有一个 Body 并且 Body 中也有一个 _RequestVerificationToken。

由于这个方法是 void (无返回值)的,所以,请求验证成功后,方法什么反馈也没有,如果失败,就会抛HttpAntiForgeryException 的异常。我们可以捕获这个异常,然后返回给客户端相应的响应(例如,一个 HTTP 403 的状态码)。

有一个可替代的方式就是调用 Validate 方法,我们自己来传这两个 Token。这时候,就要从 Request 中获取这两个值。例如,可能是在 Header 中。这种方式也可以摆脱对 HttpContext 的依赖。

对于 Web API,我们可以自定义消息处理器,在每个请求进入 Web API 的时候来负责 CSRF Token 的验证,执行必要的验证,然后继续管道执行,或者,在请求无效的情况下,直接短路错误响应(也就是说,立即返回错误码)。

代码演示

我们来演示 MessageHandler 执行 CSRF 验证的例子如清单 1-23 所示。

两种方式:

  1. 用 Ajax 请求。

  2. 用其他的请求。

我们都简单假设他们都是表单提交的。如果是一个 Ajax 请求,我们可以尝试着从请求 Header 中获取Token,同时,可以从与 Request 一同提交的 Cookie 集合中获取Cookie Token,然后,使用无参的 Validate方法验证,这样,就需要我们自己来提取 Token。

如果验证失败,客户端会得到一个 403 的错误响应。

清单 1-23. Anti_CSRF 消息处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class AntiForgeryHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken)
    {
        string cookieToken = null;
        string formToken = null;
        if (request.IsAjaxRequest())
        {
            IEnumerable<string> tokenHeaders;
            if (request.Headers.TryGetValues("__RequestVerificationToken"out tokenHeaders))
            {
                var cookie = request.Headers.GetCookies(AntiForgeryConfig.CookieName).
                FirstOrDefault();
                if (cookie != null)
                {
                    cookieToken = cookie[AntiForgeryConfig.CookieName].Value;
                }
                formToken = tokenHeaders.FirstOrDefault();
            }
        }
        try
        {
            if (cookieToken != null && formToken != null)
            {
                AntiForgery.Validate(cookieToken, formToken);
            }
            else
            {
                AntiForgery.Validate();
            }
        }
        catch (HttpAntiForgeryException)
        {
            return request.CreateResponse(HttpStatusCode.Forbidden);
        }
        return await base.SendAsync(request, cancellationToken);
    }
}

我们还需要在 API 的HttpConfiguration 中注册,这样才会在全局起作用。

1
config.MessageHandlers.Add(new AntiForgeryHandler());

构筑一个 anti-CSRF 护盾作为消息处理器并不是唯一方式。我们也可以在过滤器内部使用同样的代码,然后将过滤器应用到相应的 Action 上(类似的,怎么用过滤器验证,我们将在 5-4 详细讨论)。如果消息处理器不是全局使用,也可以附加到指定路由上。我们将在 3-9 详细讨论这一块儿。

HttpRequestMessage有一个内建的方式来检查是否为 Ajax 请求,就是用一个简单的扩展方法来实现,他依赖于 Header 的 X-Requested-With,大多数的 JavaScript 框架都会自动发送这个在 Header 中。这个方法如清单1-24 所示。

清单 1-24. 检查 HttpRequestMessage 是否为一个 Ajax 请求的扩展方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static class HttpRequestMessageExtensions
{
    public static bool IsAjaxRequest(this HttpRequestMessage request)
    {
        IEnumerable<string> headers;
        if (request.Headers.TryGetValues("X-Requested-With"out headers))
        {
            var header = headers.FirstOrDefault();
            if (!string.IsNullOrEmpty(header))
            {
                return header.ToLowerInvariant() == "xmlhttprequest";
            }
        }
        return false;
    }
}

清单 1-25 展示了,传统表单提交和 Ajax 请求都利用 anti-CSRF Token 的例子。在传统表单提交的情况下,HTML helper 会生成一个隐藏域,同时,anti-forgery token 会随着表单一块儿被自动提交。在 Ajax 请求的情况下,我们显示的从隐藏域中读取 Token,然后,将其附加到请求头中。

清单 1-25. 传统表单和 Ajax 请求方式下,提交数据到 ASP.NET WEB API 使用 Anti-CSRF 防护

//HTML表单

1
2
3
4
5
6
7
8
9
10
11
12
<form id="form1" method="post" action="/api/form" enctype="application/x-www-form-urlencoded">
    @Html.AntiForgeryToken()
    <div>
        <label for="name">Name</label>
    </div>
    <div>
        <input type="text" name="name" value="Some Name" />
    </div>
    <div>
        <button id="postData" name="postData">Post form</button>
    </div>
</form>

// Ajax 表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Html.AntiForgeryToken()
<input id="itemJS" type="text" disabled="disabled" name="text" value="some text" />
<div>
    <button id="postJS" name="postJS">Post JS</button>
</div>
<script type="text/javascript">
    $(function () {
        $("#postJS").on("click", function () {
            $.ajax({
                dataType: "json",
                data: JSON.stringify({ name: $("#itemJS").val() }),
                type: "POST",
                headers: {
                    "__RequestVerificationToken": $("#jsData input[name='__
                    RequestVerificationToken']").val()
                },
                contentType: "application/json; charset=utf-8",
                url: "/api/items"
            }).done(function (res) {
                alert(res.Name);
            });
        });
    });
</script>

[水煮 ASP.NET Web API2 方法论](1-7)CSRF-Cross-Site Request Forgery的更多相关文章

  1. [水煮 ASP.NET Web API2 方法论](3-9)空气路由的设置

    阅读导航 问题 解决方案 工作原理 代码演示 在此解释一下,空气路由,是本人臆想出来,觉着更能表达 IgnoreRoute 的意图,如果看着辣眼睛^^,请见谅. 问题 我们在之定义过集中式路由,集中式 ...

  2. [水煮 ASP.NET Web API2 方法论](1-5)ASP.NET Web API Scaffolding(模板)

    问题 我们想快速启动一个 ASP.NET Web API 解决方案. 解决方案 APS.NET 模板一开始就支持 ASP.NET Web API.使用模板往我们的项目中添加 Controller,在我 ...

  3. [水煮 ASP.NET Web API2 方法论](3-8)怎样给指定路由配置处理器

    阅读导航 问题 解决方案 工作原理 代码演示 问题 如果仅仅针对指定的路由进行某些特定的消息处理,而不是应用于所有路由,我们应该怎么做呢? 解决方案 ASP.NET WEB API 的很多功能都内建了 ...

  4. [水煮 ASP.NET Web API2 方法论](3-7)默认 Action 请求方式以及 NonActionAttribute

    问题 在 Controller 中有一个 public 的方法,但是又不想将这个 publlic 方法暴露成为一个 API. 解决方案 ASP.NET Web API 中,正常是通过 HTTP 谓词来 ...

  5. [水煮 ASP.NET Web API2 方法论](3-6)万能路由

    问题 定义什么样的路由,可以不会受请求参数类型和数量的限制,而被全部捕获? 解决方案 在路由模板中,给参数添加一个"*"前缀,例如 {*param},只要请求的 URL 能够和路由 ...

  6. [水煮 ASP.NET Web API2 方法论](3-5)路由约束

    问题 怎么样限制路由中参数的值. 解决方案 ASP.NET WEB API 允许我们通过 IHttpRouteConstraint 接口设置路由约束.集中式路由和直接式路由都可以使用 IHttpRou ...

  7. [水煮 ASP.NET Web API2 方法论](3-4)设置路由可选项

    问题 怎么样创建一个路由,不管客户端传不传这个参数,都可以被成功匹配. 解决方案 ASP.NET WEB API 的集中式路由和属性路由都支持路由声明可选参数. 在用集中式路由中可以通过 RouteP ...

  8. [水煮 ASP.NET Web API2 方法论](3-3)路由默认值

    问题 如何为路由中参数设置默认值. 解决方案 不管使用属性路由还是集中式路由,ASP.NET WEB API 都可以很方便的为路由定义默认参数.在每次客户端请求的时候,如果客户端没有传这些参数,框架会 ...

  9. [水煮 ASP.NET Web API2 方法论](3-2)直接式路由/属性路由

    问题 怎么样可以使用更贴近资源(Controller,Action)的方式定义路由. 解决方案 可以使用属性路由直接在资源级别声明路由.只要简单的在 Action 上使用属性路由 RouteAttri ...

随机推荐

  1. 清华大学计算机系大二 java 小学期考试题(摘自知乎)

    public class Main { public void test(Object o) { System.out.println("Object"); } public vo ...

  2. Codeforces Round #508 (Div. 2) E. Maximum Matching(欧拉路径)

     E. Maximum Matching 题目链接:https://codeforces.com/contest/1038/problem/E 题意: 给出n个项链,每条项链左边和右边都有一种颜色(范 ...

  3. ubuntu启动脚本

    下午分析了一下mysql的启动脚本,找到这篇,记录一下,目前很多服务都是以这种方式封装,后面自己写来借鉴一下 http://blog.fens.me/linux-upstart/

  4. tp if condition in_array用法

    <if condition="in_array($vo['status'],[3])"> <a href="javascript:void(0);&qu ...

  5. RabbitMQ基础概念(消息、队列、交换机)

    1.消息的确认 RabbitMQ需要对每一条发送的消息进行确认.消费者必须通过AMQP的basic.ack命令显式地向RabbitMQ发送一个确认,或者在订阅到队列的时候就将auto_ack参数设置为 ...

  6. C# 把类实例保存到文件里(类的序列化和反序列化)

    有时候我们希望把类的实例保存下来,以便以后的时候用.一个直观的方法就是StreamWriter把类写成一行,用\t分隔开每个属性,然后用StreamReader读出来. 但是这样太麻烦,代码行数较多, ...

  7. [技巧篇]12.从Spring的编码过滤器说起

    有一枚学生问问了我一个问题,突然灵感爆发,他使用的Spring的过滤器,前台利用GET方式向后端发出一个请求,由于里面含有中文数据,结果在后端显示的是乱码,他问我为什么?明明在Spring里面也配了字 ...

  8. iOS多线程 iOS开发Demo(示例程序)源代码

    本系列所有开发文档翻译链接地址:iOS7开发-Apple苹果iPhone开发Xcode官方文档翻译PDF下载地址(2013年12月29日更新版)   iOS程序源代码下载链接:01.大任务.zip22 ...

  9. Spring mvc详解(山东数漫江湖)

    Spring mvc框架 Spring web MVC 框架提供了模型-视图-控制的体系结构和可以用来开发灵活.松散耦合的 web 应用程序的组件.MVC 模式导致了应用程序的不同方面(输入逻辑.业务 ...

  10. javascript工厂模式、单例模式

    //工厂模式 function createObject(name,age){ var obj = new Object(); obj.name = name; obj.age = age; obj. ...