引言:
前几篇初步演示了基于.net core Razor 的 Web 框架进行开发的规则和风格,
以及如何使官方的ORM框架EntityFrameworkCore做数据库的交互,
.net5.0 core(底层平台) + Razor(终端交互) + EF Core(数据库访问) + MySql(数据库服务)这样
一个组合运作起来还是很简洁高效的。基于.net core的跨平台底层 + Razor(PC端) + WebApi(数据服务)的组合
算是回归到 Web 的本源了,能适配大部分企业级的应用场景,很好、很强大,笔者很喜欢。
本篇介绍如何实现登录功能。
具体来说就是用户访问Auth下的页面的时候需要先登录后才能访问,其他页面不受限制。
实现这样一个业务场景只需要几个简单配置和少许编码就可以了,具体步骤如下:
1. Startup.cs中配置如下:
ConfigureServices(IServiceCollection services)方法加入如下代码(见红色部分):
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(options => { //设置访问Auth文件夹下的页面都需要经过验证。 options.Conventions.AuthorizeFolder("/Auth"); //因为 Signin.cshtml 是登录页,所以可以匿名访问。 options.Conventions.AllowAnonymousToPage("/Auth/Signin"); //上面两行设置可以用下面的这行替换(链式调用) //options.Conventions.AuthorizeFolder("/Auth").AllowAnonymousToPage("/Auth/Signin"); }); //身份验证,设置身份验证方式为 Cookie 验证(网页应用程序当然只适合用 cookie) services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie( options => { options.LoginPath = new PathString("/Auth/Signin"); } ); //for SQL Server //services.AddDbContext<AuthDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MySql"))); //for MySql services.AddDbContext<AuthDbContext>(options => options.UseMySql( Configuration.GetConnectionString("MySql"),ServerVersion.AutoDetect(Configuration.GetConnectionString("MySql")))); }
Configure(IApplicationBuilder app, IWebHostEnvironment env) 方法加入如下代码(见红色部分):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); //启用身份验证,加上这句才能生效, //注意顺序,在 app.UseRouting(); 和 app.UseAuthorization();之间 app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); }
2. 在 Auth 文件夹下新增登录页面 Signin.cshtml ,代码如下:
Signin.cshtml中代码:
@page @model AuthManagement.Pages.Auth.SigninModel @{ ViewData["Title"] = "用户登录"; } <form method="post"> <table style="margin-left:160px; background-color:#f7f7f7;border:solid 1px #c0c0c0;"> <tr> <td style="padding:10px 30px 0 30px;">账号:<br /> <input type="text" name="acc" /></td> </tr> <tr> <td style="padding:10px 30px 0 30px;">密码:<br /> <input type="password" name="pwd" /></td> </tr> <tr> <td style="padding: 10px 30px 10px 30px;"> <input type="checkbox" name="rememberMe" value="1" /> 记住我 <button type="submit">登录</button> </td> </tr> </table> </form>
页面长相如下:
Signin.cshtml.cs 文件代码如下:
namespace AuthManagement.Pages.Auth { public class SigninModel : PageModel { private readonly AuthDbContext _context; //构造函数中对AuthDbContext做依赖注入 public SigninModel(AuthDbContext context) { _context = context; } public void OnGet() { } // Signin.cshtml 文件中的form不需要指定action,以 Post 方式提交数据我们只要按约定实现 OnPost() 方法就可以了。 public async Task OnPost() { string userAcc = Request.Form["acc"];//接收页面传递过来的账号 string userPwd = Request.Form["pwd"];//接收页面传递过来的密码 string rememberMe = Request.Form["rememberMe"];//如果勾选=1,否则=null // 对账号密码做一下基本的非空判断可以减少数据库操作。 if (string.IsNullOrWhiteSpace(userAcc) || string.IsNullOrWhiteSpace(userPwd)) { Response.Redirect("/Auth/Signin"); } bool isRemember = rememberMe == "1" ? true : false;
//还是使用 Lambda 表达式来找匹配的用户 TUser user = _context.TUsers.Single<TUser>(user => user.SigninAcc == userAcc && user.SignPwd == userPwd); //验证不通过跳转到登录页继续登录 if (user==null || user.UserId<1) { Response.Redirect("/Auth/Signin"); } //验证通过后用以下四步完成登录。 //第1步,设置 Cookie中要包含的用户信息 List<Claim> claims = new List<Claim> { //特别注意这一行,如果不设置那么 HttpContext.User.Identity.Name 将为 null new Claim(ClaimTypes.Name, user.SigninAcc), //下面这几行随意,根据需要添加 new Claim("UserId", user.UserId.ToString()), new Claim("UserName", user.UserName), new Claim("DeptId", user.DeptId.ToString()), new Claim("DeptName", user.DeptName) }; //第2步,指定身份认证方式 这里使用 cookie 认证 ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); //第3步,设置 cookie 属性,比如过期时间,是否持久化等 string returnUrl = Request.Query["ReturnUrl"]; AuthenticationProperties authProperties = new AuthenticationProperties { IsPersistent = isRemember,//是否持久化 //如果用户点“登录“进来,登录成功后跳转到首页,否则跳转到上一个页面 RedirectUri = string.IsNullOrWhiteSpace(returnUrl) ? "/Index" : returnUrl, ExpiresUtc = DateTime.UtcNow.AddMonths(1) //设置 cookie 过期时间:一个月后过期 }; //第4步,调用 HttpContext.SignInAsync() 方法生成一个加密的 cookie 并输出到浏览器。 await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties ); } } }
至此,一个标准的 cookie 登录流程就完成了,当用户浏览网站的时候,如果点 【部门管理】将自动跳转到登录页面,
同时将【部门管理】的网址作为查询参数ReturnUrl的值传递过去,如下图:
登录成功后直接进入【部门管理】页面,如下:
如果是直接访问登录页:
登录成功后则进入首页,如下:
最后,我们对系统做以下2点完善:
1. 在页面右上角加上登录(未登录)和登出(已登录)的按钮。
2. 修改部门保存到日志表的时候 UserId 和 UserName 我们取cookie中记录的用户编号和用户名称而不是写死。
代码如下:
1. 打开_Layout.cshtml 文件加上 登录(未登录)和登出(已登录)按钮,见红色部分代码:
<ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Auth/DeptList">【部门管理】</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a> </li> <li class="nav-item"> @if (Context.User.Identity.IsAuthenticated) //已登录显示[登出]按钮 { <a class="nav-link text-dark" asp-area="" asp-page="/Auth/Signout">[登出]</a> } else { <a class="nav-link text-dark" asp-area="" asp-page="/Auth/Signin">[登录]</a> } </li> </ul>
在 Auth 文件夹下新增 Signout.cshtml 页面,Signout.cshtml.cs 中代码如下:
namespace AuthManagement.Pages.Auth { public class SignoutModel : PageModel { public async Task OnGet() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); // 登出后跳转到首页 Response.Redirect("/Index"); } } }
2. 修改部门保存到日志表的时候取 cookie中的 UserId 和 UserName的值 ,见红色部分代码:
/// <summary> /// 将更改前和更改后的数据保存到t_log表 /// </summary> /// <returns></returns> private List<TLog> GenerateLog() { int userId = 0; string userName = "未知"; //取出登录时设置的用户信息 ClaimsPrincipal cp = HttpContext.User; if (cp.Identity.IsAuthenticated) //如果用户已经登录 { //取出登录时设置的所有用户信息 List<Claim> claims = cp.Claims.ToList<Claim>(); //通过传入 Lambda 表达式找出登录时设置的 UserId 值 string uid = claims.Single<Claim>(option => option.Type == "UserId").Value; userId = Convert.ToInt32(uid); //通过传入 Lambda 表达式找出登录时设置的 UserName 值 userName = claims.Single<Claim>(option => option.Type == "UserName").Value;
//其他以此类推,登录时设置的值,这里就可以用Lambda表达式取出来用 string deptId = claims.Single<Claim>(option => option.Type == "DeptId").Value; string deptName = claims.Single<Claim>(option => option.Type == "DeptName").Value; } string batchNo = Guid.NewGuid().ToString(); TLog beforeLog = new TLog { UserId = userId, UserName = userName, BatchNo = batchNo, TableName = "t_dept", TableData = "", LogTime = DateTime.Now }; TLog afterLog = new TLog { UserId = userId, UserName = userName, BatchNo = batchNo, TableName = "t_dept", TableData = "", LogTime = DateTime.Now }; List<TLog> logList = new List<TLog>(); logList.Add(beforeLog); logList.Add(afterLog); return logList; }