Служба WCF с wsHttpBinding - управление заголовками HTTP-запросов - PullRequest
4 голосов
/ 02 февраля 2012

Я следовал этому руководству, чтобы получить аутентификацию имени пользователя с защитой транспорта, работающей в моей службе WCF.Учебное пособие, однако, ссылается на использование basicHttpBinding, что недопустимо - мне требуется wsHttpBinding.

Идея состоит в том, чтобы использовать пользовательский BasicAuthenticationModule для службы WCF, который будет считывать заголовок «Авторизация» из запроса HTTPи выполнить процесс аутентификации в соответствии с содержимым заголовка «Авторизация».Проблема в том, что отсутствует заголовок «Авторизация»!

Я реализовал IClientMessageInspector с помощью пользовательского поведения для манипулирования исходящими сообщениями и добавления пользовательских заголовков SOAP.Я добавил следующий код в функцию BeforeSendRequest:

    HttpRequestMessageProperty httpRequest = request.Properties.Where(x => x.Key == "httpRequest").Single().Value;
    httpRequest.Headers.Add("CustomHeader", "CustomValue");

Это должно работать и, согласно многим веб-ресурсам, оно работает для basicHttpBinding, но не wsHttpBinding.Когда я говорю «работает», я имею в виду, что заголовок успешно получен службой WCF.

Это упрощенная функция, которая проверяет полученное сообщение HTTP на стороне службы WCF:

    public void OnAuthenticateRequest(object source, EventArgs eventArgs)
    {
        HttpApplication app = (HttpApplication)source;

        //the Authorization header is checked if present
        string authHeader = app.Request.Headers["Authorization"];
        if (string.IsNullOrEmpty(authHeader))
        {
            app.Response.StatusCode = 401;
            app.Response.End();
        }
    }

В нижних записях этой ветки от сентября 2011 года говорится, что это невозможно с wsHttpBinding.Я не хочу принимать этот ответ.

В качестве примечания, если я использую модуль базовой аутентификации, встроенный в IIS, а не пользовательский, я получаю

