.net framework的Identity使用

这两天回想之前做的项目,认证一直是比较头痛的地方。
于是抽出时间看了一些内容,从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,永不过期。目前还没有发现解决的方法。

上一篇:【Redis 分布式锁】(2)好用一点的“锁”


下一篇:Mutable and immutable data types in Python