这两天回想之前做的项目,认证一直是比较头痛的地方。
于是抽出时间看了一些内容,从form authentication开始,看到了identity,过程仅仅是简单的浏览,不过对之前的概念也算有了一些加深。
其实这两天才知道,form authentication已经是.net中的昨日黄花了,已经被Identity取代。虽然如此,看了form authentication以后,对认证的整体过程也是有了一些了解,不过这里还是先记录一些identity相关的内容。
Owin
identity是基于Owin框架。Owin其实是一个规范,.net对其的实现,叫做kantana,不过其实两者在使用上,我觉得基本是互通的。
Owin不再依赖IIS的请求pipeline,而是自定义了一套注册组件的机制,个人认为其实已经类似于.net core的做法了。
Owin和IIS
Owin可以运行于IIS上,也可以独立运行,独立运行的Owin有自己的SelfHost组件,当基于IIS时,Owin依赖Microsoft.Owin.Host.SystemWeb
库完成对IIS的集成。
IIS的Global.asax
和Owin的StartUp
回调也是可以同时存在的,都会被调用。
如果集成到IIS中,Owin的初始化调用,差不多是在Application_Start事件后被触发。
Owin的起始类
Owin的pipeline是在起始类的Configuration方法中进行配置的。
有两种配置方式,一种是在Web.config中的AppSettings节点指定:
<add key="owin:AppStartup" value="起始类" />
另一种是通过属性配置,在含有Configuration
方法的类上标注属性
[assembly: OwinStartup(typeof(StartupDemo.TestStartup))]
Identity
Identity一定程度上替代了原来的Form Authentication,不过如果不使用Owin的话,我理解还是得使用Form Authentication的。
Identity的出现其实是为了应对网络环境下的第三方认证(google, fb等等),双因子验证(2FA)需求,传统的Form方式比较不容易扩展。
这部分一定程度上参考了SO上的这个问题的回答
增加Identity组件
除了Owin的依赖以外,Identity还依赖于EntityFramework,需要这几个Nuget组件:
- Microsoft.AspNet.Identity.Owin
- Microsoft.AspNet.Identity.EntityFramework
- Microsoft.Owin.Host.SystemWeb
如果需要使用MySql,还需要安装
- MySql.Data.Entity
配置和初始化
配置文件
Web.config中需要增加一些配置内容
Entity这部分在安装Entity的时候回自动增加,其实不用手动修改:
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
特别的,我是用的是基于配置的Owin Startup配置,所以在AppSettings节点增加了我的入口配置
<add key="owin:AppStartup" value="Demo.WEBUI.IdentityConfig" />
Identity通过Entity将用户信息固化,所以需要配置Entity使用的ConnectionString,这一部分是.net的一个通用约定:
<connectionStrings>
<add name="DefaultConnection" connectionString="server=localhost;database=dbname;uid=root;pwd=123456" providerName="MySql.Data.MySqlClient" />
</connectionStrings>
Entity中增加对MySql的支持,一般默认安装的话,是SqlServer的:
<entityFramework>
<defaultConnectionFactory type="MySql.Data.Entity.MySqlConnectionFactory, MySql.Data.Entity.EF6" />
<providers>
<provider invariantName="MySql.Data.MySqlClient" type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6, Version=6.10.8.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
</entityFramework>
同样是Entity,增加DbProvider,这个节点没有找到比较好的说明,但是不加的话不行。
<system.data>
<DbProviderFactories>
<remove invariant="MySql.Data.MySqlClient"></remove>
<add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.10.8.0" />
</DbProviderFactories>
</system.data>
辅助类
用户
Identity的用户类,可以直接使用IdentityUser,也可以从他的基础上扩展,增加自定义字段,这些字段会加入User表中,我这里增加了三个字段。
public class ApplicationUser : IdentityUser
{
public virtual DateTime? LastLoginTime { get; set; }
public virtual DateTime? RegistrationTime { get; set; }
public virtual bool IsEnabled { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
用户管理
这里配置了用户验证的设置,比如密码强度,cookie过期时间等
public class ApplicationUserManager : UserManager<ApplicationUser>
{
private IUserStore<ApplicationUser> _store;
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
_store = store;
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = true,
RequireUniqueEmail = false
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 4,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = false,
RequireUppercase = false,
};
// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = false;
//manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
//manager.MaxFailedAccessAttemptsBeforeLockout = 5;
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
登录管理
用户登录调用的方法,里面会依赖于上一步中的ApplicationUserManager
public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
: base(userManager, authenticationManager)
{
}
public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
{
return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
}
public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
{
return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
}
}
角色管理
这里我没有做更多的扩展。本身Identity可以不基于角色,而是基于Claim进行权限控制。
所以角色管理并非必须。
public class ApplicationRoleManager : RoleManager<IdentityRole, string>
{
public ApplicationRoleManager(IRoleStore<IdentityRole, string> roleStore)
: base(roleStore)
{
}
public static ApplicationRoleManager Create(IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
{
return new ApplicationRoleManager(new RoleStore<IdentityRole, string, IdentityUserRole>(context.Get<ApplicationDbContext>()));
}
}
初始化类
如果新建一个.net mvc带身份认证的项目,会自动添加初始类。
我是在已有工程上添加的Identity,这里的初始化函数拷贝自vs自动创建的初始化类,并删除了一些并不需要的功能(只做了本地登录)。
public void Configuration(IAppBuilder app)
{
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
//app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
//app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
//app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
使用
主要围绕ApplicationUserManager
, ApplicationSignInManager
进行用户注册登录等操作,围绕ApplicationRoleManager
进行用户权限管理。
注册
基本逻辑还是来自vs默认框架的注册,但是增加了自定义的一些功能。
这里我增加了用户的角色设置,并且定义了第一个注册的用户为管理员,之后的为普通用户。
应用中,其他地方会通过用户角色来判断可以进行的操作。
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
try
{
var user = new ApplicationUser
{
UserName = model.UserName,
Email = model.Password,
IsEnabled = false, //default disabed
LastLoginTime = DateTime.Now,
RegistrationTime = DateTime.Now
};
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
//init roles
if (!RoleManager.Roles.Any())
{
await RoleManager.CreateAsync(new IdentityRole(UserRole.NORMAL));
await RoleManager.CreateAsync(new IdentityRole(UserRole.ADMINISTRATOR));
}
var adminRoleId = (await RoleManager.FindByNameAsync(UserRole.ADMINISTRATOR)).Id;
var hasAdmin = UserManager.Users.Any(p => p.Roles.Any(x => x.RoleId == adminRoleId));
var userRole = UserRole.NORMAL;
if (!hasAdmin)
{
userRole = UserRole.ADMINISTRATOR;
//make the first admin enabled
user.IsEnabled = true;
await UserManager.UpdateAsync(user);
}
await UserManager.AddToRoleAsync(user.Id, userRole);
//let the user manual log in
//await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
// string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
// var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
// await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");
return RedirectToAction("Login", "Login");
}
AddErrors(result);
}
catch (System.Data.Entity.Validation.DbEntityValidationException e)
{
Console.WriteLine(e.ToString());
throw e;
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
登录
我在定义用户时,增加了用户登录时间和用户是否启用的自定义字段。
这里将两个字段都做了使用。
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindByNameAsync(model.UserName);
if (null == user)
{
ModelState.AddModelError("", "登录失败.");
return View(model);
}
if ((!await UserManager.IsInRoleAsync(user.Id, UserRole.ADMINISTRATOR)) && !user.IsEnabled)
{
ModelState.AddModelError("", "账户未启用,请联系管理员.");
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
user.LastLoginTime = DateTime.Now;
await UserManager.UpdateAsync(user);
return RedirectToLocal(returnUrl);
default:
ModelState.AddModelError("", "登录失败.");
return View(model);
}
}
一些扩展使用
重置密码
比较安全的重置密码逻辑是通过邮箱、手机等方式,但是我在系统中,使用的是最简单的管理员直接设置的方式。
更常见的做法可能是将token通过连接的方式发送给用户,用户通过连接来手动修改密码的。
[HttpPost]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
if (!IsAdmin && !model.UserName.Equals(User.Identity.Name))
{
ModelState.AddModelError("", "无效的用户名");
return View();
}
var user = await UserManager.FindByNameAsync(model.UserName);
if (user == null)
{
return View();
}
var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var result = await UserManager.ResetPasswordAsync(user.Id, token, model.Password);
if (!result.Succeeded)
{
AddErrors(result);
}
else
{
ViewBag.msg = "修改成功!";
}
return View();
}
权限控制
其实可以通过基于Claim的方式,不过系统比较简单,通过Claim来控制稍显复杂,因此我直接使用了Role的方式。
有两种方法,第一种是在Controller/WebViewPage中的User属性里通过User.IsInRole(UserRole.ADMINISTRATOR);
方法判断用户权限,这里传入的是Role是名称,而非RoleId。
另一种是ApplicationUserManager的UserManager.GetRoles(user.Id)
方法,可以得到用户的所有的Role的Name,再逐一判断所需的Role。
问题
MySql连接不成功
有时候NuGet会为系统安装8.xx版本的MySql.Data
,搜了一下,发现不少人在这个版本上碰到了问题,降级到6.xx版本即可。
RememberMe功能
这个问题尚未解决,当用户登录调用
var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false);
这里的第三个参数,是设置登录的有效期,理论上设置为false
,则Cookie的效期是CurrentSession,而true
的话,会是比较长的一段时间。
而实际上发现,传入true
,过期时间是15天左右(这个比较正常);而传入false
,有效期会是N/A
,永不过期。目前还没有发现解决的方法。