与MVC5中现有用户数据库的复杂身份验证

时间:2014-07-31 09:06:09

标签: asp.net authentication asp.net-mvc-5 asp.net-identity wif

我正在将SaaS应用程序从Classic ASP迁移到.NET MVC5,并将使用EF6 Database First。最终用户的登录表单可由每个租户自定义(在他们自己的子域上,但指向同一个Web应用程序)。我们希望使用现有的数据库架构和新的身份验证&授权过滤器。

例如,一个租户上的用户可以通过输入他们的名字,姓氏和我们系统生成的代码来登录。另一个租户的用户可以通过输入他们的电子邮件地址和密码来登录。此外,每个租户都有一个单独的管理员登录名,该登录名使用用户名和密码。另一个租户可以对远程AD服务器使用LDAP身份验证。

是否有明确的最佳做法进行自定义身份验证?

几乎每篇文章都提出了不同的实现方法:只需设置FormsAuthentication.SetAuthCookie,使用自定义OWIN提供程序,覆盖AuthorizeAttribute等。

在Classic ASP中,我们查询数据库以找出该租户的登录类型,在登录屏幕上显示相应的字段,然后在回发后,检查字段是否与数据库中的内容匹配,然后设置会话变量适当地检查每页请求。

由于

3 个答案:

答案 0 :(得分:6)

我发现Identity框架在身份验证选项方面非常灵活。看看这段认证码:

var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);

authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);

这是Identity中工厂认证部分的标准运行,您可以在Web上的每个Identity样本中找到它。如果你仔细观察它是非常灵活的 - 身份验证所需要的只是ApplicationUser对象框架并不关心你如何获得。

所以理论上你可以做这样的事情(伪代码,我没有尝试编译这个):

// get user object from the database with whatever conditions you like
// this can be AuthCode which was pre-set on the user object in the db-table
// or some other property
var user = dbContext.Users.Where(u => u.Username == "BillyJoe" && u.Tenant == "ExpensiveClient" && u.AuthCode == "654")

// check user for null 

// check if the password is correct - don't have to do that if you are doing
// super-custom auth.
var isCorrectPassword = await userManager.CheckPasswordAsync(user, "enteredPassword");

if (isCorrectPassword)
{
    // password is correct, time to login
    // this creates ClaimsIdentity object from the ApplicationUser object
    var identity = await this.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);

    // now we can set claims on the identity. Claims are stored in cookie and available without
    // querying database
    identity.AddClaim(new Claim("MyApp:TenantName", "ExpensiveClient"));
    identity.AddClaim(new Claim("MyApp:LoginType", "AuthCode"));
    identity.AddClaim(new Claim("MyApp:CanViewProducts", "true"));


    // this tells OWIN that it can set auth cookie when it is time to send 
    // a reply back to the client
    authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

使用此身份验证,您已为用户设置了一些声明 - 它们存储在Cookie中,并通过ClaimsPrincipal.Current.Claims随处可用。声明本质上是一组键值对的集合,你可以存储任何你喜欢的东西。

我通常通过扩展程序访问用户的声明:

public static String GetTenantName(this ClaimsPrincipal principal)
{
    var tenantClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:TenantName");
    if (tenantClaim != null)
    {
        return tenantClaim.Value;
    }

    throw new ApplicationException("Tenant name is not set. Can not proceed");
}

public static String CanViewProducts(this ClaimsPrincipal principal)
{
    var productClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:CanViewProducts");
    if (productClaim == null)
    {
        return false;
    }

    return productClaim.Value == "true";
}

因此,在您的控制器/视图/业务层中,您始终可以致电ClaimsPrincipal.Current.GetTenantName(),在这种情况下,您可以获得" ExpensiveClient"回来。

或者,如果您需要检查是否为用户启用了特定功能,请执行

if(ClaimsPrincipal.Current.CanViewProducts())
{
    // display products
}

由您决定如何存储您的用户属性,但只要您将其设置为Cookie上的声明,它们就可用。

或者,您可以为每个用户向数据库添加声明:

await userManager.AddClaimAsync(user.Id, new Claim("MyApp:TenantName", "ExpensiveClient"));

这将使声明持续存在于数据库中。默认情况下,Identity框架在用户登录时会将此声明添加到用户,而无需手动添加。

但要注意,你不能在cookie上设置过多的声明。 Cookie具有浏览器设置的4K限制。身份cookie加密的工作方式使编码文本增加了大约1.1,因此您可以拥有大约3.6K的代表声明的文本。我已经遇到了issue here

<强>更新

要通过声明控制对控制器的访问,您可以使用控制器上的following filter

public class ClaimsAuthorizeAttribute : AuthorizeAttribute
{
    public string Name { get; private set; }


    public ClaimsAuthorizeAttribute(string name)
    {
        Name = name;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var user = HttpContext.Current.User as ClaimsPrincipal;
        if (user.HasClaim(Name, Name))
        {
            base.OnAuthorization(filterContext);
        }
        else
        {
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary()
            {
                {"controller", "errors"},
                {"action", "Unauthorised"}
            });
        }
    }
}

然后在控制器上使用此属性或单独执行此操作:

    [ClaimsAuthorize("Creating Something")]
    public ActionResult CreateSomething()
    {
        return View();
    }

