...

пятница, 18 апреля 2014 г.

[Из песочницы] 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://ift.tt/1pfHouU:



Запрос:

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-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.

<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'
});


К сожалению, функциональность связанная с отправкой авторизационных данных стала доступна для IE только с 10 версии, браузеры IE8/9 не поддерживают отправку данной информации, и способны работать только с GET и POST.


Авторизация



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



This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий