手头一个小应用项目,非常非常小。。。

主要功能也就是提供一个网页页面浏览数据,数据内容部分在数据库中,图像部分在阿里OSS对象存储中;

最最前期采用ASP.NET WebForm做过一些WEB应用开发,再后来学习过ASP.NET MVC4,一直也没有机会实战;

通过近期的学习考虑,准备本次小应用直接通过ASP.NET Core最新版 3.1 来架构,虽然有点大马拉小车,但也是个难得的实践机会;

碰到的第一个问题就是,小应用虽小,但也是一个内部应用项目,并不是对外部用户全开放的,需要为用户配置登录账号,用户通过账号登录后才可以进行页面浏览查询;

以前了解过MemberShip 和MVC4 框架下的Identity ,总体来说,当时总觉得好像逻辑好复杂,自定义部分很不顺(当然也可能是没深入研究的原因);

而完全自己开发一个网页应用的论证登录框架好像有点太过了,目前还没那能力,个人还是以开发实际功能应用为主的,并不是框架开发高手;

所以,本次应用准备采用ASP.NET Core 3.1自带的Identity 做用户管理部分框架,深入学习Identity ,并考虑一些简单扩展(例如:管理员对用户的管理、批量导入新建等等)

准备环境: VS2019 版本号:16.6.2 , ASP.NET Core 版本 3.1

学习一:研究默认解决方案自带个人认证

 第一步:新建一个带个人认证的ASP.NET Core Web应用程序

 项目命名为 MyCoreTest1

 

 选择 Web应用程序(MVC) (该架构也可以加入Restful WEB API的内容,比较符合我的应用要求)

然后在右侧身份验证点击更改 -> 选择 个人用户账户 -> 选择默认的存储应用内的用户账户 最后点击 确认按钮:

注:选择了身份验证后,会自动只能选择HTTPS,后面会有几个窗口需要点确定(好像是和SSL证书相关的)

最后点创建按钮,就自动把一个项目建好了:

 

 

 

直接跑起来看看吧,按F5 或者 Ctrl+F5:

 

 

 标题栏右侧 有两个按钮,分别为 Register 和 Login,既然是全新的应用,目前肯定没用户,那就点Register按钮试试看:

 

 默认Identity框架用email 作为用户账号,密码有一定的安全策略(必须超过6位、必须带大写字符、小写字符、数字、特殊字符)

 

 

 

 

 输入账号: test@test.com ,密码 P@ssw0rd ,然后点 Register按钮,出现一个数据库错误;

当然是因为还没有建数据库的原因,直接点击中间的 Apply Migrations 来让应用自动创建数据库;

 

 按钮旁边出现: Try refreshing the page ,则表示数据库创建好了,(后面会提到数据库创建到哪里去了。),

 

 按F5刷新页面,会进入 注册确认 页面,默认框架对于用于注册必须有一个确认动作,主要是预留给有 邮件确认需求的;(即用来确认用户是否真的拥有这个邮箱)

如果不在这个页面点击 Click here to confirm your account,会发生能登记账号,但不能登录的情况:

 

 

 点击后,出现 Confirm email 信息,表示用户已确认:

 

 这个时候点击右上角  Login :  输入刚才登记的用户信息: (test@test.com , 密码:P@ssw0rd)

 

登录后,右上角出现 注册用户email ,则表示登录成功:

 

 哇,真的很神奇,一句代码没写,一个基本用户注册、登录功能全实现了!

现在,查看一下数据库被建在哪里了,以及为啥会被建到那里,打开 视图 --> SQL Server 对象资源管理器

 

 

原来数据库被建在Localdb 里了:

 

打开后,可以看到已经建立如下表:

 

 打开AspNetUsers 表可以看到里面刚才新建的一个用户:

 

 去查看一下项目中的 appsettings.json 配置文件,其中定义了数据库连接字符串 (DefaultConnection):

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-MyCoreTest1-3DF9EC28-CCC2-495F-AF13-FE6E503EC9F6;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

同时在Startup.cs中有一段代码定义使用这个字符串:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddControllersWithViews();
            services.AddRazorPages();
        }

 可根据需要修改字符串名 及 数据库的字符串值 来连接自己希望的数据库;

