引言:
前幾篇初步演示了基於.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"); } //設置登錄頁面為/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"); return; //驗證不通過就不繼續向下執行了 } bool isRemember = rememberMe == "1" ? true : false; //還是使用 Lambda 表達式來找匹配的用戶 TUser user = _context.TUsers.FirstOrDefault<TUser>(user => user.SigninAcc == userAcc && user.SignPwd == userPwd && user.IsValid==1); //驗證不通過跳轉到登錄頁繼續登錄 if (user==null || user.UserId<1) { Response.Redirect("/Auth/Signin"); return;//驗證不通過就不繼續向下執行了 } //驗證通過后用以下四步完成登錄。 //第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步,用第1步中的信息生成身份標識(身份認證方式是 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的值傳遞過去,如下圖:
登錄成功后系統會自動獲取ReturnUrl參數的值(這里是 /Auth/DeptList)后進入【部門管理】頁面,如下:
如果是直接訪問登錄頁:
登錄成功后則進入首頁,如下:
最后,我們對系統之前的功能做以下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. 打開 DeptEdit.cshtml.cs 頁面 , 修改保存到日志表的這部分代碼(見紅色部分),
將 UserId 和 UserName 的取值由固定值改成取 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) //如果用戶已經登錄 {
//取出用戶帳號信息
//***注:如果我們登錄時寫入 Cookie 中的是 UserId 如: new Claim(ClaimTypes.Name, user.UserId),
//***那么這里取出來的將是 UserId , 即 Identity.Name 對應的是 ClaimTypes.Name 的值。
string userAcc = cp.Identity.Name;
//取出登錄時設置到 cookie 中的所有用戶信息 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;
//其他以此類推,登錄時設置到 cookie 中的值都可以用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; }
注:上面 userAcc 、 deptId 、 deptName這三個值雖然取出來了但是沒有用到,
這里只是為了演示如何通過 Cookie 設置值和取值,
實際項目中如果不需要 , 那么在登錄的時候就不用寫到 Cookie 中去了。