在ABP的Web层中实现复杂请求跨域访问

在最近的项目中,后端使用ABP,前端采用React,前后端完全分离。其中大部分接口都通过WebApi层调用,项目中未使用Session。但最后在添加一个网站的验证码验证留言功能时,使用了Session验证的方式,所以将验证码请求与校验功能放在了Web层。由于测试阶段前后端不同域,涉及到跨域请求的问题。跨域问题可以通过代理等手段解决,但是也可以在后端做些简单的修改来进行实现。WebApi的跨域处理比较简单,有官方给出的解决方案Microsoft.AspNet.WebApi.Cors。但是Web层一般不涉及跨域,所以自己进行了探索实现。

一、常见方案

  1. 在web.config中添加配置。
<system.webServer>
        <httpProtocol> 
            <customHeaders> 
                <add name="Access-Control-Allow-Methods" value="OPTIONS,POST,GET"/> 
                <add name="Access-Control-Allow-Headers" value="x-requested-with"/> 
                <add name="Access-Control-Allow-Origin" value="*" /> 
            </customHeaders> 
        </httpProtocol> 
</system.webServer>
  1. 在被访问的控制器上加上[AllowCrossSiteJson("localhost:3000")]的Attribute。
    AllowCrossSiteJsonAttribute类代码如下:
public class AllowCrossSiteJsonAttribute : ActionFilterAttribute
    {
        private string[] _domains;
        public AllowCrossSiteJsonAttribute(string domain)
        {
            _domains = new string[] { domain };
        }
        public AllowCrossSiteJsonAttribute(string[] domains)
        {
            _domains = domains;
        }
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var context = filterContext.RequestContext.HttpContext;
            var host = context.Request.Headers.Get("Origin");
            if (host != null&& _domains.Contains(host))
            {
                //域
                filterContext.RequestContext.HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", host);
                //Http方法
                filterContext.RequestContext.HttpContext.Response.AppendHeader("Access-Control-Allow-Methods", "*");
            }
            base.OnActionExecuting(filterContext);
        }
    }

二、常见方案问题分析

  1. 上面两种常见方案,方案一堪称简单粗暴,方案二则有针对性的开放一些域来进行跨域访问,比如localhost:3000
  2. 上面两种方案,都存在一个致命的问题,仅对简单跨域请求有效,无法处理复杂的跨域请求。
    • 那么何为复杂的跨域请求?可以参考阮一峰的科普http://www.ruanyifeng.com/blog/2016/04/cors.html。
      比如我们常用的Post或Put请求,Content-Type字段的类型一般是application/json时,就是复杂请求。
    • 复杂请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错,而这次preflight的Http方法就是Options。换句话说,如果你的xhr请求发出前,会先发出一个Options请求,就说明你要执行的请求是复杂请求。
    • 对于复杂的跨域请求,如果连preflight都没有通过,何谈后续的跨域请求?!

三、增加对复杂请求的预检(Preflight,即Options请求)处理支持

asp.net的web层,Options请求是在哪里进行处理?到达控制器中的action时,已经是正式请求了,最终发现应该可以在Global.asax中,通过Application_BeginRequest方法进行处理。

protected override void Application_BeginRequest(object sender, EventArgs e)
{
    if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS")//拦截处理Options请求
    {
        string domain = Request.Headers.Get("Origin");
        //
        //这里可以对domain进行校验,即维护一个可跨域访问的列表,进行比对,校验通过后才执行下面的操作。本文中不做处理。
        //
        Response.Headers.Add("Access-Control-Allow-Origin", domain);
        Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,authorization");//authorization是我项目中要用到的,读者可以忽略
        Response.Flush();
        Response.End();
    }
    base.Application_BeginRequest(sender, e);
}

这样,我们对Options跨域请求进行了“可支持跨域”的应答。之后的正式请求到达控制器中的Action,又有相应的跨域访问处理。那么对于整个的复杂请求跨域就完成实现了。
但是,上文中我们提到,要实现的是验证码Session验证功能,那么就还涉及到Cookie跨域携带的问题,我们来做进一步的改造。

四、携带Cookie跨域

  1. 修改Global.aspx中的Application_BeginRequest方法,增加代码Response.Headers.Add("Access-Control-Allow-Credentials", "true");
protected override void Application_BeginRequest(object sender, EventArgs e)
{
        if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS")//拦截处理Options请求
        {
            string domain = Request.Headers.Get("Origin");
            //
            //这里可以对domain进行校验,即维护一个可跨域访问的列表,进行比对,校验通过后才执行下面的操作。本文中不做处理。
            //
            Response.Headers.Add("Access-Control-Allow-Origin", domain);
            Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,authorization");//authorization是我项目中要用到的,读者可以忽略
            Response.Headers.Add("Access-Control-Allow-Credentials", "true");//可携带Cookie
            Response.Flush();
            Response.End();
        }
        base.Application_BeginRequest(sender, e);
}
  1. 修改AllowCrossSiteJsonAttribute类的OnActionExecuting方法,
    增加代码filterContext.RequestContext.HttpContext.Response.AppendHeader("Access-Control-Allow-Credentials", "true");,另外需要注意filterContext.RequestContext.HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", host);,代码中host不能用*来代替,必须使用具体的host名称,这是跨域携带cookie的要求。
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
        var context = filterContext.RequestContext.HttpContext;
        var host = context.Request.Headers.Get("Origin");
        if (host != null&& _domains.Contains(host))
        {
            //域,带cookie请求必须明确指定host,不能使用*代替
            filterContext.RequestContext.HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", host);
            //Http方法
            filterContext.RequestContext.HttpContext.Response.AppendHeader("Access-Control-Allow-Methods", "*");
            //可携带cookie
            filterContext.RequestContext.HttpContext.Response.AppendHeader("Access-Control-Allow-Credentials", "true");
        }
        base.OnActionExecuting(filterContext);
}

至此,我们完成了在asp.net MVC中复杂请求携带cookie跨域的处理。下面给出前端的调用代码以供参考。
Ajax版本

    $.ajax({
        url: 'http://192.168.100.66:3006/OnlineMessage',
        type: 'post',
        xhrFields: {
            withCredentials: true
        },
        dataType: 'application/json; charset=utf-8',
        data: {
            "author": "1",
            "qq": "2",
            "phone": "3",
            "email": "4",
            "content": "留言",
            "checkCode": "一二三四"
        },
        complete: function (data) {
            alert(JSON.stringify(data));
        }
    });

Xhr版本

    function loadXMLDoc() {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "http://192.168.100.66:3006/OnlineMessage");
        xhr.setRequestHeader("Content-type", "application/json");
        xhr.withCredentials = true;
        xhr.send('{"author": "1","qq": "2","phone": "3","email": "4","content": "留言","checkCode": "一二三四"}');
    }
posted @ 2018-02-11 17:11  览岳  阅读(1284)  评论(0编辑  收藏  举报