使用 wsHttpBinding 的 WCF 服务 - 操作 HTTP 请求标头

发布于 2025-01-01 07:20:24 字数 4863 浏览 1 评论 0原文

我一直在关注教程,以便通过传输进行用户名身份验证我的 WCF 服务中的安全工作。然而,本教程提到使用 basicHttpBinding ,这是不可接受的 - 我需要 wsHttpBinding

这个想法是在 WCF 服务上有一个自定义的 BasicAuthenticationModule ,它将从 HTTP 请求中读取“Authorization”标头,并根据“Authorization”标头内容执行身份验证过程。问题是缺少“授权”标头!

我通过自定义行为实现了 IClientMessageInspector,以便操作传出消息并添加自定义 SOAP 标头。我在 BeforeSendRequest 函数中添加了以下代码:

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

这应该可以工作,并且根据许多 Web 资源,它适用于 basicHttpBinding 但不适用于 wsHttpBinding。当我说“有效”时,我的意思是 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 中内置的基本身份验证模块而不是自定义模块,我会得到

参数“用户名”不得包含逗号。**尝试 Roles.IsInRole("RoleName") 或 `[PrincipalPermission(SecurityAction.Demand, Role = "RoleName")] 时出现错误消息

可能是因为我的PrimaryIdentity.Name 属性包含证书使用者名称,因为我将 TransportWithMessageCredential 安全性与基于证书的消息安全性结合使用。

我愿意接受建议以及解决问题的替代方法。谢谢。

更新

看起来,HTTP 标头稍后在整个 WCF 服务代码中被正确读取。 (HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties["httpRequest"] 包含我的自定义标头。但是,这已经是消息级别的了。如何将标头传递给传输身份验证例程?

更新2
经过一番研究后,我得出的结论是,当 Web 浏览器收到 HTTP 状态代码 401 时,它会向我显示登录对话框,我可以在其中指定我的凭据。然而,WCF 客户端只是抛出异常并且不想发送凭据。在 Internet Explorer 中访问 https://myserver/myservice/service.svc 时,我能够验证此行为。尝试使用链接中的信息进行修复,但无济于事。这是 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) 未经授权。)

I have been following this tutorial in order to get username authentication with transport security working in my WCF service. The tutorial however refers to using basicHttpBinding which is unacceptable - I require wsHttpBinding.

The idea is to have a custom BasicAuthenticationModule on WCF service which would read the "Authorization" header from the HTTP request and perform the auth process according to "Authorization" header contents. The problem is that "Authorization" header is missing!

I have implemented IClientMessageInspector via custom behavior in order to manipulate outgoing messages and add custom SOAP headers. I have added the following code in BeforeSendRequest function:

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

This should work and according to many web resources, it works for basicHttpBinding but not wsHttpBinding. When I say "works", I mean that the header is successfully received by WCF service.

This is the simplified function which inspects the received HTTP message on WCF service side:

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

Bottom posts of this thread dated september 2011 say that this is not possible with wsHttpBinding. I don't want to accept that response.

As a side note, if I use the Basic Authentication Module built in IIS and not the custom one, I get

The parameter 'username' must not contain commas.** error message when trying Roles.IsInRole("RoleName") or `[PrincipalPermission(SecurityAction.Demand, Role = "RoleName")]

probably because my PrimaryIdentity.Name property contains the certificate subject name as I am using TransportWithMessageCredential security with certificate-based message security.

I am open for suggestions as well as alternate approaches to the problem. Thanks.

UPDATE

