7

我正在尝试实现具有以下要求的 MVC4 Web 应用程序:

(a) 它仅向经过身份验证的用户提供服务。至于身份验证,我想使用简单的成员资格,因为它是 MVC 的最新身份验证技术,给我定义自己的数据库表的优势,提供开箱即用的 OAuth 支持,并且很容易与 MVC 和WebAPI。

(b) 它通过 WebApi 为移动/JS 客户端公开了一些核心功能,这些功能应通过基本 HTTP 身份验证 (+SSL) 进行身份验证。通常,我会让 JS 客户端使用 jQuery AJAX 调用 WebApi 控制器,并为不同的用户角色装饰 Authorize 属性。

(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 类没有为此提供任何方法(除了登录,但我会避免仅仅为了授权而更改登录的用户,也是因为潜在的“混合”环境,如 (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;
            });
    }
}
4

3 回答 3

3

这是另一种满足您所有要求的解决方案。它在 MVC 4 应用程序中将 SimpleMemberhsip 与表单身份验证和基本身份验证混合使用。它还可以支持授权,但将 Role 属性保留为 null 不需要它。

于 2013-02-14T13:53:59.443 回答
3

谢谢,这似乎是目前最好的解决方案!我设法从头开始创建了一个虚拟解决方案(在这里找到它:http: //sdrv.ms/YpkRcf),它似乎在以下情况下工作:

1) 当我尝试访问 MVC 控制器受限操作时,我会按预期重定向到登录页面以进行身份​​验证。

2) 当我触发对 WebApi 控制器受限操作的 jQuery ajax 调用时,调用成功(当然不使用 SSL 时除外)。

但是,在登录网站后,API调用仍然需要身份验证时,它不起作用。谁能解释这里发生了什么?在接下来的内容中,我详细介绍了我的程序,因为我认为它可能对像我这样的初学者有用。

谢谢(对不起,以下内容的格式,但我无法让这个编辑器适当地标记代码......)


程序

  1. 创建一个新的 mvc4 应用程序(基本的 mvc4 应用程序:这已经带有通用提供程序。所有通用提供程序类名都以 Default... 开头);

  2. 为您的非本地数据库自定义 web.config,例如:

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

此外,设置用于散列密码的 machineKey 通常很有用,这样您就可以在服务器之间自由移动该站点,而不会弄乱您的密码。使用机器密钥生成器网站来定义这样的条目:

  <system.web>
        <machineKey
   validationKey="...thekey..."
   decryptionKey="...thekey..."
   validation="SHA1"
   decryption="AES" />
  1. 如果需要,创建一个与 web.config 的连接字符串相对应的新的空数据库。然后启动我们的好老朋友 WSAT(从 VS 项目菜单中)并通过根据需要添加用户和角色来配置安全性。

  2. 如果您愿意,请添加一个带有 Index 操作的 HomeController,因为此模板中不存在任何控制器,因此您无法在没有它的情况下测试启动您的 Web 应用程序。

  3. 从 NuGet 添加 Thinktecture.IdentityModel.45 并添加/更新所有您喜欢的 NuGet 包。请注意,在撰写本文时,来自 MS 的不显眼的 jquery 验证不再与 jQuery 1.9 或更高版本兼容。我宁愿使用http://plugins.jquery.com/winf.unobtrusive-ajax/。所以,删除 jquery.unobtrusive* 并在你的包 (App_Start/BundleConfig.cs) 中添加这个库(由 winf.unobtrusive-ajax 和附加方法组成)。

  4. 通过在 DefaultApi 路由配置之后添加代码来修改 App_Start 中的 WebApiConfig.cs:

    公共静态类 WebApiConfig { public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", 默认值: 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. 将 LoginModel.cs 添加到模型中:

    公共类 LoginModel { [必需] [Display(Name = "UserName", ResourceType = typeof(StringResources))] public string UserName { get; 放; }

    [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. 将 ClaimsTransformer.cs 添加到您的解决方案根目录,如下所示:

    公共类 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. 将 Application_PostAuthenticateRequest 添加到 Global.asax.cs:

    公共类 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(将 YourAppNamespace 替换为您的应用根命名空间):

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

  4. 为帐户控制器添加其他模型及其视图(您可以从 MVC3 应用程序模板中派生它们,即使我更喜欢使用需要字符串资源名称而不是文字的属性将它们更改为更易于本地化的变体)。

  5. 要测试基于浏览器的身份验证,请将一些 [Authorized] 操作添加到控制器(例如 HomeController),然后尝试访问它。

  6. 要测试基本的 HTTP 身份验证,请在某些视图(例如 Home/Index)中插入这样的代码(在令牌变量中设置您的用户名和密码):

    ...

    <p>测试调用

    $(function() { $("#test").click(function () { var token = "USERNAME:PASSWORD"; var hash = $.base64.encode(token); var header = "Basic " + hash;控制台日志(标题);
            $.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);
                }
            });
        });
    });
    

这需要用于编码/解码 Base64 的 jQuery 插件:jquery.base64.js 及其缩小版。

要允许 SSL,请按照此处的说明进行操作:http ://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx (基本上,在 Web 项目属性中启用 SSL 并连接到属性值中指定的端口)。

于 2013-02-12T23:31:01.577 回答