在MVC4应用程序和WebApi中使用基本的HTTP身份验证的SimpleMembership

7
我正在尝试实现一个符合以下要求的MVC4 Web应用程序:
(a) 仅向已认证用户提供服务。至于身份验证,我想使用简单成员身份验证,因为它是MVC的最新身份验证技术,可以让我定义自己的数据库表,提供开箱即用的OAuth支持,并且很容易与MVC和WebApi集成。
(b) 通过WebApi公开一些核心功能,供移动/JS客户端使用,这些客户端应该通过基本HTTP身份验证(+SSL)进行身份验证。通常情况下,我会有使用jQuery AJAX调用带有不同用户角色Authorize属性的WebApi控制器的JS客户端。
(c) 理想情况下,在混合环境中,我希望避免双重身份验证:即如果用户已经通过浏览器进行了身份验证,并且正在访问涉及JS调用WebApi控制器操作的页面,则(a)机制应该足够。
因此,虽然(a)由默认MVC模板处理,但(b)需要基本HTTP身份验证而无需浏览器介入。为此,我应该创建一个DelegatingHandler,就像我在这篇文章中找到的那样:http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers。问题在于,它的实现需要一种从接收到的用户名和密码中检索IPrincipal的方法,而WebSecurity类没有提供任何此类方法(除了Login,但我会避免仅为授权目的更改已登录用户,也因为存在潜在的“混合”环境,如(c)所述)。因此,似乎我的唯一选择是放弃简单成员身份验证。有没有更好的建议?这里是引用自上述文章的相关(略作修改)代码:
public interface IPrincipalProvider
{
    IPrincipal GetPrincipal(string username, string password);
}

public sealed class Credentials
{
    public string Username { get; set; }
    public string Password { get; set; }
}

public class BasicAuthMessageHandler : DelegatingHandler
{
    private const string BasicAuthResponseHeader = "WWW-Authenticate";
    private const string BasicAuthResponseHeaderValue = "Basic";

    public IPrincipalProvider PrincipalProvider { get; private set; }

    public BasicAuthMessageHandler(IPrincipalProvider provider)
    {
        if (provider == null) throw new ArgumentNullException("provider");
        PrincipalProvider = provider;
    }

    private static Credentials ParseAuthorizationHeader(string sHeader)
    {
        string[] credentials = Encoding.ASCII.GetString(
            Convert.FromBase64String(sHeader)).Split(new[] { ':' });

        if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) ||
            String.IsNullOrEmpty(credentials[1])) return null;

        return new Credentials
        {
            Username = credentials[0],
            Password = credentials[1],
        };
    }

    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        AuthenticationHeaderValue authValue = request.Headers.Authorization;
        if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter))
        {
            Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter);
            if (parsedCredentials != null)
            {
                Thread.CurrentPrincipal = PrincipalProvider
                    .GetPrincipal(parsedCredentials.Username, parsedCredentials.Password);
            } 
        } 

        return base.SendAsync(request, cancellationToken)
            .ContinueWith(task =>
            {
                var response = task.Result;
                if (response.StatusCode == HttpStatusCode.Unauthorized
                    && !response.Headers.Contains(BasicAuthResponseHeader))
                {
                    response.Headers.Add(BasicAuthResponseHeader,
                        BasicAuthResponseHeaderValue);
                } 
                return response;
            });
    }
}
3个回答

3
谢谢,这似乎是目前可用的最佳解决方案!我设法从头开始创建了一个虚拟解决方案(在这里找到:http://sdrv.ms/YpkRcf),它在以下情况下似乎有效:
1) 当我尝试访问MVC控制器限制操作时,我被重定向到登录页面,以便进行身份验证。
2) 当我触发一个jQuery ajax调用WebApi控制器限制操作时,调用成功(当然,不使用SSL时除外)。
然而,在网站登录后,仍然需要身份验证才能调用API时,它不起作用。有人能解释一下这是怎么回事吗?接下来我详细说明我的过程,因为我认为对像我这样的初学者很有用。
谢谢(对于下面内容的格式化抱歉,但我无法让此编辑器适当地标记代码...)

过程

  1. create a new mvc4 app (basic mvc4 app: this already comes with universal providers. All the universal providers class names start with Default...);

  2. customize web.config for your non-local DB, e.g.:

      <connectionStrings>
    <add name="DefaultConnection"
     providerName="System.Data.SqlClient"
     connectionString="data source=(local)\SQLExpress;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True" />
    