先再建个新的空数据库,起名:MyCoreTestDB1

 

 

 

 在appsettings.json 配置文件增加一个名为 MyConnection 连接字符串:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-MyCoreTest1-3DF9EC28-CCC2-495F-AF13-FE6E503EC9F6;Trusted_Connection=True;MultipleActiveResultSets=true",
    "MyConnection": "Server=(localdb)\\mssqllocaldb;Database=MyCoreTestDB1;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("MyConnection")));
            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddControllersWithViews();
            services.AddRazorPages();
        }

然后再跑起来看看,点击 Login

 

 

就出现了数据库表不存在的错误,继续点击 Apply Migrations 后再刷新,显示 Invalid Login attempt (当然啦,数据库是新的啦,没有用户)

 

 

 按上面注册用户步骤,完成用户注册及用户确认,然后再次登录:

默认建好的带个人认证的解决方案,目录结构如下:

 

发现一个问题,那些 注册用户、登录页面 代码在哪里呢? (登录界面还能看到有个View:  _LoginPartial.cshtml ,注册用户页面直接啥也没有) 

在 Identity 的 Areas 里只有一个Page: _ViewStart.cshtml。。。

我只能猜测应该是封装后,编译到DLL里了。

那么问题来了,如果我觉得这些默认页面不是我想要的,或者说上面一些信息我需要修改一下,怎么办?

右键点击 项目: MyCoreTest1 然后点击  添加 --> 新搭建基架的项目

 

 左侧选择 标识 ,然后中间选择 标识 (这NB的翻译。。。)

会自动搭建基架 ,安装一些NuGet包:

 

 弹出了,可以重载的内容: (翻译成替代,我也真服了) 

 这次我就先重载 注册页 和 登录页,然后数据上下文类也先用原本的: (会发现没办法点用户类旁边的加号,也即不能用自己定义的用户类,后面再说怎么用自己定义的用户类)

点 添加后,VS自动开始加载代码:

 

 

 

 首先会弹出一个ReadMe: 这个链接可以去看看: https://go.microsoft.com/fwlink/?linkid=2116645

 

资源管理器里可以看到Areas -> Identity -> Pages 下加入了Account 目录, 有了Login.cshtml 和 Register.cshtml

 双击 Login.cshtml会发现 这个不是一个原来理解的View ,而是 ASP.NET Core 新的一种类似于原来aspx的开发方式: Razor Pages

在此代码基础上就可以进行修改来实现自己的一些要求或设想;

首先,我目前还不考虑第三方外部认证方式,直接就把页面上相关显示的DIV删除:

@page
@model LoginModel

@{
    ViewData["Title"] = "Log in";
}

<h1>@ViewData["Title"]</h1>
<div class="row">
    <div class="col-md-4">
        <section>
            <form id="account" method="post">
                <h4>Use a local account to log in.</h4>
                <hr />
                <div asp-validation-summary="All" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Input.Email"></label>
                    <input asp-for="Input.Email" class="form-control" />
                    <span asp-validation-for="Input.Email" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.Password"></label>
                    <input asp-for="Input.Password" class="form-control" />
                    <span asp-validation-for="Input.Password" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <div class="checkbox">
                        <label asp-for="Input.RememberMe">
                            <input asp-for="Input.RememberMe" />
                            @Html.DisplayNameFor(m => m.Input.RememberMe)
                        </label>
                    </div>
                </div>
                <div class="form-group">
                    <button type="submit" class="btn btn-primary">Log in</button>
                </div>
                <div class="form-group">
                    <p>
                        <a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>
                    </p>
                    <p>
                        <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
                    </p>
                    <p>
                        <a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
                    </p>
                </div>
            </form>
        </section>
    </div>    
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

好了,页面清爽一点了:

 

 

 下一步,我想用户注册不需要强制用Email注册

打开 Register.cshtml.cs 在 InputModel中增加一个UserName 字段,必须输入、长度1-20,显示名为 账号:

