之前的文章写了关于WebApi的跨域问题,当中的方法只是解决了简单请求的跨域问题而非简单请求的跨域问题则没有解决。
要弄清楚 CORS规范将哪些类型的跨域资源请求划分为简单请求的范畴,需要额外了解几个名称的含义,其中包括 “简单 (HTTP)方 法 (Simple Method) “、“简单(请求)报头 (Simple Hader)” 和 “ 自定义请求报头 (Author Request Header/ Custom Request Header)” 。
CORS规范将GET、HEAD、POST这三个HTTP方法视为“简单HTTP方法”,而将请求报头Accept、Accept-Language、Content-Language以及Content-Type采用Application/X-www-form-urlencoded、multipart/form-data、text/plain的报头称为简单请求报头。
简而言之,简单请求就是只包含简单请求报头的采用简单方法的Http请求,其他的请求即为费简单请求,如本文提到的跨域签名所需要的签名参数就是添加在HTTP请求的自定义头部里面(如果把签名信息包含在处理函数的参数里面就显得接口签名做的很LOW)。
跨域其实说的是浏览器的同源策略对JavaScript脚本的Ajax请求的一些限制,阻止程序对脚本请求的数据的操作,而不是阻止请求的发送以及数据的接收,如果用抓包工具查看网络数据的话,其实可以很明显的看到请求的发送,以及接口正常的数据返回。但是为什么浏览器不能正常的处理数据呢?这是因为浏览器需要得到资源提供者的授权之后才会把资源分发给消费者(即JavaScript脚本),而Ajax在进行跨域资源的请求的时候会在报头添加一个“Origin”头部,这个头部的值就是当前发起请求的域。因此想要解决简单请求的跨域问题只需要对请求报文中的Origin的值进行处理,对已授权的域的响应添加一个响应报头"Access-Control-Allow-Origin",一般对授权域的响应报头"Access-Control-Allow-Origin"值置为“*”。而非简单请求的跨域调用流程就跟简单报文的调用流程有所差异呢,在发送非简单报文时,浏览器就会采用“预检”机制来完成非简单请求的跨域资源请求。所谓预检机制就是浏览器在发送真正的跨域资源请求前 ,先发送一个预检请求(PreflightRequest)。 预检请求为一个采用 0PTIONS方法的请求,这是一个不包含主体的请求,用户凭证相关的报头也会被剔除。基于真正资源请求的一些辅助授权的信息会包含在此预检请求的响应报头中。 除了代表请求页面所在站点的 “Origin” 报头之外,如 下所示的是两个典型的请求报头 。
● Access-Control-Request-Method:跨域资源请求采用的HTTP方 法 。
● Access-Control-Request-Headers:跨域资源请求携带的自定义报头列表 。
资源的提供者在接收到预检请求之后会根据其提供的相关报头进行授权检验 ,具体的检验逻辑包括确定请求站点是否值得信任 ,以及请求采用HTTP方法和自定义报头是否被允许 。如果预检请求没有通过授权检验 ,资源提供者一般会返回一个状态为“400,Bad Reuqest”的响应(也可自定义返回报文消息,如本文) 。 反之则会返回一个状态为 “200,OK” 的响应(也可自定义返回报文消息) ,授权相关信息会包含在响应报头中。
除了上面介绍的 “Access-Control-Allow-Origin” 和 “Access-Control-Allow-Method” 报头之外,预检请求的响应还具有如下 3个典型的报头。
● Access-Control-Allow-Method:跨域资源请求允许采用 的 HTTP方法列表 。
● Access-Control-Allow-Headers:跨域资源请求允许携带的自定义报头列表 。
● Access-Control-Max-Age:浏览器可以将响应结果进行缓存的时间 (单位为秒 ),这样可以让浏览器避免频繁地发送预检请求 。
如果预检请求满足如下三个条件,浏览器则认为后续将要发送的跨域资源请求是被授权的
● 通过请求的 “Origin” 报头表示的源站点必须存在于 “Access-Control-Allow-Origin”响应报头标识的站点列表中。
●预检请求的 “Access-Control-Request-Headers” 报头存储的报头名称均在响应报头“Access-Control-Allow-Headers” 表示的报头列表之内。
●预检请求的“Access-Control-Request-Method” 报头表示的请求方法在预检请求响应报文“Access-Control-Allow-Methods”表示的列表之内。
因此以上可知:想要完成非简单报文的跨域请求,就必须要在服务器端对预检请求进行正常的应答,当收到预检请求时,对其进行正常的响应,这就需要在应答报文中添加“Access-Control-Allow-Origin”、“Access-Control-Allow-Methods”、“Access-Control-Allow-Headers”,这三个自定义报文头,同时这三个报文头要按照预检机制的要求进行填充。尤其是“Access-Control-Allow-Headers”自定义头部列表要包含签名所需的所有自定义头部名。
以上便是对跨域、预检机制以及其解决方法的描述,接下来讲实际的处理方法
(1)跨域支持:对控制器添加一个Filter属性并重写OnActionExecuted方法,向响应报文添加自定义头部。
(2)预检报文应答:因为一般的API控制器都没有实现OPTIONS方法,而预检报文的请求方法是OPTIONS,因此需要我们实现OPTION方法。至于每个控制器多了一个平时都不使用的OPTIONS方法,写在那里难看,这就是系统架构要考虑的问题了,这里我们只说具体问题的具体解决方法。
(3)接口签名验证:对控制器添加一个Filter属性对自定义报文进行自己的验证逻辑,这个就随意发挥了。
跨域的预检报文支持代码:
/// <summary> /// 添加跨域支持 /// </summary> public class EnableCors : ActionFilterAttribute { /// <summary> /// 操作标记 /// </summary> private bool Flag { get; set; } /// <summary> /// 默认构造函数 true:开启跨域 false:关闭跨域支持 /// </summary> /// <param name="para"></param> public EnableCors(bool para) { Flag = para; } /// <summary> /// 方法执行之后执行 /// </summary> /// <param name="actionExecutedContext"></param> public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { base.OnActionExecuted(actionExecutedContext); if (Flag != true) return; if (actionExecutedContext.Response == null) return; if (actionExecutedContext.Response.Headers.Contains("Access-Control-Allow-Origin")) { actionExecutedContext.Response.Headers.Remove("Access-Control-Allow-Origin"); } if (actionExecutedContext.Response.Headers.Contains("Access-Control-Allow-Method")) { actionExecutedContext.Response.Headers.Remove("Access-Control-Allow-Method"); } if (actionExecutedContext.Response.Headers.Contains("Access-Control-Allow-Headers")) { actionExecutedContext.Response.Headers.Remove("Access-Control-Allow-Headers"); } actionExecutedContext.Response.Headers.Add("Access-Control-Allow-Origin", "*"); actionExecutedContext.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); actionExecutedContext.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,TimeStamp,Parameter,RandNum"); } }
接口签名代码:
/// <summary> /// 接口签名属性 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class ApiAuthorization : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { base.OnAuthorization(actionContext); if (actionContext.Request.Method == HttpMethod.Options) return;//支持跨域的自定义报头请求(预检机制) //TODO这里就是接口签名的代码,可以取自定义头部进行处理,签名通过则直接return,否则对actionContext.Response进行赋值,表示签名失败后面的操作不执行 actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden, new CheckResult() { Result = false, Message = "Access denied!" }); } }
/// <summary> /// 基础控制器 /// </summary> [DataType(ApiDataType.Json)] [EnableCors(true)] [Description("Api基础控制器")] [ApiAuthorization] public class ApiBaseController : ApiController { /// <summary> /// 为了支持ajax跨域的预检机制 /// </summary> public void Options() { } }
通过以上步骤就完成了跨域的预检报文应答,接口签名。实际运行效果如下图所示:
首先我发送一个非简单报文的操作,我通过Fiddler实际抓到了两个包,第一个就是上面所说的预检报文,第二个才是包含我们所有自定义报文头部的实现我们真正跨域资源请求的报文。
而预检请求报文以及应答报文的信息如图(格式也如上文所讲):
跨域资源请求报文及其应答报文如下(注意我们用于接口签名的自定义报文头):
下面来一个正常通过跨域操作,并且签名验证失败的报文,同样是一个预检报文,一个正式报文