另外,在处理密码时,设置机器密钥以进行哈希加密常常是非常有用的。这样,您可以在服务器之间自由移动此网站,而不会导致密码混乱。使用机器密钥生成器网站定义一个条目,如下所示:

  <system.web>
        <machineKey
   validationKey="...thekey..."
   decryptionKey="...thekey..."
   validation="SHA1"
   decryption="AES" />
  1. if required create a new, empty database corresponding to the connection string of your web.config. Then start our good old pal WSAT (from VS Project menu) and configure security by adding users and roles as required.

  2. if you want to, add a HomeController with an Index action, because no controller is present in this template and thus you could not test-start your web app without it.

  3. add Thinktecture.IdentityModel.45 from NuGet and add/update all your favorite NuGet packages. Notice that at the time of writing this, jquery validation unobtrusive from MS is no more compatible with jQuery 1.9 or higher. I rather use http://plugins.jquery.com/winf.unobtrusive-ajax/ . So, remove jquery.unobtrusive* and add this library (which consists of winf.unobtrusive-ajax and additional-methods) in your bundles (App_Start/BundleConfig.cs).

  4. modify the WebApiConfig.cs in App_Start by adding it the code after the DefaultApi route configuration:

    public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );

        // added for Thinktecture
        var authConfig = new AuthenticationConfiguration
        {
            InheritHostClientIdentity = true,
            ClaimsAuthenticationManager = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager
        };
    
        // setup authentication against membership
        authConfig.AddBasicAuthentication((userName, password) => Membership.ValidateUser(userName, password));
    
        config.MessageHandlers.Add(new AuthenticationHandler(authConfig));
    }
    

    }

为了更清晰,API控制器将被放置在Controllers/Api/文件夹下,请创建此文件夹。
  1. Add to models a LoginModel.cs:

    public class LoginModel { [Required] [Display(Name = "UserName", ResourceType = typeof(StringResources))] public string UserName { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Password", ResourceType = typeof(StringResources))]
    public string Password { get; set; }
    
    [Display(Name = "RememberMe", ResourceType = typeof(StringResources))]
    public bool RememberMe { get; set; }
    

    }

这个模型需要一个StringResources.resx资源文件(使用代码生成),我通常将其放在Assets文件夹下,并在属性中引用3个字符串。

  1. Add a ClaimsTransformer.cs to your solution root, like this:

    public class ClaimsTransformer : ClaimsAuthenticationManager { public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal) { if (!incomingPrincipal.Identity.IsAuthenticated) { return base.Authenticate(resourceName, incomingPrincipal); }

        var name = incomingPrincipal.Identity.Name;
    
        return Principal.Create(
            "Custom", 
            new Claim(ClaimTypes.Name, name + " (transformed)"));
    }
    

    }

  2. Add Application_PostAuthenticateRequest to Global.asax.cs:

    public class MvcApplication : HttpApplication { ... protected void Application_PostAuthenticateRequest() { if (ClaimsPrincipal.Current.Identity.IsAuthenticated) { var transformer = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager; var newPrincipal = transformer.Authenticate(string.Empty, ClaimsPrincipal.Current);

            Thread.CurrentPrincipal = newPrincipal;
            HttpContext.Current.User = newPrincipal;
        }
    }
    

    }

  3. web.config (replace YourAppNamespace with your app root namespace):

    <configSections> <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" /> ...

  4. add the other models for account controller, with their views (you can derive them from MVC3 application template, even if I prefer changing them to more localizable-friendly variants using attributes requiring string resource names rather than literals).

  5. to test browser-based authentication, add some [Authorized] action to a controller (e.g. HomeController), and try accessing it.

  6. to test basic HTTP authentication, insert in some view (e.g. Home/Index) a code like this (set your user name and password in the token variable):

    ...

    <p>Test call


    $(function() { $("#test").click(function () { var token = "USERNAME:PASSWORD"; var hash = $.base64.encode(token); var header = "Basic " + hash; console.log(header);
            $.ajax({
                url: "/api/dummy",
                dataType: "json",
                beforeSend: function(xhr) {
                    xhr.setRequestHeader("Authorization", header);
                },
                success: function(data) {
                    alert(data);
                },
                error: function(jqXHR, textStatus, errorThrown) {
                    alert(errorThrown);
                }
            });
        });
    });
    
这需要使用jQuery插件进行Base64编码/解码:jquery.base64.js及其压缩版本。
要允许SSL,请按照这里的说明操作:http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx(基本上,在Web项目属性中启用SSL并连接到属性值中指定的端口)。

我必须补充说明,根据https://dev59.com/qGnWa4cB1Zd3GeqP03Qb、http://brockallen.com/2012/07/08/mvc-4-antiforgerytoken-and-claims和http://feetens.wordpress.com/2012/06/06/a-slew-of-problems的建议,我不得不修改ClaimsTransformer代码,否则在使用AntiForgeryToken时会出现错误:return Principal.Create("Custom", new Claim(ClaimTypes.Name, name), new Claim(ClaimTypes.NameIdentifier, name));。 - Naftis

3

谢谢,这似乎是一个更好的解决方案,至少对于我的情况来说是这样,因为我不需要完全用旧的成员提供程序替换它。我会尽快尝试这个方法。同时,感谢你们两个的回答! - Naftis


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