C#后端=>

一、前言
最近在做前后端分离时需要处理跨域问题,遇到的坑在此与大家进行分享,避免多走些弯路。

二、CORS
CORS,常被大家称之为跨越问题,准确的叫法是跨域资源共享(CORS,Cross-origin resource sharing),是W3C标准,是一种机制,它使用额外的HTTP头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
即:请求服务器不同端口的另一个资源,出于安全原因,浏览器限制发起的跨源HTTP请求。

三、解决办法
理跨域问题,必定围绕Access-Control-Allow-Origin来处理。

方案一(交给后端处理)
1、简单请求跨域
处理简单请坟跨域是最简单的,利用WebApi过滤器/拦截器,作用是添加响应头信息
代码如下:

public class ActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext filter)
    {
        //简单请求和复杂请求的统一跨域处理
        filter.Response.Headers.Add("Access-Control-Allow-Origin", HttpHelper.SetAllowOrigin());//HttpHelper.SetAllowOrigin()是动态设置域名,也可以设置"*"表示所有域名
        filter.Response.Headers.Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type");
        filter.Response.Headers.Add("Access-Control-Allow-Methods", "PUT,GET,POST,DELETE,OPTIONS");
        base.OnActionExecuted(filter);
    }
}

简单说明以上代码中出现的HttpHelper.SetAllowOrigin(),作用为动态设置域名,Origin在web.config中配置,多个域名用英文逗号分隔,允许所有域名跨域用"*"
代码如下:

public class HttpHelper
{
    //Access-Control-Allow-Origin设置多个域名
    private static readonly string Origin = ConfigurationManager.AppSettings["Origin"];

    /// <summary>
    /// CORS跨域处理
    /// </summary>
    /// <returns></returns>
    public static string SetAllowOrigin()
    {
        //获取从 HTTP 请求头传过来的 Origin 字段,然后在程序中验证它的值是否合法,并且做出适当的响应
        string requestOrigin = HttpContext.Current.Request.Headers["Origin"];//前端传过来的Origin
        if (Origin == "*") { return Origin; }
        else if (!string.IsNullOrEmpty(Origin) && !string.IsNullOrEmpty(requestOrigin) && Origin.Split(',').Where(s => requestOrigin.Contains(s)).Any()) { return requestOrigin; }
        else { return string.Empty; }
    }
}

2、复杂请求跨域
复杂请求跨域是在简单请求跨域的基础上多了个OPTIONS请求,然而就是这个浏览器发出OPTIONS嗅探比较坑;
处理OPTIONS嗅探的正确姿势:
①在Global的Application_BeginRequest中处理OPTIONS,判断是OPTIONS请求直接放行不做任何处理
代码如下:

protected void Application_BeginRequest()
{
   //CORS跨域复杂请求的OPTIONS嗅探处理
   if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS")
    {
         //当请求方式是OPTIONS时  ①设置域名允许跨域  ②不返回数据,直接放行
         Response.Headers.Add("Access-Control-Allow-Origin", HttpHelper.SetAllowOrigin());
         Response.Headers.Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type");
         Response.End();
    }
}

②重复(1、简单请求跨域)的操作,此处不再过多说明
③后端同学如果没有强迫症的话跨域处理可以到此为止(瑕疵:复杂请求会调两次接口,先是OPTIONS预检待服务端同意跨域后才真正的提交数据请求接口);有强迫症的同学建议让前端在请求的时候设置一下请求头headers:{'Content-Type':'application/x-www-form-urlencoded'}}或者序列化一下参数QS.stringify(data),设置完后其实也就不会触发CORS的预检请求(preflight);"预检",从而少了后端Global的Application_BeginRequest中处理OPTIONS这一步

以上操作完毕还未能跨域的请检查代码或web.config配置是否正确,例如WebDav未关闭会阻止OPTIONS请求而导致CORS失败
web.config配置如下:

<system.webServer>


<!--<httpProtocol>
  <customHeaders>
    <add name="Access-Control-Allow-Origin" value="*" />
    <add name="Access-Control-Allow-Headers" value="Origin, X-Requested-With, Content-Type" />
    <add name="Access-Control-Allow-Methods" value="PUT,GET,POST,DELETE,OPTIONS"/>
  </customHeaders>
</httpProtocol>

--><!--多域名设置--><!--
<rewrite>
  <outboundRules>
    <rule name="AddCrossDomain">
      <match serverVariable="RESPONSE_Access_Control_Allow_Origin" pattern=".*" />
      <conditions logicalGrouping="MatchAll" trackAllCaptures="true">
        <add input="{HTTP_ORIGIN}" pattern="(http(s)?://((.+\.)?127.0.0.1:8010))" />
      </conditions>
      <action type="Rewrite" value="{C:0}" />
    </rule>
  </outboundRules>
</rewrite>-->

<modules runAllManagedModulesForAllRequests="true">
  <remove name="WebDavModule" />
</modules>
<handlers>
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
  <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
  <remove name="WebDav" />
  <remove name="OPTIONSVerbHandler" />
  <add name="OPTIONS" path="*" verb="OPTIONS" modules="ProtocolSupportModule" resourceType="Unspecified" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers></system.webServer>

最后再整理一下相关知识点
出现发送optiosn请求的原因
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。

  1. 请求方法是以下三种方法之一:
  • HEAD
  • GET
  • POST
  1. HTTP的头信息不超出以下几种字段:
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。
当请求存在跨域资源共享(CORS)并且是非简单请求,就会触发CORS的预检请求(preflight);"预检"请求用的请求方法是OPTIONS。实际开发过程中,后台采用token检验机制,前台发送请求必须将token放到Request Header中,那么就需要传输自定义Header信息、或则请求头中的Content-Type="application/json",就会形成非简单请求。