给WCF REST服务添加基本的HTTP身份验证

9

我有一个WCF HTTP REST服务,并且我使用另一种编程语言的HTTP客户端来连接它,该客户端编写自己的自定义HTTP。

我想在我的WCF服务中添加WWW-Authenticate基本身份验证支持。

我的方法看起来像这样:

[WebInvoke(UriTemplate = "widgets", Method = "POST")]
public XElement CreateWidget(XElement e)   
{
...
}

我是否可以以某种方式过滤传入的HTTP请求,以便在到达REST方法(如上面的CreateWidget)之前检查有效的基本认证字符串? 注意:我的身份验证信息存储在我的数据库中。

基本上,我想要在请求标头中检查以下内容: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==,然后我可以自己解析该字符串并验证数据库中的用户名和密码。

web.config文件如下:

<?xml version="1.0"?>
<configuration>

  <connectionStrings>
    <add name="DatabaseConnectionString" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=Database;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <httpRuntime maxRequestLength="10485760" />
  </system.web>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </modules>
  </system.webServer>

  <system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
    <standardEndpoints>
      <webHttpEndpoint>
        <standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true" maxReceivedMessageSize="1048576" maxBufferSize="1048576" />
      </webHttpEndpoint>
    </standardEndpoints>
  </system.serviceModel>

</configuration>
3个回答

9

我也对REST HTTP WCF服务中的自定义身份验证感兴趣,最终我让它正常工作了。

话虽如此,我的代码将为您提供一种使其正常工作的方法,但我建议阅读这篇指南,该指南以更深入的方式解释了所有内容:http://wcfsecurityguide.codeplex.com/

首先,将Web.Config文件的system.web部分更改为以下内容:

<system.web>
  <compilation debug="true" targetFramework="4.0" />
  <httpRuntime maxRequestLength="10485760" />
  <authentication mode="None"></authentication>
  <httpModules>
    <add name="BasicAuthenticationModule" type="YourNamespace.UserNameAuthenticator" />
  </httpModules>
</system.web>

然后将另一个文件添加到您的项目中:UserNameAuthenticator.cs。
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Security.Principal;
using System.ServiceModel.Activation;

namespace YourNamespace
{
    public class UserNameAuthenticator : IHttpModule
    {
        public void Dispose()
        {
        }

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

        public bool CustomAuth(string username, string password)
        {
            //TODO: Implement your custom auth logic here
            return true;
        }

        public string[] GetCustomRoles(string username)
        {
            return new string[] { "read", "write" };
        }

        public void OnAuthorizationRequest(object source, EventArgs eventArgs)
        {
            HttpApplication app = (HttpApplication)source;
            //If you want to handle authorization differently from authentication
        }

        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))
            {
                string authStr = app.Request.Headers["Authorization"];
                if (authStr == null || authStr.Length == 0)
                {
                    // No credentials; anonymous request
                    return;
                }
                authStr = authStr.Trim();
                if (authStr.IndexOf("Basic", 0) != 0)
                {
                    //header not correct we do not authenticate
                    return;
                }

                authStr = authStr.Trim();
                string encodedCredentials = authStr.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 (CustomAuth(username, password))
                {
                    string[] roles = GetCustomRoles(username);
                    app.Context.User = new GenericPrincipal(new
                    GenericIdentity(username, "Membership Provider"), roles);
                }
                else
                {
                    DenyAccess(app);
                    return;
                }
            }
            else
            {
                //the authorization header is not present
                //the status of response is set to 401 and it ended
                //the end request will check if it is 401 and add
                //the authentication header so the client knows
                //it needs to send credentials to authenticate
                app.Response.StatusCode = 401;
                app.Response.End();
            }
        }

        public void OnEndRequest(object source, EventArgs eventArgs)
        {
            if (HttpContext.Current.Response.StatusCode == 401)
            {
                //if the status is 401 the WWW-Authenticated is added to
                //the response so client knows it needs to send credentials
                HttpContext context = HttpContext.Current;
                context.Response.StatusCode = 401;
                context.Response.AddHeader("WWW-Authenticate", "Basic Realm");
            }
        }
        private void DenyAccess(HttpApplication app)
        {
            app.Response.StatusCode = 401;
            app.Response.StatusDescription = "Access Denied";
            // error not authenticated
            app.Response.Write("401 Access Denied");
            app.CompleteRequest();
        }
    } // End Class
} //End Namespace

我按照您建议的步骤进行了操作,但模块从未被调用。我尝试在IIS上添加基本身份验证,但仍然无法调用该模块。我是否漏掉了什么? - Eli Perpinyal
我不确定为什么它对我有效,但可能与这个有关:https://dev59.com/X3RC5IYBdhLWcg3wROzk(我是通过VS调试自动托管的Web服务器进行测试的) - Brian R. Bondy
1
在这里需要记住的是,通过使用Web.Config文件来定义身份验证方法,您需要明确禁止IIS进行身份验证。因此,在IIS中:
  • 您必须勾选“启用匿名访问”
  • 您不得勾选“集成Windows身份验证”
  • 您不得勾选“用于Windows域服务器的摘要身份验证”
  • 您不得勾选“基本身份验证(密码以明文形式发送)”(尽管有些混淆,但身份验证由web.config文件提供)
  • 您不得勾选“.NET Passport身份验证”
- AlwaysLearning
我会检查输入的验证:在s.Split(new char[] { ':' })之后,我会确保元素的数量恰好为2。 - Ron Klein

3
我曾经遇到过类似的问题,发现了很多不同的方法,特别是跨域调用和基本身份验证似乎有些困难。例如Jquery首先会发出OPTIONS调用以验证是否允许POST请求。WCF通常会拒绝此请求,导致您收到奇怪的错误。
最终我解决了这个问题,并且你可以从我的博客中下载示例代码:http://sameproblemmorecode.blogspot.com/2011/10/creating-secure-restfull-wcf-service.html

2

补充一下,如果你想要加载登录对话框,Chrome浏览器需要在OnEndRequest方法中将"BasicRealm"改为"BasicRealm=site":

    public void OnEndRequest(object source, EventArgs eventArgs)
    {
        if (HttpContext.Current.Response.StatusCode == 401)
        {
            //if the status is 401 the WWW-Authenticated is added to 
            //the response so client knows it needs to send credentials 
            HttpContext context = HttpContext.Current;
            context.Response.StatusCode = 401;
            context.Response.AddHeader("WWW-Authenticate", "Basic Realm=site");
        }
    }

谢谢,这是一个非常简单的解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接