同时修改  OnPostAsync 方法中一个赋值,将UserName直接赋值给UserName,而不是把email赋值给UserName;

        public class InputModel
        {
            [Required]
            [StringLength(20, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
            [Display(Name = "账号")]
            public string UserName { get; set; }

            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }
        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = new IdentityUser { UserName = Input.UserName, Email = Input.Email };
                var result = await _userManager.CreateAsync(user, Input.Password);
                //.........省略其他下面的代码
            }

修改 Register.cshtml 增加一项输入,并且删除 第三方外部用户注册部分的DIV:

@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Create a new account.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Input.UserName"></label>
                <input asp-for="Input.UserName" class="form-control" />
                <span asp-validation-for="Input.UserName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            //....省略下面一些代码
        </form>
    </div>    
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

跑起来后,可以采用账号 + Email + 密码 来注册账号,(这里没有把Email的强制输入关闭,主要是后面还有个确认页面是传送email过去的,先留着这个强制了)

 

 但是发现这个用户虽然注册成功,但是在登录页面无法登录,email也不行,账号也不行。。。

通过查看Login页面的OnPostAsync 方法中验证用户的函数调用,才发现是硬代码把 email输入当做 username来验证了,是因为原先的框架 username 等于 email 

 

 知道原因就好修改了,先修改InputModel:

        public class InputModel
        {
            [Required]
            [Display(Name = "账号")]
            public string UserName { get; set; }

            [Required]
            [DataType(DataType.Password)]
            public string Password { get; set; }

            [Display(Name = "Remember me?")]
            public bool RememberMe { get; set; }
        }
var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);

再修改一下页面:

@page
@model LoginModel

@{
    ViewData["Title"] = "Log in";
}

<h1>@ViewData["Title"]</h1>
<div class="row">
    <div class="col-md-4">
        <section>
            <form id="account" method="post">
                <h4>Use a local account to log in.</h4>
                <hr />
                <div asp-validation-summary="All" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Input.UserName"></label>
                    <input asp-for="Input.UserName" class="form-control" />
                    <span asp-validation-for="Input.UserName" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.Password"></label>
                    <input asp-for="Input.Password" class="form-control" />
                    <span asp-validation-for="Input.Password" class="text-danger"></span>
                </div>
                //.....省略下面一些代码
            </form>
        </section>
    </div>    
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

 搞定,用testuser账号登录:

下一步,想修改一下取消email确认,(至于完善email确认,在后期再补充考虑),并且在用户注册后,直接自动Login 进入;

注释掉 和邮件确认相关的代码,并且把新建一个IdentityUser中直接硬编码 EmailConfirmed = true

再把SignInAsync 第2个参数改为 True,即创建完用户就直接登录:

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            //ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = new IdentityUser { UserName = Input.UserName, Email = Input.Email, EmailConfirmed = true };
                var result = await _userManager.CreateAsync(user, Input.Password);
                if (result.Succeeded)
                {
                    //_logger.LogInformation("User created a new account with password.");

                    //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    //code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    //var callbackUrl = Url.Page(
                    //    "/Account/ConfirmEmail",
                    //    pageHandler: null,
                    //    values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
                    //    protocol: Request.Scheme);

                    //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    //    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    //if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    //{
                    //    return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    //}
                    //else
                    //{
                    await _signInManager.SignInAsync(user, isPersistent: true);
                    return LocalRedirect(returnUrl);
                    //}
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

 

新建后,直接就登录了:

 

在一开始没有全部重载Identity的文件情况下,如果还需对其他文件进行重载,可以继续 右键点击 项目: MyCoreTest1 然后点击  添加 --> 新搭建基架的项目

选择添加标识,然后会从基架检索信息:

 

 

 

再次出现选择 标识 可以重载的文件:

 

 

  这次先把 用户自己维护信息的 Manage\Index加一下,加完以后,Areas->Identity->Pages->Account 多了一个Manage 目录: 下面就有Index.cshtml

 默认  /Identity/Account/Manage/ 页面为: (可以修改手机号码、email、密码、双因素认证、以及个人账号信息的下载或删除)

 

其中 双因素认证,本次先不考虑,则准备先去除:

直接打开 \Areas\Identity\Pages\Account\Manage\_ManageNav.cshtml,然后注释掉一些代码:

@inject SignInManager<IdentityUser> SignInManager
@{
    var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
}
<ul class="nav nav-pills flex-column">
    <li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
    <li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
    <li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
    @*@if (hasExternalLogins)
    {
        <li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
    }
    <li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>*@
    <li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
</ul>

修改后:

 

后续准备找份详细资料,能够罗列出 Identity 下所有各文件选项对应功能意义,这样就可以根据自己实际情况来考虑是不是要重载。