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

Список источников

ссылка на оригинал статьи http://habrahabr.ru/post/219895/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *