[Из песочницы] WCF + Cross Domain Ajax Calls (CORS) + Авторизация

Добрый день! Хотелось бы продемонстрировать один из возможных подходов к решению проблемы работы с 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.1Host: bar.otherOrigin: http://foo.example[Другие заголовки]

Ответ: HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 00:23:53 GMTAccess-Control-Allow-Origin: *Content-Type: application/xml

[XML Data]

Заголовки Access-Control-Allow-Origin — данный заголовок определяет, с каких ресурсов могут приходить запросы. Может использоваться * или конкретный домен, например http://foo.example. Данный заголовок может быть только один, и может содержать только одно значение, т.е. список доменов задать нельзя. Access-Control-Allow-Methods — этот заголовок определяет, какие методы могут использоваться для общения с сервером. Ограничимся следующими: 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. Отличается своей простотой и негибкостью. В частности, конкретно данный пример невозможно использовать, если возможных доменов более одного, кроме того он разрешает 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 »»; }

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: Находим и добавляем созданное для EnableCorsBehavior расширение в конфигурацию Behaviour нашего Endpoint’a … … Нам осталось только обработать предварительный запрос с методом 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' }); К сожалению, функциональность связанная с отправкой авторизационных данных стала доступна для IE только с 10 версии, браузеры IE8/9 не поддерживают отправку данной информации, и способны работать только с GET и POST.

Авторизация Во всех подходах выше неявно используется аутентификационные данные с основного сайта. Имея авторизацию на основном сайте http://bar.other, мы имеем возможность вернуть данные пользователя по Ajax запросу с сайта http://foo.example. В моём случае это использовалось для того, чтобы дать возможность пользователю получать уведомления и реагировать на события, находясь на одном из сайтов, живущих в рамках одного бизнес проекта, но расположенных на разных доменах и платформах. Как уже было сказано выше, ключевыми моментами тут являются заголовок Access-Control-Allow-Credentials и выставления для XmlHttpRequest флага withCredentials=true.Список источников

© Habrahabr.ru