параметр'username' не должен содержать запятых. ** сообщение об ошибке при попытке Roles.IsInRole("RoleName") или `[PrincipalPermission (SecurityAction.Demand, Role =" RoleName ")]

, вероятно, потому что мое свойство PrimaryIdentity.Name содержитимя субъекта сертификата, поскольку я использую TransportWithMessageCredential безопасность с защитой сообщений на основе сертификатов.

Я открыт для предложений, а также альтернативных подходов к проблеме.Спасибо.

ОБНОВЛЕНИЕ

Как представляется, заголовок HTTP читается правильно позже во всем коде службы WCF.(HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties["httpRequest"] содержит мой пользовательский заголовок.Однако это уже уровень сообщений. Как передать заголовок в процедуру проверки подлинности транспорта?

ОБНОВЛЕНИЕ 2
После небольшого исследования,Я пришел к выводу, что когда веб-браузер получает код состояния HTTP 401, он предоставляет мне диалоговое окно входа в систему, где я могу указать свои учетные данные.Однако клиент WCF просто выдает исключение и не хочет отправлять учетные данные.Мне удалось проверить это поведение при посещении https://myserver/myservice/service.svc в Internet Explorer.Пытался исправить, используя информацию из этой ссылки, но безрезультатно.Это ошибка в WCF или я что-то упустил?

EDIT

Здесь приведены соответствующие разделы из моего system.servicemodel (из * 1066)*) - Я почти уверен, что все правильно настроил.

  <serviceBehaviors>
    <behavior name="ServiceBehavior">
      <serviceMetadata httpsGetEnabled="true" httpGetEnabled="false" />
      <serviceDebug includeExceptionDetailInFaults="true" />
      <serviceCredentials>
        <clientCertificate>
          <authentication certificateValidationMode="ChainTrust" revocationMode="NoCheck" />
        </clientCertificate>
        <serviceCertificate findValue="server.uprava.djurkovic-co.me" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="My" />
      </serviceCredentials>
      <serviceAuthorization principalPermissionMode="UseAspNetRoles" roleProviderName="AspNetSqlRoleProvider" />
    </behavior>
  </serviceBehaviors>
    ................
  <wsHttpBinding>
    <binding name="EndPointWSHTTP" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="20480000" maxReceivedMessageSize="20480000" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
      <readerQuotas maxDepth="20480000" maxStringContentLength="20480000" maxArrayLength="20480000" maxBytesPerRead="20480000" maxNameTableCharCount="20480000" />
      <reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false" />
      <security mode="TransportWithMessageCredential">
        <transport clientCredentialType="Basic" />
        <message clientCredentialType="Certificate" negotiateServiceCredential="true" algorithmSuite="Default" />
      </security>
    </binding>
  </wsHttpBinding>
    ............
  <service behaviorConfiguration="ServiceBehavior" name="DjurkovicService.Djurkovic">
    <endpoint address="" binding="wsHttpBinding" bindingConfiguration="EndPointWSHTTP" name="EndPointWSHTTP" contract="DjurkovicService.IDjurkovic" />
  </service>

Исключение, возвращаемое службой:

HTTP-запрос не авторизован с помощью схемы аутентификации клиента 'Anonymous'.Заголовок аутентификации, полученный от сервера, был «Базовая область, согласование, NTLM».(Удаленный сервер возвратил ошибку: (401) не авторизован.)

Ответы [ 2 ]

2 голосов
/ 03 февраля 2012

Интересно, что когда я писал последний комментарий относительно ответа выше, я остановился на мгновение.Мой комментарий содержал «... Если заголовок HTTP не содержит заголовка« Авторизация », я устанавливаю статус 401, что вызывает исключение». Я установил статус на 401. Понял?Решение было всегда.

Исходный пакет не содержит заголовка авторизации, даже если я его явно добавляю.Однако, каждый последующий пакет содержит его, как я тестировал, когда модуль авторизации неактивен.Итак, почему я не пытаюсь отличить этот исходный пакет от других?Поэтому, если я вижу, что это начальный пакет, установите код состояния HTTP на 200 (ОК), а если нет - проверьте заголовок аутентификации.Это было легко, поскольку исходный пакет отправляет запрос на токен безопасности в конверте SOAP (содержит теги <t:RequestSecurityToken>).

Хорошо, давайте посмотрим на мою реализацию, на случай, если она понадобится кому-то еще.

Это реализация BasicAuthenticationModule, которая реализует IHTTPModule:

public class UserAuthenticator : IHttpModule
{
    public void Dispose()
    {
    }

    public void Init(HttpApplication application)
    {
        application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest);
        application.EndRequest += new EventHandler(this.OnEndRequest);
    }

    public void OnAuthenticateRequest(object source, EventArgs eventArgs)
    {
        HttpApplication app = (HttpApplication)source;

        // Get the request stream
        Stream httpStream = app.Request.InputStream;

        // I converted the stream to string so I can search for a known substring
        byte[] byteStream = new byte[httpStream.Length];
        httpStream.Read(byteStream, 0, (int)httpStream.Length);
        string strRequest = Encoding.ASCII.GetString(byteStream);

        // This is the end of the initial SOAP envelope
        // Not sure if the fastest way to do this but works fine
        int idx = strRequest.IndexOf("</t:RequestSecurityToken></s:Body></s:Envelope>", 0);
        httpStream.Seek(0, SeekOrigin.Begin);
        if (idx != -1)
        {
            // Initial packet found, do nothing (HTTP status code is set to 200)
            return;
        }

        //the Authorization header is checked if present
        string authHeader = app.Request.Headers["Authorization"];
        if (!string.IsNullOrEmpty(authHeader))
        {
            if (authHeader == null || authHeader.Length == 0)
            {
                // No credentials; anonymous request
                return;
            }

            authHeader = authHeader.Trim();
            if (authHeader.IndexOf("Basic", 0) != 0)
            {
                // the header doesn't contain basic authorization token
                // we will pass it along and
                // assume someone else will handle it
                return;
            }

            string encodedCredentials = authHeader.Substring(6);

            byte[] decodedBytes = Convert.FromBase64String(encodedCredentials);
            string s = new ASCIIEncoding().GetString(decodedBytes);

            string[] userPass = s.Split(new char[] { ':' });
            string username = userPass[0];
            string password = userPass[1];
            // the user is validated against the SqlMemberShipProvider
            // If it is validated then the roles are retrieved from 
            // the role provider and a generic principal is created
            // the generic principal is assigned to the user context
            // of the application

            if (Membership.ValidateUser(username, password))
            {
                string[] roles = Roles.GetRolesForUser(username);
                app.Context.User = new GenericPrincipal(new
                GenericIdentity(username, "Membership Provider"), roles);
            }
            else
            {
                DenyAccess(app);
                return;
            }
        }
        else
        {
            app.Response.StatusCode = 401;
            app.Response.End();
        }
    }

    public void OnEndRequest(object source, EventArgs eventArgs)
    {
        // The authorization header is not present.
        // The status of response is set to 401 Access Denied.
        // We will now add the expected authorization method
        // to the response header, so the client knows
        // it needs to send credentials to authenticate
        if (HttpContext.Current.Response.StatusCode == 401)
        {
            HttpContext context = HttpContext.Current;
            context.Response.AddHeader("WWW-Authenticate", "Basic Realm");
        }
    }

    private void DenyAccess(HttpApplication app)
    {
        app.Response.StatusCode = 403;
        app.Response.StatusDescription = "Forbidden";

        // Write to response stream as well, to give the user 
        // visual indication of error 
        app.Response.Write("403 Forbidden");

        app.CompleteRequest();
    }
}

Важно: чтобы мы могли читать поток HTTP-запросов, совместимость ASP.NET не должна быть включена .

Чтобы IIS загрузил этот модуль, необходимо добавить его в <system.webServer> раздел web.config, например:

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
    <remove name="BasicAuthenticationModule" />
    <add name="BasicAuthenticationModule" type="UserAuthenticator" />
  </modules>

Но перед этим вы должны убедиться, что раздел BasicAuthenticationModule не заблокирован и по умолчанию должен быть заблокирован.Вы не сможете заменить его, если он заблокирован.

Чтобы разблокировать модуль: (примечание: я использую IIS 7.5)

  1. Откройте диспетчер IIS
  2. На левой панели щелкните свое имя хоста
  3. . На средней панели в разделе «Управление» откройте «Редактор конфигурации»
  4. Щелкните поле со списком рядом с меткой «Раздел» вВ верхней части панели разверните «system.webServer», затем перейдите к «модулям»
  5. В разделе «(Коллекция)» щелкните значение «(Count = nn)», чтобы получить маленькую кнопку с «...».появляются.Нажмите на него.
  6. В списке «Элементы» найдите «BasicAuthenticationModule» и на правой панели нажмите «Разблокировать элемент» (если имеется!).
  7. Если вы изменили этот параметр, закройтеРедактор конфигурации, сохраняющий изменения.

На стороне клиента необходимо иметь возможность добавлять пользовательские заголовки HTTP к исходящему сообщению.Лучший способ сделать это - реализовать IClientMessageInspector и добавить свои заголовки, используя функцию BeforeSendRequest.Я не буду объяснять, как реализовать IClientMessageInspector, по этой теме доступно множество ресурсов.

Чтобы добавить HTTP-заголовок «Authorization» к сообщению, выполните следующие действия:

    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {    

        // Making sure we have a HttpRequestMessageProperty
        HttpRequestMessageProperty httpRequestMessageProperty;
        if (request.Properties.ContainsKey(HttpRequestMessageProperty.Name))
        {     
            httpRequestMessageProperty = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
            if (httpRequestMessageProperty == null)
            {      
                httpRequestMessageProperty = new HttpRequestMessageProperty();
                request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty);
            } 
        }
        else
        {     
            httpRequestMessageProperty = new HttpRequestMessageProperty();
            request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty);
        } 
        // Add the authorization header to the WCF request    
        httpRequestMessageProperty.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(Service.Proxy.ClientCredentials.UserName.UserName + ":" + Service.Proxy.ClientCredentials.UserName.Password)));
        return null;
    }    

Вот и все, решение заняло некоторое время, но оно того стоило, так как я нашел много похожих вопросов без ответа по всей сети.

0 голосов
/ 02 февраля 2012

Вы пытаетесь внедрить HTTP аутентификацию, поэтому просмотрите эту статью MSDN , чтобы убедиться, что вы правильно настроили свою службу.Как вы узнали, учебник, на который вы ссылаетесь, работает для basicHttpBinding, но wsHttpBinding требует специальной настройки для поддержки HTTP-аутентификации.

...