MVC5 中现有用户数据库的复杂身份验证
-
21-12-2019 - |
题
我正在将 SaaS 应用程序从经典 ASP 迁移到 .NET MVC5,并将首先使用 EF6 数据库。最终用户的登录表单可由每个租户自定义(在自己的子域上,但指向相同的 Web 应用程序)。我们希望使用现有的数据库模式和新的身份验证和授权过滤器。
例如,一个租户的用户可以通过输入他们的名字、姓氏和我们系统生成的代码来登录。另一个租户的用户可以通过输入其电子邮件地址和密码来登录。此外,每个租户都有一个单独的管理员登录名,使用用户名和密码。另一个租户可以对远程 AD 服务器使用 LDAP 身份验证。
是否有进行自定义身份验证的明确最佳实践方法?
几乎每篇文章似乎都提出了实现这一目标的不同方法:只需设置 FormsAuthentication.SetAuthCookie
, ,使用自定义 OWIN 提供程序,覆盖 AuthorizeAttribute
, , ETC。
在经典 ASP 中,我们查询数据库以找出该租户的登录类型,在登录屏幕上显示相应的字段,然后在回发时,检查字段与数据库中的内容是否匹配,然后适当地设置会话变量,这些变量是检查每个页面请求。
谢谢
解决方案
我发现身份框架在身份验证选项方面非常灵活。看一下这段验证码:
var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
这是 Identity 中工厂身份验证部分的非常标准的运行,您会在网络上的每个 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()
在这种情况下,您会得到“ExppressiveClient”。
或者,如果您需要检查是否为用户启用了特定功能,您可以这样做
if(ClaimsPrincipal.Current.CanViewProducts())
{
// display products
}
如何存储用户属性取决于您,但只要您将它们设置为 cookie 上的声明,它们就可用。
或者,您可以为每个用户将声明添加到数据库中:
await userManager.AddClaimAsync(user.Id, new Claim("MyApp:TenantName", "ExpensiveClient"));
这会将声明保存到数据库中。默认情况下,身份框架会在用户登录时向用户添加此声明,而无需您手动添加。
但请注意,您不能对 cookie 设置太多声明。浏览器设置 Cookie 的大小限制为 4K。身份 cookie 加密的工作方式会将编码文本增加约 1.1,因此您可以拥有大约 3.6K 的文本来表示声明。我遇到过这个 问题在这里
更新
要通过声明控制对控制器的访问,您可以使用 跟随过滤器 在控制器上:
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();
}
用户将需要“创建某些东西”声明才能访问此操作,否则他们将被重定向到“未经身份验证”页面。
最近,我尝试了声明身份验证并制作了一个类似于您的要求的原型应用程序。请看一下简单版本: https://github.com/trailmax/ClaimsAuthorization/tree/SimpleClaims 其中声明是为每个用户单独存储的。或者有更复杂的解决方案,其中声明属于角色,并且当用户登录时,角色声明分配给用户: https://github.com/trailmax/ClaimsAuthorization/tree/master
其他提示
您需要两个组件。身份验证本身以及每个用户获得身份验证的策略。
第一个很简单,用这两行就可以完成......
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);
}
}
在第二部分中,我做了很多挥手的动作,因为它的动态性非常复杂。总的来说,您应该使用与旧应用程序中使用的相同策略来生成正确的输入等。那里没有任何变化,只是登录方式有所不同。
使用 Visual Studio 2013更新3 您可以创建一个新的Web应用程序,其中包含 mvc5,ef6和标识。以下是在创建新应用程序时如何选择身份:
选择MVC模板,单击更改身份验证,将弹出突出显示的窗口。个人用户帐户=身份。单击“确定”并继续。
如此,您已创建具有身份的应用程序。您现在可以根据以下自定义登录和注册。要查看控制器文件夹中的ConsubController.cs。在这里,您将找到注册和登录的脚本。
如果你看看
public async Task<ActionResult> Register(RegisterViewModel model)
.
功能,您将注意到它包含:
IdentityResult result = await UserManager.CreateAsync(new ApplicationUser() { UserName = newUser.UserName }, newUser.Password);
.
这是用户创建的位置。如果要使用身份,则应保存用户用户名和密码。如果需要,您可以使用电子邮件作为用户名。等等。
完成后,我将用户添加了指定的角色(我找到了用户,然后将其添加到角色):
ApplicationUser userIDN = UserManager.FindByName(newUser.UserName);
result = await UserManager.AddToRoleAsync(userIDN.Id, "Admin");
.
在我的场景中,我创建了一个额外的扩展表,其中我在该表中保存了他们的地址,电话号码等,可以持有任何其他登录信息。您可以在创建身份中的用户帐户之前或之后添加这些新条目。我会创建扩展信息,然后创建身份账户以确定。
重要的:对于用户正在登录的任何方案,其中包含不通过身份保存的用户名或电子邮件地址的内容,您必须执行自定义解决方案。
示例:用户类型在他们的名字,姓氏和代码中。您可以执行两件事:将名字和姓氏保存到用户名和代码中的用户名和代码中的密码中,并验证这些登录 或 您将为这些属性检查您的自定义表,并确保它们匹配,如果和当它们可以调用此小美容:
await SignInAsync(new ApplicationUser() { UserName = model.UserName }, isPersistent: false);
.
一旦您调用SignInasync函数,您可以继续并将其引导到受保护的页面。
注意:我在函数调用上创建应用程序,但如果您使用它不止一旦使用它,您将是声明应用程序的理想选择:
ApplicationUser user = new ApplicationUser() { UserName = model.UserName };
.
注#2:如果您不想用户异步方法,那些函数都有它们的非异步版本。
注3:在使用usermanagement的任何页面的顶部,它被声明。确保您是否正在创建自己的控制器,该控制器未通过Visual Studio使用身份生成,您将在类内的顶部内包含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; }
.
如果您有任何疑问,请告诉我,我希望这有帮助。