WebApi系列~在WebApi中實現Cors訪問

Cors是個比較熱的技術,這在蔣金楠的博客裏也有體現,Cors簡單來說就是“跨域資源訪問”的意思,這種訪問我們指的是Ajax實現的異步訪問,形象點說就是,一個A網站公開一些接口方法,對於B網站和C網站可以通過發Xmlhttprequest請求來調用A網站的方法,對於xmlhttprequest封裝比較好的插件如jquery的$.ajax,它可以讓開發者很容易的編寫AJAX異步請求,無論是Get,Post,Put,Delete請求都可以發送。

Cors並不是什麼新的技術,它只是對HTTP請求頭進行了一個加工,還有我們的Cors架構裏,對jsonp也有封裝,讓開發者在使用jsonp訪問裏,編寫的代碼量更少,更直觀,呵呵。(Jsonp和Json沒什麼關係,它是從一個URI返回一個Script響應塊,所以,JSONP本身是和域名沒關係的,而傳統上的JSON是走xmlhttprequest的,它在默認情況下,是不能跨域訪問的)

做在後

一  下面先說一下,對jsonp的封裝

1 註冊jsonp類型,在global.asax裏Application_Start方法中

 GlobalConfiguration.Configuration.Formatters.Insert(0, new EntityFrameworks.Web.Core.JsonpMediaTypeFormatter());

2 編寫JsonpMediaTypeFormatter這個類型中實現了對jsonp請求的響應,並在響應流中添加指定信息,如callback方法名。

複製代碼
 /// <summary>
    /// 對jsonp響應流的封裝
    /// </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中註冊這個HttpHandler,使它對HTTP的處理進行二次加工,它可以有同步和異步兩個版本,本例中實現異步方式實現

 //對指定URI的網站進行跨域資源的共享
 GlobalConfiguration.Configuration.MessageHandlers.Add(new EntityFrameworks.Web.Core.Handlers.CorsMessageHandler());

下面是MessageHandlers原代碼,實現對HTTP請求的二次處理

複製代碼
    /// <summary>
    /// 跨域資源訪問的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);
            //根據HttpActionDescriptor得到應用的CorsAttribute特性
            CorsAttribute corsAttribute = actionDescriptor.GetCustomAttributes<CorsAttribute>().FirstOrDefault() ??
                controllerDescriptor.GetCustomAttributes<CorsAttribute>().FirstOrDefault();
            if (null == corsAttribute)
            {
                return base.SendAsync(request, cancellationToken);
            }
            //利用CorsAttribute實施授權並生成響應報頭
            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;
            }

            //添加響應報頭
            if (headers != null && headers.Any())
                foreach (var item in headers)
                    response.Headers.Add(item.Key, item.Value);

            return Task.FromResult<HttpResponseMessage>(response);
        }
    }
複製代碼

2 添加Cors特性,以便處理可以跨域訪問的域名,如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擴展方法 /// </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 下面是爲指定的API類型添加指定域名訪問的特性

    [CorsAttribute("http://localhost:11879/", "http://localhost:5008/")]/*需要加在類上*/
    public class ValuesController : ApiController
  {
    //代碼省略
  }

下面看一下實例的結果:

上圖中分別使用了jsonp和json兩種方法,看一下它們的響應結果

CORS實際上是在服務端的響應頭上添加的標準的Access-Control-Allow-Origin的信息,它是一種跨域資源訪問的標準

可以看到,jsonp實現上是一種遠程JS方法的調用,客戶端發起一個HTTP請求,這通過callback參數(一串隨機數)來區別多個客戶端,每個客戶端的請求callback都是不同的,它們由服務器端處理數據,再通過callback隨機數去爲指定客戶端返回數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章