As it seems, the HTTP header gets read correctly later throughout the WCF service code.
(HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties["httpRequest"] contains my custom header. However, this is already message-level.. How to pass the header to transport authentication routine?

UPDATE 2
After a bit of a research, I came to a conclusion that, when a web browser receives HTTP status code 401, it presents me with the login dialog where I can specify my credentials. However a WCF client simply throws an exception and doesn't want to send credentials. I was able to verify this behavior when visiting https://myserver/myservice/service.svc in Internet Explorer. Tried to fix using information from this link but to no avail. Is this a bug in WCF or am I missing something?

EDIT

Here are relevant sections from my system.servicemodel (from web.config) - I am pretty sure I got that configured right though.

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

The exception returned by the service is:

The HTTP request is unauthorized with client authentication scheme 'Anonymous'. The authentication header received from the server was 'Basic Realm,Negotiate,NTLM'. (The remote server returned an error: (401) Unauthorized.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

情归归情 2025-01-08 07:20:24

有趣的是,当我写关于上述答案的最后评论时,我停了下来。我的评论包含“...如果 HTTP 标头不包含“授权”标头,我将状态设置为 401,这会导致异常。” 我将状态设置为 401。明白了吗?解决方案一直就在那里。

即使我显式添加授权标头,初始数据包也不包含授权标头。然而,正如我在授权模块处于非活动状态时所测试的那样,每个后续数据包确实包含它。所以我想,为什么我不尝试将这个初始数据包与其他数据包区分开来呢?因此,如果我看到它是初始数据包,请将 HTTP 状态代码设置为 200(正常),如果不是,请检查身份验证标头。这很容易,因为初始数据包在 SOAP 信封中发送对安全令牌的请求(包含 标记)。

好吧,让我们看一下我的实现,以防其他人需要它。

这是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 加载此模块,您必须将其添加到 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. 单击“在上部窗格部分中的“Section”标签中,展开“system.webServer”,然后导航到“modules”。
  5. 在“(Collection)”键下,单击“(Count=nn)”值,出现一个带有“...”的小按钮。单击它。
  6. 在“项目”列表中,找到“BasicAuthenticationModule”,然后在右侧窗格中单击“解锁项目”(如果存在!)。
  7. 如果更改了此设置,请关闭配置编辑器并保存更改。

在客户端,您需要能够将自定义 HTTP 标头添加到传出消息中。执行此操作的最佳方法是实现 IClientMessageInspector 并使用 BeforeSendRequest 函数添加标头。我不会解释如何实现 IClientMessageInspector,网上有大量关于该主题的资源。

要将“授权”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;
    }    

好了,解决问题需要一段时间,但这是值得的,因为我在整个网络上发现了许多类似的未解答的问题。

Interestingly enough, while I was writing the last comment regarding the answer above, I stopped for a moment. My comment contained "...If the HTTP header doesn't contain the "Authorization" header, I set the status to 401, which causes the exception." I set the status to 401. Got it? The solution was there all along.

The initial packet doesn't contain the authorization header even if I explicitly add it. However each consequent packet does contain it as I have tested while having the authorization module inactive. So I though, why don't I try to distinguish this initial packet from the others? So if I see that it's the initial packet, set HTTP status code to 200 (OK), and if it's not - check for authentication header. That was easy, since the initial packet sends a request for the security token in a SOAP envelope (Contains <t:RequestSecurityToken> tags).

Ok so let's take a look at my implementation, in case someone else would need it.

This is the BasicAuthenticationModule implementation, which implements 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();
    }
}

Important: in order for us to be able to read the http request stream, ASP.NET compatibility must not be enabled.

To make your IIS load this module, you must add it to <system.webServer> section of web.config, like this:

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

But before that, you must ensure BasicAuthenticationModule section is not locked, and it should be locked by default. You will not be able to replace it if it's locked.

To unlock the module: (note: I am using IIS 7.5)

  1. Open IIS Manager
  2. In the left pane, click on your host name
  3. In the middle pane, under "Management" section, open "Configuration Editor"
  4. Click the combo box next to "Section" label in the upper pane section, expand "system.webServer" then navigate to "modules"
  5. Under "(Collection)" key, click "(Count=nn)" value to have a small button with "..." appear. Click on it.
  6. In the "Items" list, find "BasicAuthenticationModule" and in the right pane click "Unlock Item" (if present!).
  7. If you changed this setting, close the Configuration Editor, saving changes.

On the client side, you need to be able to add custom HTTP headers to the outgoing message. The best way to do this is to implement IClientMessageInspector and add your headers using the BeforeSendRequest function. I will not explain how to implement IClientMessageInspector, there are plenty of resources on that topic available online.

To add the "Authorization" HTTP header to the message, do the following:

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

There ya go, it took a while to resolve but it was worth it, as I found many similar unanswered questions throughout the web.

蓝戈者 2025-01-08 07:20:24

您正在尝试实施 HTTP 身份验证,因此请查看此 MSDN文章,以确保您已正确配置服务。正如您所发现的,您引用的教程适用于 basicHttpBinding,但 wsHttpBinding 需要特殊配置才能支持 HTTP 身份验证。

You are trying to implementing HTTP authentication so look at this MSDN article to ensure you've configured your service correctly. As you found out, the tutorial you reference works for basicHttpBinding but wsHttpBinding needs special configuration to support HTTP authentication.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文