5

WCF サービスで動作するトランスポート セキュリティでユーザー名認証を取得するために、このチュートリアルに従っています。ただし、チュートリアルでは、 basicHttpBindingwhich is unacceptable - I requireの使用について言及していますwsHttpBinding

BasicAuthenticationModuleアイデアは、HTTP 要求から「Authorization」ヘッダーを読み取り、「Authorization」ヘッダーの内容に従って認証プロセスを実行する WCF サービスのカスタムを持つことです。問題は、「Authorization」ヘッダーが欠落していることです!

IClientMessageInspector発信メッセージを操作し、カスタム SOAP ヘッダーを追加するために、カスタム動作を実装しました。BeforeSendRequest関数に次のコードを追加しました。

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

これは機能するはずであり、多くの Web リソースによるとbasicHttpBindingwsHttpBinding. 「動作する」とは、WCF サービスがヘッダーを正常に受信したことを意味します。

これは、WCF サービス側で受信した HTTP メッセージを検査する単純化された関数です。

    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 年 9 月付けのこのスレッドの下部の投稿では、これは では不可能であると述べていwsHttpBindingます。私はその返事を受け入れたくない。

補足として、IIS に組み込まれている基本認証モジュールを使用し、カスタム モジュールを使用しないと、次のようになります。

The parameter 'username' must not include commas.**Roles.IsInRole("RoleName")または `[PrincipalPermission(SecurityAction.Demand, Role = "RoleName")]を試行したときのエラー メッセージ

おそらく、証明書ベースのメッセージ セキュリティでセキュリティをPrimaryIdentity.Name使用しているため、プロパティに証明書のサブジェクト名が含まれているためです。TransportWithMessageCredential

問題に対する代替アプローチだけでなく、提案も受け付けています。ありがとう。

アップデート

お分かりのように、HTTP ヘッダーは、後で WCF サービス コード全体で正しく読み取られます。 (HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties["httpRequest"]カスタムヘッダーが含まれています。ただし、これは既にメッセージ レベルです。トランスポート認証ルーチンにヘッダーを渡す方法は?

更新 2
少し調査した結果、Web ブラウザーが HTTP ステータス コード 401 を受信すると、資格情報を指定できるログイン ダイアログが表示されるという結論に達しました。ただし、WCF クライアントは単に例外をスローし、資格情報を送信したくありません。https://myserver/myservice/service.svcInternet Explorer でアクセスしたときに、この動作を確認できましたこのリンクからの情報を使用して修正しようとしましこれは WCF のバグですか、それとも何か不足していますか?

編集

これが私のsystem.servicemodel(からのweb.config)関連セクションです - 私はそれを正しく設定したと確信しています。

  <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 要求は、クライアント認証スキーム「匿名」では許可されていません。サーバーから受信した認証ヘッダーは「Basic Realm,Negotiate,NTLM」でした。(リモート サーバーがエラーを返しました: (401) Unauthorized.)

4

2 に答える 2

3

興味深いことに、上記の回答に関する最後のコメントを書いているときに、ちょっと立ち止まりました。私のコメントには、「... HTTP ヘッダーに「Authorization」ヘッダーが含まれていない場合、ステータスを 401 に設定すると、例外が発生します。」ステータスを 401 に設定しました。解決策はずっとそこにありました。

明示的に追加しても、最初のパケットには認証ヘッダーが含まれていません。ただし、承認モジュールを非アクティブにしているときにテストしたように、結果として生じる各パケットにはそれが含まれています。では、この最初のパケットを他のパケットと区別してみませんか? したがって、最初のパケットであることがわかった場合は、HTTP ステータス コードを 200 (OK) に設定し、そうでない場合は、認証ヘッダーを確認します。最初のパケットは SOAP エンベロープ (<t:RequestSecurityToken>タグを含む) でセキュリティ トークンの要求を送信するため、これは簡単でした。

わかりましたので、他の誰かが必要とする場合に備えて、私の実装を見てみましょう。

これは、IHTTPModule を実装する BasicAuthenticationModule 実装です。

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 互換性を有効にしないでください。

<system.webServer>IIS にこのモジュールを読み込ませるには、次のように 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. "(Collection)" キーの下で、"(Count=nn)" 値をクリックして、"..." の小さなボタンを表示します。クリックして。
  6. [アイテム] リストで [BasicAuthenticationModule] を見つけ、右側のペインで [アイテムのロック解除] をクリックします (存在する場合)。
  7. この設定を変更した場合は、構成エディターを閉じて、変更を保存します。

クライアント側では、カスタム HTTP ヘッダーを送信メッセージに追加できる必要があります。これを行う最善の方法は、IClientMessageInspector を実装し、BeforeSendRequest関数を使用してヘッダーを追加することです。IClientMessageInspector の実装方法については説明しませんが、オンラインで入手できるトピックに関するリソースはたくさんあります。

「Authorization」HTTP ヘッダーをメッセージに追加するには、次の手順を実行します。

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

さて、解決するのにしばらく時間がかかりましたが、ウェブ全体で多くの同様の未回答の質問を見つけたので、解決する価値がありました.

于 2012-02-03T19:51:58.893 に答える
0

HTTP認証を実装しようとしているため、このMSDN の記事を参照して、サービスが正しく構成されていることを確認してください。お分かりのように、参照するチュートリアルは basicHttpBinding で機能しますが、wsHttpBinding には HTTP 認証をサポートするための特別な構成が必要です。

于 2012-02-02T15:48:03.440 に答える