WebApi系列~在WebApi中實現Cors訪(fang)問(wen)
說在前
Cors是(shi)個比較(jiao)熱的(de)技術,這(zhe)在(zai)蔣(jiang)金楠的(de)博客里也有體現(xian),Cors簡單來說(shuo)就是(shi)“跨域資源訪問”的(de)意(yi)思(si),這(zhe)種訪問我們指的(de)是(shi)Ajax實現(xian)的(de)異(yi)(yi)步訪問,形象(xiang)點說(shuo)就是(shi),一個A網(wang)站(zhan)公開一些接口方法,對于B網(wang)站(zhan)和C網(wang)站(zhan)可(ke)以通過發(fa)Xmlhttprequest請求(qiu)(qiu)來調用A網(wang)站(zhan)的(de)方法,對于xmlhttprequest封裝比較(jiao)好的(de)插(cha)件如jquery的(de)$.ajax,它可(ke)以讓開發(fa)者很容易(yi)的(de)編寫AJAX異(yi)(yi)步請求(qiu)(qiu),無論是(shi)Get,Post,Put,Delete請求(qiu)(qiu)都(dou)可(ke)以發(fa)送。
Cors并(bing)不(bu)是什么新的(de)技術,它(ta)只是對HTTP請(qing)求頭進行了一個加工(gong),還有(you)我們的(de)Cors架構(gou)里(li),對jsonp也有(you)封(feng)裝(zhuang),讓開發者在(zai)使用jsonp訪問里(li),編寫(xie)的(de)代碼量更少(shao),更直觀,呵呵。(Jsonp和(he)Json沒什么關系,它(ta)是從一個URI返(fan)回一個Script響應(ying)塊,所以,JSONP本身是和(he)域名沒關系的(de),而傳統上(shang)的(de)JSON是走xmlhttprequest的(de),它(ta)在(zai)默認情(qing)況下,是不(bu)能跨(kua)域訪問的(de))
做在后
一 下面先說一下,對jsonp的封裝
1 注冊jsonp類型,在global.asax里Application_Start方法中
GlobalConfiguration.Configuration.Formatters.Insert(0, new EntityFrameworks.Web.Core.JsonpMediaTypeFormatter());
2 編(bian)寫JsonpMediaTypeFormatter這(zhe)個(ge)類型中實現了對jsonp請求的(de)響(xiang)應,并在響(xiang)應流中添加指定信息,如callback方(fang)法名。
/// <summary> /// 對jsonp響應(ying)流的封裝 /// </summary> public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter { public string Callback { get; private set; } public JsonpMediaTypeFormatter(string callback = null) { this.Callback = callback; } public override Task WriteToStreamAsync( Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) { if (string.IsNullOrEmpty(this.Callback)) { return base.WriteToStreamAsync(type, value, writeStream, content, transportContext); } try { this.WriteToStream(type, value, writeStream, content); return Task.FromResult<AsyncVoid>(new AsyncVoid()); } catch (Exception exception) { TaskCompletionSource<AsyncVoid> source = new TaskCompletionSource<AsyncVoid>(); source.SetException(exception); return source.Task; } } private void WriteToStream( Type type, object value, Stream writeStream, HttpContent content) { JsonSerializer serializer = JsonSerializer.Create(this.SerializerSettings); using (StreamWriter streamWriter = new StreamWriter(writeStream, this.SupportedEncodings.First())) using (JsonTextWriter jsonTextWriter = new JsonTextWriter(streamWriter) { CloseOutput = false }) { jsonTextWriter.WriteRaw(this.Callback + "("); serializer.Serialize(jsonTextWriter, value); jsonTextWriter.WriteRaw(")"); } } public override MediaTypeFormatter GetPerRequestFormatterInstance( Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType) { if (request.Method != HttpMethod.Get) { return this; } string callback; if (request.GetQueryNameValuePairs().ToDictionary(pair => pair.Key, pair => pair.Value).TryGetValue("callback", out callback)) { return new JsonpMediaTypeFormatter(callback); } return this; } [StructLayout(LayoutKind.Sequential, Size = 1)] private struct AsyncVoid { } }
二 對指定域名實現友好的跨域資源訪問
1 在global.asax中注冊這個(ge)(ge)HttpHandler,使它對HTTP的處理進行二(er)次加工,它可以有同步和異(yi)步兩個(ge)(ge)版本,本例中實(shi)現異(yi)步方式實(shi)現
//對指(zhi)定URI的網站進行跨(kua)域資源的共享 GlobalConfiguration.Configuration.MessageHandlers.Add(new EntityFrameworks.Web.Core.Handlers.CorsMessageHandler());
下面是MessageHandlers原代(dai)碼,實現(xian)對HTTP請(qing)求的二(er)次處理(li)
/// <summary> /// 跨域資源(yuan)訪(fang)問的(de)HTTP處理程序 /// </summary> public class CorsMessageHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { //得到描述目標Action的HttpActionDescriptor HttpMethod originalMethod = request.Method; bool isPreflightRequest = request.IsPreflightRequest(); if (isPreflightRequest) { string method = request.Headers.GetValues("Access-Control-Request-Method").First(); request.Method = new HttpMethod(method); } HttpConfiguration configuration = request.GetConfiguration(); HttpControllerDescriptor controllerDescriptor = configuration.Services.GetHttpControllerSelector().SelectController(request); HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), request.GetRouteData(), request) { ControllerDescriptor = controllerDescriptor }; HttpActionDescriptor actionDescriptor = configuration.Services.GetActionSelector().SelectAction(controllerContext); //根據(ju)HttpActionDescriptor得到應用的CorsAttribute特性(xing) CorsAttribute corsAttribute = actionDescriptor.GetCustomAttributes<CorsAttribute>().FirstOrDefault() ?? controllerDescriptor.GetCustomAttributes<CorsAttribute>().FirstOrDefault(); if (null == corsAttribute) { return base.SendAsync(request, cancellationToken); } //利用(yong)CorsAttribute實施(shi)授(shou)權并生成響應報頭 IDictionary<string, string> headers; request.Method = originalMethod; bool authorized = corsAttribute.TryEvaluate(request, out headers); HttpResponseMessage response; if (isPreflightRequest) { if (authorized) { response = new HttpResponseMessage(HttpStatusCode.OK); } else { response = request.CreateErrorResponse(HttpStatusCode.BadRequest, corsAttribute.ErrorMessage); } } else { response = base.SendAsync(request, cancellationToken).Result; } //添(tian)加響(xiang)應報頭 if (headers != null && headers.Any()) foreach (var item in headers) response.Headers.Add(item.Key, item.Value); return Task.FromResult<HttpResponseMessage>(response); } }
2 添加Cors特性,以(yi)便處理可以(yi)跨域訪問(wen)的(de)域名,如(ru)B網站和C網站
/// <summary> /// Cors特性 /// </summary> [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public class CorsAttribute : Attribute { public Uri[] AllowOrigins { get; private set; } public string ErrorMessage { get; private set; } public CorsAttribute(params string[] allowOrigins) { this.AllowOrigins = (allowOrigins ?? new string[0]).Select(origin => new Uri(origin)).ToArray(); } public bool TryEvaluate(HttpRequestMessage request, out IDictionary<string, string> headers) { headers = null; string origin = null; try { origin = request.Headers.GetValues("Origin").FirstOrDefault(); } catch (Exception) { this.ErrorMessage = "Cross-origin request denied"; return false; } Uri originUri = new Uri(origin); if (this.AllowOrigins.Contains(originUri)) { headers = this.GenerateResponseHeaders(request); return true; } this.ErrorMessage = "Cross-origin request denied"; return false; } private IDictionary<string, string> GenerateResponseHeaders(HttpRequestMessage request) { //設置響應頭"Access-Control-Allow-Methods" string origin = request.Headers.GetValues("Origin").First(); Dictionary<string, string> headers = new Dictionary<string, string>(); headers.Add("Access-Control-Allow-Origin", origin); if (request.IsPreflightRequest()) { //設置響應頭"Access-Control-Request-Headers" //和"Access-Control-Allow-Headers" headers.Add("Access-Control-Allow-Methods", "*"); string requestHeaders = request.Headers.GetValues("Access-Control-Request-Headers").FirstOrDefault(); if (!string.IsNullOrEmpty(requestHeaders)) { headers.Add("Access-Control-Allow-Headers", requestHeaders); } } return headers; } } /// <summary> /// HttpRequestMessage擴(kuo)展方法 /// </summary> public static class HttpRequestMessageExtensions { public static bool IsPreflightRequest(this HttpRequestMessage request) { return request.Method == HttpMethod.Options && request.Headers.GetValues("Origin").Any() && request.Headers.GetValues("Access-Control-Request-Method").Any(); } }
3 下面是為指(zhi)定(ding)的API類型添(tian)加指(zhi)定(ding)域名訪問的特性(xing)
[CorsAttribute("//localhost:11879/", "//localhost:5008/")]/*需(xu)要(yao)加在類上*/ public class ValuesController : ApiController { //代碼(ma)省(sheng)略 }
下面看一下實例的結果:
上(shang)圖中分別使用(yong)了jsonp和json兩種方法,看一下它們的響應結果(guo)
CORS實際上(shang)是在服務端(duan)的(de)響(xiang)應頭上(shang)添(tian)加的(de)標(biao)準的(de)Access-Control-Allow-Origin的(de)信息,它是一(yi)種跨域(yu)資源訪問(wen)的(de)標(biao)準
可(ke)以看到,jsonp實現上是(shi)一(yi)種(zhong)遠程JS方法的(de)(de)調(diao)用,客(ke)戶(hu)端(duan)(duan)(duan)發起(qi)一(yi)個HTTP請求(qiu),這(zhe)通(tong)過callback參數(shu)(一(yi)串隨(sui)機數(shu))來(lai)區別多個客(ke)戶(hu)端(duan)(duan)(duan),每個客(ke)戶(hu)端(duan)(duan)(duan)的(de)(de)請求(qiu)callback都是(shi)不同的(de)(de),它(ta)們由服務器端(duan)(duan)(duan)處理(li)數(shu)據(ju),再通(tong)過callback隨(sui)機數(shu)去為指(zhi)定客(ke)戶(hu)端(duan)(duan)(duan)返回(hui)數(shu)據(ju)。
感謝您的閱讀!