Хотелось бы продемонстрировать один из возможных подходов к решению проблемы работы с WCF сервисами с различных доменов. Найденная мной информация по данной теме была или неполной, или содержала избыточное количество информации, затрудняющей понимание. Хочу рассказать о несколько способах взаимодействия WCF и AJAX POST запросов, включающих в себя информацию о Cookies и авторизации.
Как известно, просто так AJAX вызов на другой домен не заработает, в силу соображений безопасности. Для решения данной проблемы был придуман и релизован стандарт CORS(wiki, mozilla). Этот стандарт подразумевает использование специфичных HTTP заголовков для разрешения и ограничения доступа. Упрощенный процесс коммуникации с использованием данного протокола подразумевает следующее:
Клиент(браузер) инициирует подключение с HTTP заголовком Origin, сервер должен ответить используя заголовок Access-Control-Allow-Origin. Пример пары запрос/ответ с адреса http://foo.example на сервис http://bar.other/resources/public-data/:
Запрос:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
Origin:http://foo.example
[Другие заголовки]Ответ:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Access-Control-Allow-Origin: *
Content-Type: application/xml[XML Data]
Заголовки
Access-Control-Allow-Origin— данный заголовок определяет, с каких ресурсов могут приходить запросы. Может использоваться*или конкретный домен, напримерhttp://foo.example. Данный заголовок может быть только один, и может содержать только одно значение, т.е. список доменов задать нельзя.Access-Control-Allow-Headers— этот заголовок определяет, какие методы могут использоваться для общения с сервером. Ограничимся следующими:POST,GET,OPTIONS, но так же можно использовать иPUT, иDELETE, и другие.Access-Control-Allow-Headers— этот заголовок определяет список доступных заголовков. НапримерContent-Type, который позволит задать тип ответаapplication/json.Access-Control-Allow-Credentials— этот заголовок определяет, разрешается ли передавать Cookie и Authorization заголовки. Возможные значенияtrueиfalse. Важно: данные будут передаваться, только если в заголовкеAccess-Control-Allow-Originбудет явно выставлен конкретный домен, если использовать*— заголовок будет проигнорирован.
В общем случае ограничения накладывает браузер. Если ему что-то не понравится в заголовках, он не отдаст эти данные пользователю(если не вернется необходимый Access-Control-Allow-Headers, или серверу, если не будет указан Access-Control-Allow-Credentials и правильный Access-Control-Allow-Origin. Перед POST запросом на другой домен, браузер предварительно сделает OPTIONS запрос(preflight request) для получения информации о разрешенных методах работы с сервисом.
WCF
По данной теме существует определенное количество информации разнообразного качества. К сожалению, WCF не позволяет стандартными средствами использовать эти заголовки, однако существует несколько вариантов решения этой пробемы. Я предлагаю вашему вниманию некоторые из них.
Решение с использованием web.config.
Данное решение подразумевает добавление необходимых заголовков прямо в web.config.
<system.webServer> <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="http://foo.example" /> <add name="Access-Control-Allow-Headers" value="Content-Type" /> <add name="Access-Control-Allow-Methods" value="POST, GET, OPTIONS" /> <add name="Access-Control-Allow-Credentials" value="true" /> </customHeaders> </httpProtocol> </system.webServer>
Отличается своей простотой и негибкостью. В частности, конкретно данный пример невозможно использовать, если возможных доменов более одного, кроме того он разрешает CORS на весь сайт(в конкретном случае).
Решение с использованием Global.asax
Данное решение подразумевает написание в Global.asax.cs кода, добавляющего необходимые заголовки в каждый запрос.
protected void Application_BeginRequest(object sender, EventArgs e) { var allowedOrigins = new [] { "http://foo.example", "http://bar.example" }; var request = HttpContext.Current.Request; var response = HttpContext.Current.Response; var origin = request.Headers["Origin"]; if (origin != null && allowedOrigins.Any(x => x == origin)) { response.AddHeader("Access-Control-Allow-Origin", origin); response.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.AddHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With"); response.AddHeader("Access-Control-Allow-Credentials", "true"); if (request.HttpMethod == "OPTIONS") { response.End(); } } }
Это решение поддерживает несколько доменов, но распространяется на весь сайт. Безусловно, все условия на конкретные сервисы можно прописать тут же, но на мой взгляд это сопряжено с неудобствами в поддержке списка разрешенных сервисов.
Решение с добавлением заголовков в коде WCF сервиса
Данное решение отличается от предыдущего лишь тем, что заголовки добавляются для конкретного сервиса или метода. В общем случа решение выглядит так:
[ServiceContract] public class MyService { [OperationContract] [WebInvoke(Method = "POST", ...)] public string DoStuff() { AddCorsHeaders(); return "<Data>"; } private void AddCorsHeaders() { var allowedOrigins = new [] { "http://foo.example", "http://bar.example" }; var request = WebOperationContext.Current.IncomingRequest; var response = WebOperationContext.Current.OutgoingResponse; var origin = request.Headers["Origin"]; if (origin != null && allowedOrigins.Any(x => x == origin)) { response.AddHeader("Access-Control-Allow-Origin", origin); response.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.AddHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With"); response.AddHeader("Access-Control-Allow-Credentials", "true"); if (request.HttpMethod == "OPTIONS") { response.End(); } } } }
Данный подход позволяет ограничить использование CORS в рамках сервиса или даже метода. Основной минус — вызов AddCorsHeaders необходим в каждом методе сервиса. Плюс — простота использования.
Решение с использованием собственных EndPointBehavior и DispatchMessageInspector
Данный подход использует возможности WCF по расширение функциональности.
Создаются 2 класса EnableCorsBehavior:
using System; using System.ServiceModel.Channels; using System.ServiceModel.Configuration; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; namespace My.Web.Cors { public class EnableCorsBehavior : BehaviorExtensionElement, IEndpointBehavior { public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new EnableCorsMessageInspector()); } public void Validate(ServiceEndpoint endpoint) { } public override Type BehaviorType { get { return typeof(EnableCorsBehavior); } } protected override object CreateBehavior() { return new EnableCorsBehavior(); } } }
и EnableCorsMessageInspector:
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Dispatcher; namespace My.Web.Cors { public class EnableCorsMessageInspector : IDispatchMessageInspector { public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) { var allowedOrigins = new [] { "http://foo.example", "http://bar.example" }; var httpProp = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name]; if (httpProp != null) { string origin = httpProp.Headers["Origin"]; if (origin != null && allowedOrigins.Any(x => x == origin)) { return origin; } } return null; } public void BeforeSendReply(ref Message reply, object correlationState) { string origin = correlationState as string; if (origin != null) { HttpResponseMessageProperty httpProp = null; if (reply.Properties.ContainsKey(HttpResponseMessageProperty.Name)) { httpProp = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name]; } else { httpProp = new HttpResponseMessageProperty(); reply.Properties.Add(HttpResponseMessageProperty.Name, httpProp); } httpProp.Headers.Add("Access-Control-Allow-Origin", origin); httpProp.Headers.Add("Access-Control-Allow-Credentials", "true"); httpProp.Headers.Add("Access-Control-Request-Method", "POST,GET,OPTIONS"); httpProp.Headers.Add("Access-Control-Allow-Headers", "X-Requested-With,Content-Type"); } } } }
Добавляем в web.config созданный EnableCorsBehavior:
<system.serviceModel> ... <extensions> <behaviorExtensions> <add name="crossOriginResourceSharingBehavior" type="My.Web.Cors.EnableCorsBehavior, My.Web, Version=1.0.0.0, Culture=neutral" /> </behaviorExtensions> </extensions> ... </system.serviceModel>
Находим и добавляем созданное для EnableCorsBehavior расширение в конфигурацию Behaviour нашего Endpoint‘a
<system.serviceModel> <services> <service name="My.Web.Services.MyService"> <endpoint address="" behaviorConfiguration="My.Web.Services.MyService" binding="webHttpBinding" contract="My.Web.Services.MyService" /> </service> </services> ... <behaviors> ... <endpointBehaviors> ... <behavior name="My.Web.Services.MyService"> <webHttp/> <crossOriginResourceSharingBehavior /> <!-- нужно добавить эту строчку --> </behavior> ... </endpointBehaviors> ... </behaviours> ... </system.serviceModel>
Нам осталось только обработать предварительный запрос с методом OPTIONS. В моем случае я использовал самый простой вариант: в теле сервиса добавляется метод-обработчик OPTIONS запросов.
[OperationContract] [WebInvoke(Method = "OPTIONS", UriTemplate = "*")] public void GetOptions() { // Заголовки обработаются в EnableCorsMessageInspector }
Разумеется, существует и аналогичное WCF расширение для работы с preflight запросами, об одном из них можно будет прочитать по ссылке из списка литературы в конце статьи. Основной минус — необходимость добавления метода GetOptions в тело сервиса и немалое количество дополнительного кода. С другой стороны, данный подход позволяет практически полностью разделить логику сервиса и логику коммуникации.
Пара слов о Javascript и браузерах
Для поддержки отправки авторизационных и Cookie данных, необходимо, чтобы в XmlHttpRequest был выставлен в true флаг withCredentials. Думаю, что многие используют jQuery для работы c AJAX, поэтому приведу пример для него:
$.ajax({ type: 'POST', cache: false, dataType: 'json', xhrFields: { withCredentials: true }, contentType: 'application/json; charset=utf-8', url: options.serviceUrl + '/DoStuff' });
К сожалению, функциональность связанная с отправкой авторизационных данных стала доступна только в IE10, браузеры IE8/9 не поддерживают отправку данной информации, и способны работать только с GET и POST.
Авторизация
Во всех подходах выше неявно используется аутентификационные данные с основного сайта. Имея авторизацию на основном сайте http://bar.other, мы имеем возможность вернуть данные пользователя по Ajax запросу с сайта http://foo.example. В моём случае это использовалось для того, чтобы дать возможность пользователю получать уведомления и реагировать на события, находясь на одном из сайтов, живущих в рамках одного бизнес проекта, но расположенных на разных доменах и платформах. Как уже было сказано выше, ключевыми моментами тут являются заголовок Access-Control-Allow-Credentials и выставления для XmlHttpRequest флага withCredentials=true.
Список источников
- code.msdn.microsoft.com/windowsdesktop/Implementing-CORS-support-c1f9cd4b — пример полной имплементации MessageInspector для всех запросов и статья к ней
- enable-cors.org/server_wcf.html — достаточно короткий мануал о написании собственного Behavior и MessageInspector, не учитывает особенности OPTIONS запросов, и не позволяет использовать проверку/выбор домена Origin
- developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS#Access-Control-Allow-Headers — один из наиболее полных источников информации по CORS
ссылка на оригинал статьи http://habrahabr.ru/post/219895/
Добавить комментарий