用户需要&#34;创建一些东西&#34;要求他们访问此操作,否则他们将被重定向到&#34; Unauthenticated&#34;页。

最近,我使用声明身份验证并制作了类似于您的要求的原型应用程序。请查看简单版本:https://github.com/trailmax/ClaimsAuthorisation/tree/SimpleClaims,其中为每个用户单独存储声明。或者有更复杂的解决方案,其中声明属于某个角色,当用户登录时,分配给用户的角色声明:https://github.com/trailmax/ClaimsAuthorisation/tree/master

答案 1 :(得分:2)

您需要两个组件。身份验证本身以及每个用户获得身份验证的策略。

第一个很简单,用这两行完成......

var identity = await UserManager.CreateIdentityAsync(user, 
    DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() 
    { IsPersistent = isPersistent }, identity);

当用户登录时,他们会获得一个身份,其中包含用户对角色的声明以及他们的身份。这些是作为cookie提供给用户的。在此之后,您只需使用[Authorize]装饰控制器,以确保只有经过身份验证的用户才能登录。此处非常标准。

问题中唯一复杂的部分是第二部分;每个用户如何通过管理员设置身份验证的策略。

关于这在行动中如何起作用的一些伪代码是......

// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(int tenantId)
{
    var tenant = DB.GetTenant(tenantId);
    return View(tenant);
}

在您的视图中,您将输出租户的身份验证策略。这可能是电子邮件和密码,代码和电子邮件,或者您的要求。

当用户输入他们的信息并点击登录时,您必须确定他们使用的策略,并检查他们的信息是否匹配。

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
    var tenant = DB.GetTenant(model.tenantId);
    //If user info matches what is expected for the tenants strategy
    if(AuthenticateUserInfo(tenant, model.UserInputs))
    {
       //Sign the user in
       var identity = await UserManager.CreateIdentityAsync(user, 
           DefaultAuthenticationTypes.ApplicationCookie);
       AuthenticationManager.SignIn(new AuthenticationProperties() 
           { IsPersistent = isPersistent }, identity);
    }
}

我在第二部分做了大量的挥手,因为它的动态性如此复杂。总的来说,您应该使用您在遗留应用程序中使用的相同策略来生成正确的输入等。没有什么改变,只有你登录的方式会有所不同。

答案 2 :(得分:1)

使用 Visual Studio 2013 Update 3 ,您可以创建一个已安装 MVC5,EF6和Identity 的新Web应用程序。以下是创建新应用程序时如何选择标识:

选择MVC模板后,单击更改身份验证,将弹出突出显示的窗口。个人用户帐户=身份。单击确定继续。 Select Identity in MVC Application

完成此操作后,您已使用Identity创建了一个应用程序。您现在可以按如下方式自定义登录和注册。

您想查看Controllers文件夹中的AccountController.cs。在这里,您将找到注册和登录的脚本。

如果你看一下

public async Task<ActionResult> Register(RegisterViewModel model)

功能,您会注意到它包含:

IdentityResult result = await UserManager.CreateAsync(new ApplicationUser() { UserName = newUser.UserName }, newUser.Password);

这是用户创建的地方。如果要使用Identity,则应保存用户的用户名和密码。如果需要,您可以使用电子邮件作为用户名。等

执行此操作后,我向用户添加指定的角色(我找到用户,然后将其添加到角色中):

ApplicationUser userIDN = UserManager.FindByName(newUser.UserName);
result = await UserManager.AddToRoleAsync(userIDN.Id, "Admin");

在我的场景中,我创建了一个额外的扩展表,其中我保存了他们的地址,电话号码等。在该表中,您可以保存任何其他登录信息。您可以在Identity中创建用户帐户之前或之后添加这些新条目。我会创建扩展信息,然后创建身份帐户,以确保。

重要提示:对于用户使用的用户名或电子邮件地址未登录的任何情况,您必须执行以下操作:定制解决方案

示例:用户输入名字,姓氏和代码。您可以做两件事:将名字和姓氏保存到身份的用户名字段中,将代码保存到密码中并验证登录方式 你会检查你的自定义表格中的那些属性,并确保它们匹配,如果他们这样做,你可以称之为这个小美女:

await SignInAsync(new ApplicationUser() { UserName = model.UserName }, isPersistent: false);

一旦调用了SignInAsync功能,您就可以继续将它们引导到受保护的页面。

注意:我在函数调用中创建ApplicationUser,但如果您多次使用它,那么您最好按以下方式声明ApplicationUser:

ApplicationUser user = new ApplicationUser() { UserName = model.UserName };

注意#2:如果您不想使用异步方法,这些功能都具有非异步版本。

注意#3:在使用UserManagement的任何页面的最顶部,正在声明它。确保如果您要创建自己的控制器,而Visual Studio未使用Identity生成该控制器,则在该类的顶部包含UserManagement声明脚本:

namespace NameOfProject.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        public AccountController() : this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()))) { }
        public AccountController(UserManager<ApplicationUser> userManager) { UserManager = userManager; }
        public UserManager<ApplicationUser> UserManager { get; private set; }

如果您有任何疑问,请告诉我,我希望这会有所帮助。