• .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,永不过期。目前还没有发现解决的方法。

  • 相关阅读:
    变量提升
    前端UI框架和JS类库
    ES6---Map数据结构
    ES6---Set数据结构
    Array.from//Array.of的用法
    闭包的理解和应用场景
    vue-router 的用法
    原型链和作用域链的理解
    WordPress更换了域名 主页、文章、图片路径错误 解决办法
    wordpress 安装新的主题后启动后报错
  • 原文地址:https://www.cnblogs.com/mosakashaka/p/12608845.html
Copyright © 2020-2023  润新知