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