一、簡介
前后端分離的站點一般都會用jwt或IdentityServer4之類的生成token的方式進行登錄鑒權。這里要說的是小項目沒有做前后端分離的時站點登錄授權的正確方式。
二、傳統的授權方式
這里說一下傳統授權方式,傳統授權方式用session或cookies來完成。
1.在請求某個Action之前去做校驗,驗證當前操作者是否登錄過,登錄過就有權限
2.如果沒有權限就跳轉到登錄頁中去
3.傳統登錄授權用的AOP-Filter:ActionFilter。
具體實現為:
1.增加一個類CurrentUser.cs 保存用戶登錄信息
/// <summary> /// 登錄用戶的信息 /// </summary> public class CurrentUser { /// <summary> /// 用戶Id /// </summary> public int Id { get; set; } /// <summary> /// 用戶名稱 /// </summary> public string Name { get; set; } /// <summary> /// 賬號 /// </summary> public string Account { get; set; } }
2.建一個Cookice/Session幫助類CookieSessionHelper.cs

public static class CookieSessionHelper { public static void SetCookies(this HttpContext httpContext, string key, string value, int minutes = 30) { httpContext.Response.Cookies.Append(key, value, new CookieOptions { Expires = DateTime.Now.AddMinutes(minutes) }); } public static void DeleteCookies(this HttpContext httpContext, string key) { httpContext.Response.Cookies.Delete(key); } public static string GetCookiesValue(this HttpContext httpContext, string key) { httpContext.Request.Cookies.TryGetValue(key, out string value); return value; } public static CurrentUser GetCurrentUserByCookie(this HttpContext httpContext) { httpContext.Request.Cookies.TryGetValue("CurrentUser", out string sUser); if (sUser == null) { return null; } else { CurrentUser currentUser = Newtonsoft.Json.JsonConvert.DeserializeObject<CurrentUser>(sUser); return currentUser; } } public static CurrentUser GetCurrentUserBySession(this HttpContext context) { string sUser = context.Session.GetString("CurrentUser"); if (sUser == null) { return null; } else { CurrentUser currentUser = Newtonsoft.Json.JsonConvert.DeserializeObject<CurrentUser>(sUser); return currentUser; } } }
3.建一個登錄控制器AccountController.cs
public class AccountController : Controller { //登錄頁面 public IActionResult Login() { return View(); } //登錄提交 [HttpPost] public IActionResult LoginSub(IFormCollection fromData) { string userName = fromData["userName"].ToString(); string passWord = fromData["password"].ToString(); //真正寫法是讀數據庫驗證 if (userName == "test" && passWord == "123456") { #region 傳統session/cookies //登錄成功,記錄用戶登錄信息 CurrentUser currentUser = new CurrentUser() { Id = 123, Name = "測試賬號", Account = userName }; //寫sessin // HttpContext.Session.SetString("CurrentUser", JsonConvert.SerializeObject(currentUser)); //寫cookies HttpContext.SetCookies("CurrentUser", JsonConvert.SerializeObject(currentUser)); #endregion //跳轉到首頁 return RedirectToAction("Index", "Home"); } else { TempData["err"] = "賬號或密碼不正確"; //賬號密碼不對,跳回登錄頁 return RedirectToAction("Login", "Account"); } } /// <summary> /// 退出登錄 /// </summary> /// <returns></returns> public IActionResult LogOut() { HttpContext.DeleteCookies("CurrentUser"); //Session方式 // HttpContext.Session.Remove("CurrentUser"); return RedirectToAction("Login", "Account"); } }
4.登錄頁Login.cshtml 內容
<form action="/Account/LoginSub" method="post"> <div> 賬號:<input type="text" name="userName" /> </div> <div> 賬號:<input type="password" name="passWord" /> </div> <div> <input type="submit" value="登錄" /> <span style="color:#ff0000">@TempData["err"]</span> </div> </form>
5.建一個登錄成功跳轉到主頁控制器HomeController.cs
public class HomeController : Controller { public IActionResult Index() { //從cookie獲取用戶信息 CurrentUser user = HttpContext.GetCurrentUserByCookie(); //CurrentUser user = HttpContext.GetCurrentUserBySession(); return View(user); } }
6.頁面 Index.cshtml
@{ ViewData["Title"] = "Index"; } @model SessionAuthorized.Demo.Models.CurrentUser <h1>歡迎 @Model.Name 來到主頁</h1> <div><a href="/Account/Logout">退出登錄</a></div>
7.增加鑒權過濾器MyActionAuthrizaFilterAttribute.cs,實現IActinFilter,在OnActionExecuting中寫鑒權邏輯
public class MyActionAuthrizaFilterAttribute : Attribute, IActionFilter { public void OnActionExecuted(ActionExecutedContext context) { //throw new NotImplementedException(); } /// <summary> /// 進入action前 /// </summary> /// <param name="context"></param> public void OnActionExecuting(ActionExecutingContext context) { //throw new NotImplementedException(); Console.WriteLine("開始驗證權限..."); // CurrentUser currentUser = context.HttpContext.GetCurrentUserBySession(); CurrentUser currentUser = context.HttpContext.GetCurrentUserByCookie(); if (currentUser == null) { Console.WriteLine("沒有權限..."); if (this.IsAjaxRequest(context.HttpContext.Request)) { context.Result = new JsonResult(new { Success = false, Message = "沒有權限" }); } context.Result = new RedirectResult("/Account/Login");
return; } Console.WriteLine("權限驗證成功..."); } private bool IsAjaxRequest(HttpRequest request) { string header = request.Headers["X-Requested-With"]; return "XMLHttpRequest".Equals(header); } }
在需要鑒權的控制器或方法上加上這個Filter即可完成鑒權,這里在主頁中加入鑒權,登錄成功的用戶才能訪問
8.如果要用Session,還要在startup.cs中加入Session
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddSession(); }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseSession(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); }
到這里,傳統的鑒權就完成了,下面驗證一下效果。
三、 .NET5中正確的鑒權方式
傳統的授權方式是通過Action Filter(before)來完成的,上圖.Net Core的filter順序可以發現,Action filter(befre)之前還有很多個filter,如果可以在前把鑒權做了,就能少跑了幾步冤枉路,所以,正確的鑒權應該是在Authorization filter中做,Authorization filter是.NET5里面專門做鑒權授權用的。
怎么做呢,鑒權授權通過中間件支持。
1.在staup.cs的Configure方法里面的app.UseRouting();之后,在app.UseEndpoints()之前,增加鑒權授權;
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseSession(); app.UseRouting(); app.UseAuthentication();//檢測用戶是否登錄 app.UseAuthorization(); //授權,檢測有沒有權限,是否能夠訪問功能 app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); }
2.在ConfigureServices中增加
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); //services.AddSession(); 傳統鑒權 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = new PathString("/Account/Login");//沒登錄跳到這個路徑 }); }
3.標記哪些控制器或方法需要登錄認證,在控制器或方法頭標記特性[Authorize],如果里面有方法不需要登錄驗證的,加上匿名訪問標識 [AllowAnonymousAttribute]
// [MyActionAuthrizaFilterAttribute] 傳統授權 [Authorize] public class HomeController : Controller { public IActionResult Index() { //從cookie獲取用戶信息 // CurrentUser user = HttpContext.GetCurrentUserByCookie(); //CurrentUser user = HttpContext.GetCurrentUserBySession(); var userInfo = HttpContext.User; CurrentUser user = new CurrentUser() { Id = Convert.ToInt32(userInfo.FindFirst("id").Value), Name = userInfo.Identity.Name, Account=userInfo.FindFirst("account").Value }; return View(user); } /// <summary> /// 無需登錄,匿名訪問 /// </summary> /// <returns></returns> [AllowAnonymousAttribute] public IActionResult About() { return Content("歡迎來到關於頁面"); } }
4.登錄處AccountController.cs的代碼
public class AccountController : Controller { //登錄頁面 public IActionResult Login() { return View(); } //登錄提交 [HttpPost] public IActionResult LoginSub(IFormCollection fromData) { string userName = fromData["userName"].ToString(); string passWord = fromData["password"].ToString(); //真正寫法是讀數據庫驗證 if (userName == "test" && passWord == "123456") { #region 傳統session/cookies //登錄成功,記錄用戶登錄信息 //CurrentUser currentUser = new CurrentUser() //{ // Id = 123, // Name = "測試賬號", // Account = userName //}; //寫sessin // HttpContext.Session.SetString("CurrentUser", JsonConvert.SerializeObject(currentUser)); //寫cookies //HttpContext.SetCookies("CurrentUser", JsonConvert.SerializeObject(currentUser)); #endregion //用戶角色列表,實際操作是讀數據庫 var roleList = new List<string>() { "Admin", "Test" }; var claims = new List<Claim>() //用Claim保存用戶信息 { new Claim(ClaimTypes.Name,"測試賬號"), new Claim("id","1"), new Claim("account",userName),//...可以增加任意信息 }; //填充角色 foreach(var role in roleList) { claims.Add(new Claim(ClaimTypes.Role, role)); } //把用戶信息裝到ClaimsPrincipal ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Customer")); //登錄,把用戶信息寫入到cookie HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, new AuthenticationProperties { ExpiresUtc = DateTime.Now.AddMinutes(30)//過期時間30分鍾 }).Wait(); //跳轉到首頁 return RedirectToAction("Index", "Home"); } else { TempData["err"] = "賬號或密碼不正確"; //賬號密碼不對,跳回登錄頁 return RedirectToAction("Login", "Account"); } } /// <summary> /// 退出登錄 /// </summary> /// <returns></returns> public IActionResult LogOut() { // HttpContext.DeleteCookies("CurrentUser"); //Session方式 // HttpContext.Session.Remove("CurrentUser"); HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Login", "Account"); } }
5.驗證結果:
可以看到,一開始沒登錄狀態,訪問/Home/Index會跳轉到登錄頁面,訪問/Home/About能成功訪問,證明匿名訪問ok,
后面的登錄,顯示用戶信息,退出登錄也沒問題,證明功能沒問題,鑒權到這里就完成了。
這個自帶的登錄鑒權是利用cookie反解釋為用戶信息的,所以在站點多個服務器負載均衡也是沒問題的(本人已驗證nginx負載下沒問題)。
四、.NET5中角色授權
上面的claims中已經記錄了用戶角色,這個角色就可以用來做授權了。
在startup.cs中修改沒權限時跳轉頁面路徑
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); //services.AddSession(); 傳統鑒權 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = new PathString("/Account/Login");//沒登錄跳到這個路徑 options.AccessDeniedPath = new PathString("/Account/AccessDenied");//沒權限跳到這個路徑 }); }
AccountController.cs增加方法
public IActionResult AccessDenied() { return View(); }
視圖內容
沒有權限訪問-401
1.單個角色訪問權限
在方法頭加上特性 [Authorize(Roles ="角色代碼")]
在HomeController.cs中增加一個方法
/// <summary> /// 角色為Admin能訪問 /// </summary> /// <returns></returns> [Authorize(Roles ="Admin")] public IActionResult roleData1() { return Content("Admin能訪問"); }
驗證。
開始角色為
訪問roleData1數據:
訪問成功,然后把角色Admin去掉
var roleList = new List<string>() { //"Admin", "Test" };
重新登錄,在訪問rleData1數據:
訪問不成功,跳轉到預設的沒權限的頁面了。
2.“多個角色包含一個”權限
[Authorize(Roles = "Admin,Test")]//多個角色用逗號隔開,角色包含有其中一個就能訪問 public IActionResult roleData2() { return Content("roleData2訪問成功"); }
3.“多個角色組合”權限
/// <summary> /// 同時擁有標記的全部角色才能訪問 /// </summary> /// <returns></returns> [Authorize(Roles = "Admin")] [Authorize(Roles = "Test")] public IActionResult roleData3() { return Content("roleData3訪問成功"); }
五、自定義策略授權
上面的角色授權的缺點在哪里呢,最大的缺點就是角色要提前寫死到方法上,如果要修改只能改代碼,明顯很麻煩,實際項目中權限都是根據配置修改的,
所以就要用到自定義策略授權了。
第一步:
增加一個CustomAuthorizatinRequirement.cs,要求實現接口:IAuthorizationRequirement
/// <summary>
/// 策略授權參數
/// </summary>
public class CustomAuthorizationRequirement: IAuthorizationRequirement
{
/// <summary>
///
/// </summary>
public CustomAuthorizationRequirement(string policyname) { this.Name = policyname; } public string Name { get; set; } }
增加CustomAuthorizationHandler.cs------專門做檢驗邏輯的;要求繼承自AuthorizationHandler<>泛型抽象類;
/// <summary> /// 自定義授權策略 /// </summary> public class CustomAuthorizationHandler: AuthorizationHandler<CustomAuthorizationRequirement> { public CustomAuthorizationHandler() { } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement) { bool flag = false; if (requirement.Name == "Policy01") { Console.WriteLine("進入自定義策略授權01..."); ///策略1的邏輯 } if (requirement.Name == "Policy02") { Console.WriteLine("進入自定義策略授權02..."); ///策略2的邏輯 } if(flag) { context.Succeed(requirement); //驗證通過了 } return Task.CompletedTask; //驗證不同過 } }
第二步,讓自定義的邏輯生效。
starup.cs的ConfigureServices方法中注冊進來
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); //services.AddSession(); 傳統鑒權 services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>(); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = new PathString("/Account/Login");//沒登錄跳到這個路徑 options.AccessDeniedPath = new PathString("/Account/AccessDenied");//沒權限跳到這個路徑 }); services.AddAuthorization(optins => { //增加授權策略 optins.AddPolicy("customPolicy", polic => { polic.AddRequirements(new CustomAuthorizationRequirement("Policy01") // ,new CustomAuthorizationRequirement("Policy02") ); }); }); }
第三步,把要進授權策略的控制器或方法增加標識
HomeContrller.cs增加測試方法
/// <summary> /// 進入授權策略 /// </summary> /// <returns></returns> [Authorize(policy: "customPolicy")] public IActionResult roleData4() { return Content("自定義授權策略"); }
訪問roleData4,看是否進到自定義授權策略邏輯
可以看到自定義授權策略生效了,授權策略就可以在這里做了,下面加上授權邏輯。
我這里的權限用路徑和角色關聯授權,加上授權邏輯后的校驗代碼。
/// <summary> /// 自定義授權策略 /// </summary> public class CustomAuthorizationHandler : AuthorizationHandler<CustomAuthorizationRequirement> { public CustomAuthorizationHandler() { } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement) { bool flag = false; //把context轉換到httpConext,方便取上下文 HttpContext httpContext = context.Resource as HttpContext; string path = httpContext.Request.Path;//當前訪問路徑,例:"/Home/roleData4" var user = httpContext.User; //用戶id string userId = user.FindFirst("id")?.Value; if (userId == null) { //沒登錄,直接結束 return Task.CompletedTask; } //登錄成功時根據角色查出來這個用戶的全部角色的菜單權限中的url地址,存到redis,這里實際是根據用戶id從redis查詢出來url地址。 List<string> paths = new List<string>() { "/Home/roleData4", "/Home/roleData3" }; if (requirement.Name == "Policy01") { Console.WriteLine("進入自定義策略授權01..."); ///策略1的邏輯 if (paths.Contains(path)) { flag = true; } } if (requirement.Name == "Policy02") { Console.WriteLine("進入自定義策略授權02..."); ///策略2的邏輯 } if (flag) { context.Succeed(requirement); //驗證通過了 } return Task.CompletedTask; //驗證不同過 } }
加上邏輯后再訪問。
訪問成功,自定義授權策略完成。
源代碼:https://github.com/weixiaolong325/SessionAuthorized.Demo