扩展 MVC Identity






4.97/5 (9投票s)
扩展 MVC Identity
引言
我最初写下这篇操作指南是为了提醒自己。我从经验和其他阅读材料(书籍和文章)中收集了其内容。然后我希望与大家分享,尽管有很多文章和操作指南都在讨论这个精彩的主题,但我仍希望它能对某人有所帮助。
让我们从头开始。
当你启动一个 Web 应用程序时,会弹出一个显示授权选项的屏幕。我不会涵盖这些选项,因为我在这里感兴趣的是“个人用户账户”。
你还会看到一个“更改”按钮,默认情况下会选中“个人用户授权”。保持不变,然后创建你的 MVC 应用程序。
一旦你的应用程序创建完成,你就会获得许多关于身份验证和授权的即用功能。要感受一下,点击“注册”链接并注册自己。然后应用程序将通过显示你的名字并向你问好来识别你。你可以点击它,你将被重定向到“管理账户”视图,以便更改密码。你也可以注销并再次登录。所有幕后工作都已为你完成。
我们刚刚使用的链接位于共享文件夹中的 _loginPartial
视图中。这些链接触发 AccountController
上的动作。
当然,如果应用程序记住你,那是因为某个地方存在持久化机制和存储用户信息的仓库。我们稍后会讲到。
现在,如果你想限制已认证用户对控制器任何动作或整个控制器的访问,你可以向其添加 [Authorize]
属性。当你尝试访问它时,如果你未登录,你将被重定向到登录视图。登录 URL 配置在 App_Start 文件夹中的 StartupAuth.cs 中。以下是代码片段:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
当然,你可以限制除你指定的用户之外的所有用户的访问,如下所示:
[Authorize(Users="m2a")]
或按角色或多个角色 [Authorize(Role="admin, techees")]
即使你在控制器级别添加了这些属性,你也可以通过添加限制较少的角色或属性(如 [AllowAnonymous]
)来允许访问任何动作。这是非常简单的内容,你可以轻松尝试。
数据库
正如我之前所说,在幕后,有一个数据库存储着用户信息。点击你的项目并“显示所有文件”。在 App-Data 文件夹下,你会看到一个 MDF 扩展名的文件,这就是数据库。
双击它,如果想浏览,它将在服务器资源管理器中打开。但是,像哈希密码这样的用户信息存储在 AppNetUser
表中。
我对此不喜欢的一点是数据库名称。这可能只是一个细节,但我希望名称比这个aspnet-thing+datetime...更有意义。所以要做到这一点,打开你的 Web config 文件并进入 connectionstring
部分。你会看到默认情况下连接字符串是 DefaultConnection
。因此,在那里更改名称,并更改目录名称,使其对你来说有意义,然后重建应用程序。你会注意到创建了一个新数据库,你必须再次注册。要删除旧数据库,你需要从文件服务器和服务器资源管理器中进行操作。
核心身份
AspNet.Identity.Core
是幕后实现魔法的 API。它暴露了许多接口(仓储模式实现),如 IUser
、IRole
、IUserStore
等。还有许多具体对象,如 UserManager
和 RoleManager
。我们稍后会介绍 UserManager
类,但它基本上暴露了管理用户信息(如密码哈希)的领域逻辑。还有一个 UserStore
可用于管理用户,但你应始终使用 UserManager
,除非你需要低级别功能并需要 UserStore
来完成。例如,如果你需要控制用户信息的持久化方式或位置,你可能希望使用 MongoDb 或任何其他你选择的 NoSql 数据库来持久化用户。如果你选择坚持使用 SQL Server,那么 Entity Framework 就派上用场了。
实体框架
在 Identity.EntityFramework
程序集中,有许多对象可以方便用户和角色管理,例如 IdentityUser
和 IdentityRole
。你还有一个 IdentityDbContext
,它允许你与 SQL 数据库交互。
扩展 Identity
假设你想在登录视图中添加一个秘密问题,以便将其记录为安全措施。要做到这一点,打开 identityModel
类,并向 ApplicationUser
类添加一个属性,如下所示:
public class ApplicationUser : IdentityUser
{
public string SecretQuestion { get; set; }
public string SecretQuestionResponse { get; set; }
}
现在你需要将它与登录视图连接起来,以便从用户那里获取信息。现在我们意识到我们还需要一些关于公司的有用信息,所以我们继续定义一个代理 poco,如下所示:
public class Agency
{
public int Id { get; set; }
public string Name { get; set; }
public int NumberOfEmployees { get; set; }
}
为了让这个 poco 与 Entity Framework 交互,我们需要一个引用该对象的 DbSet
类。我们添加它,如下所示:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext() : base("DefaultConnection")
{
}
public DbSet<Agency> AgencyContext;
}
ApplicationDbContext
获取 IdentityDbContext
类,该类提供对用户和角色管理以及 AgencyContext
的访问。这意味着无论你在何处实例化此对象,你都将完全控制它们。
但是请记住,就用户而言,UserManager
是一个更好的选择,正如我们前面所说的。同样,这取决于你尝试做什么。
现在,我们设置好了,运行应用程序。出现了臭名昭著的黄屏死机。如果你错过了,请查看图 1,了解它的样子。但基本上,它是在说你需要更新数据库,以处理你刚刚添加的新内容。
为此,我们需要使用包管理器控制台启用迁移(enable-migrations)。一旦添加,解决方案中会添加一个 Migrations 文件夹,其中包含一个配置类。
在配置类中,将 automaticMigrationEnabled
设置为 true
,以避免在开发过程中保留脚本。一旦你准备好在生产环境之前基准化数据库,你仍然需要管理这些脚本。
internal sealed class Configuration : DbMigrationsConfiguration<identity2.Models.ApplicationDbContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
ContextKey = "identity2.Models.ApplicationDbContext";
}
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
}
}
请注意那里的种子方法。此方法用于使用参考数据填充数据库。可能是用户、角色或任何你希望在数据库创建后立即存在的数据。
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
context.Users.AddOrUpdate(firstUser => firstUser.UserName, new ApplicationUser { UserName = "Mack", PasswordHash = new PasswordHasher().HashPassword("password") });
}
对我来说,这种做法太Entity Framework化了。由于我在 AccountController
中有一个如何管理此事的示例,我将对其进行重构,如下所示:
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
//context.Users.AddOrUpdate(firstUser => firstUser.UserName, new ApplicationUser { UserName = "Mack", PasswordHash = new PasswordHasher().HashPassword("password") });
if (!context.Users.Any(user => string.Compare(user.UserName, "mack", StringComparison.CurrentCultureIgnoreCase) == 0))
{
var userstore = new UserStore<ApplicationUser>(context);
var usermanager = new UserManager<ApplicationUser>(userstore);
usermanager.Create(new ApplicationUser {UserName = "mack"}, "password");
}
}
如果你查看 AccountController
的构造函数,你会注意到 UserStore
需要一个上下文,而 UserManager
需要一个 UserStore
。
public AccountController()
: this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
{
}
我们刚刚在种子方法中完成了此操作,并将用户和密码添加到了创建函数中。现在继续通过管理器控制台更新数据库。
如果你想查看当有待应用的迁移时会发生什么,请在 update-database 之后添加 -verbose 标志。之后,我们再次运行应用程序,一切都按预期运行。
这是包含“admin”角色的更新代码:
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
//context.Users.AddOrUpdate(firstUser => firstUser.UserName, new ApplicationUser { UserName = "mack", PasswordHash = new PasswordHasher().HashPassword("password") });
if (!context.Roles.Any(role => string.Compare(role.Name, "admin", StringComparison.CurrentCultureIgnoreCase) == 0))
{
var rolestore = new RoleStore<IdentityRole>(context);
var rolemanager = new RoleManager<IdentityRole>(rolestore);
rolemanager.Create(new IdentityRole { Name = "admin" });
}
if (!context.Users.Any(user => string.Compare(user.UserName, "mack", StringComparison.CurrentCultureIgnoreCase) == 0))
{
var userstore = new UserStore<ApplicationUser>(context);
var usermanager = new UserManager<ApplicationUser>(userstore);
var user = new ApplicationUser { UserName = "Mack" };
usermanager.Create(user, "password");
usermanager.AddToRole(user.Id, "admin");
}
}
正如你所注意到的,种子方法还有重构空间。因为如果你需要添加超过 100 个用户,你不会希望重复相同的代码超过 100 次。
外部登录
外部登录使用 OpenId 或 OAuth 等协议提供用户身份。你在提供商网站上注册应用程序,然后获得一个密钥添加到你的应用程序中。
身份提供者如何向你提供用户身份对我来说有点模糊。
它足够复杂,因此在此处避免讨论,而且我当然不理解“执行发现”操作和 Google 的 xrds 文档... 无论如何,如果你有兴趣学习更多,外面有很多资料。
尽管如此,为用户提供此机会的优势在于他们不必记住另一个密码,而作为开发者,最大的优势在于你不必担心存储和管理用户凭据等。但你仍然必须信任 Microsoft、Google、Facebook、Twitter 等提供商。
要启用此功能,请取消注释 Startup 对象(在 Startup.Auth.cs 文件中)中的代码。请注意,Google 不需要任何密钥或注册。取消注释代码如下:
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Uncomment the following lines to enable logging in with third party login providers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
app.UseGoogleAuthentication();
}
然后点击登录视图上的Google按钮。按照说明操作,最后你将被重定向为应用程序中的已识别用户。请注意,你甚至可以在著名的“你好”问候语之后更改你显示的姓名!
幕后 OWIN 正在发挥作用。要对此有所了解,请返回到 AccountController
并查看 AuthenticationManager
属性。它来自 Owin Context。
要查看你用来登录 Google 的电子邮件地址(例如),请转到 ExternalLoginCallback
函数并在开头编写以下代码:
var fromGoogle= await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie);
现在您可以浏览到 Identity,然后到 EmailAddress Claim 以获取电子邮件地址。