Добрый день!
Хотелось бы продемонстрировать один из возможных подходов к решению проблемы работы с WCF сервисами с различных доменов. Найденная мной информация по данной теме была или неполной, или содержала избыточное количество информации, затрудняющей понимание. Хочу рассказать о нескольких способах взаимодействия WCF и AJAX POST запросов, включающих в себя информацию о Cookies и авторизации.
Как известно, просто так AJAX вызов на другой домен не заработает, в силу соображений безопасности. Для решения данной проблемы был придуман и релизован стандарт CORS(wiki, mozilla). Этот стандарт подразумевает использование специфичных HTTP заголовков для разрешения и ограничения доступа. Упрощенный процесс коммуникации с использованием данного протокола подразумевает следующее:
Клиент(браузер) инициирует подключение с HTTP заголовком
В общем случае ограничения накладывает браузер. Если ему что-то не понравится в заголовках, он не отдаст эти данные пользователю(если не вернется необходимый
По данной теме существует определенное количество информации разнообразного качества. К сожалению, WCF не позволяет стандартными средствами использовать эти заголовки, однако существует несколько вариантов решения этой пробемы. Я предлагаю вашему вниманию некоторые из них.
Данное решение подразумевает добавление необходимых заголовков прямо в web.config.
Отличается своей простотой и негибкостью. В частности, конкретно данный пример невозможно использовать, если возможных доменов более одного, кроме того он разрешает CORS на весь сайт(в конкретном случае).
Данное решение подразумевает написание в Global.asax.cs кода, добавляющего необходимые заголовки в каждый запрос.
Это решение поддерживает несколько доменов, но распространяется на весь сайт. Безусловно, все условия на конкретные сервисы можно прописать тут же, но на мой взгляд это сопряжено с неудобствами в поддержке списка разрешенных сервисов.
Данное решение отличается от предыдущего лишь тем, что заголовки добавляются для конкретного сервиса или метода. В общем случа решение выглядит так:
Данный подход позволяет ограничить использование CORS в рамках сервиса или даже метода. Основной минус — вызов
Данный подход использует возможности WCF по расширение функциональности.
Создаются 2 класса
и
Добавляем в
Находим и добавляем созданное для
Нам осталось только обработать предварительный запрос с методом
Разумеется, существует и аналогичное WCF расширение для работы с preflight запросами, об одном из них можно будет прочитать по ссылке из списка литературы в конце статьи. Основной минус — необходимость добавления метода
Для поддержки отправки авторизационных и Cookie данных, необходимо, чтобы в XmlHttpRequest был выставлен в
К сожалению, функциональность связанная с отправкой авторизационных данных стала доступна для IE только с 10 версии, браузеры IE8/9 не поддерживают отправку данной информации, и способны работать только с GET и POST.
Во всех подходах выше неявно используется аутентификационные данные с основного сайта. Имея авторизацию на основном сайте
Хотелось бы продемонстрировать один из возможных подходов к решению проблемы работы с WCF сервисами с различных доменов. Найденная мной информация по данной теме была или неполной, или содержала избыточное количество информации, затрудняющей понимание. Хочу рассказать о нескольких способах взаимодействия WCF и AJAX POST запросов, включающих в себя информацию о Cookies и авторизации.
Как известно, просто так AJAX вызов на другой домен не заработает, в силу соображений безопасности. Для решения данной проблемы был придуман и релизован стандарт CORS(wiki, mozilla). Этот стандарт подразумевает использование специфичных HTTP заголовков для разрешения и ограничения доступа. Упрощенный процесс коммуникации с использованием данного протокола подразумевает следующее:
Клиент(браузер) инициирует подключение с HTTP заголовком
Origin
, сервер должен ответить используя заголовок Access-Control-Allow-Origin
. Пример пары запрос/ответ с адреса foo.example
на сервис bar.other/resources/public-data
:Запрос:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
Origin: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
— данный заголовок определяет, с каких ресурсов могут приходить запросы. Может использоваться*
или конкретный домен, например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
расширение в конфигурацию Behavior
нашего 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>
...
</behaviors>
...
</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.
Авторизация
Во всех подходах выше неявно используется аутентификационные данные с основного сайта. Имея авторизацию на основном сайте
bar.other
, мы имеем возможность вернуть данные пользователя по Ajax запросу с сайта 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