在《asp.net core認證與授權》中講解了固定和自定義角色授權系統許可權,其實我們還可以通過其他方式來授權,比如可以通過角色組,用戶名,生日等,但這些主要取決於ClaimTypes,其實我們也可以自定義鍵值來授權,這些統一叫策略授權,其中更強大的是,我們可以自定義授權Handler來達到靈活授權... ...
在《asp.net core認證與授權》中講解了固定和自定義角色授權系統許可權,其實我們還可以通過其他方式來授權,比如可以通過角色組,用戶名,生日等,但這些主要取決於ClaimTypes,其實我們也可以自定義鍵值來授權,這些統一叫策略授權,其中更強大的是,我們可以自定義授權Handler來達到靈活授權,下麵一一展開。
註意:下麵的代碼只是部分代碼,完整代碼參照:https://github.com/axzxs2001/Asp.NetCoreExperiment/tree/master/Asp.NetCoreExperiment/%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86/PolicyPrivilegeManagement
首先看基於角色組,或用戶名,或基於ClaimType或自定義鍵值等授權策略,這些都是通過Services.AddAuthorization添加,並且是AuthorizationOptions來AddPolicy,這裡策略的名稱統一用RequireClaim來命名,不同的請求的策略名稱各不相同,如用戶名時就用policy.RequireUserName(),同時,在登錄時,驗證成功後,要添加相應的Claim到ClaimsIdentity中:
Startup.cs
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddMvc(); 4 services.AddAuthorization(options => 5 { 6 //基於角色組的策略 7 options.AddPolicy("RequireClaim", policy => policy.RequireRole("admin", "system")); 8 //基於用戶名 9 //options.AddPolicy("RequireClaim", policy => policy.RequireUserName("桂素偉")); 10 //基於ClaimType 11 //options.AddPolicy("RequireClaim", policy => policy.RequireClaim(ClaimTypes.Country,"中國")); 12 //自定義值 13 // options.AddPolicy("RequireClaim", policy => policy.RequireClaim("date","2017-09-02")); 14 }).AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>{ 15 options.LoginPath = new PathString("/login"); 16 options.AccessDeniedPath = new PathString("/denied"); 17 }); 18 }
HomeController.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.Linq; 5 using System.Threading.Tasks; 6 using Microsoft.AspNetCore.Mvc; 7 using PolicyPrivilegeManagement.Models; 8 using Microsoft.AspNetCore.Authorization; 9 using Microsoft.AspNetCore.Authentication; 10 using Microsoft.AspNetCore.Authentication.Cookies; 11 using System.Security.Claims; 12 13 namespace PolicyPrivilegeManagement.Controllers 14 { 15 [Authorize(Policy = "RequireClaim")] 16 public class HomeController : Controller 17 { 18 public IActionResult Index() 19 { 20 return View(); 21 } 22 23 public IActionResult About() 24 { 25 ViewData["Message"] = "Your application description page."; 26 return View(); 27 } 28 29 public IActionResult Contact() 30 { 31 ViewData["Message"] = "Your contact page."; 32 return View(); 33 } 34 35 public IActionResult Error() 36 { 37 return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 38 } 39 [AllowAnonymous] 40 [HttpGet("login")] 41 public IActionResult Login(string returnUrl = null) 42 { 43 TempData["returnUrl"] = returnUrl; 44 return View(); 45 } 46 [AllowAnonymous] 47 [HttpPost("login")] 48 public async Task<IActionResult> Login(string userName, string password, string returnUrl = null) 49 { 50 var list = new List<dynamic> { 51 new { UserName = "gsw", Password = "111111", Role = "admin",Name="桂素偉",Country="中國",Date="2017-09-02",BirthDay="1979-06-22"}, 52 new { UserName = "aaa", Password = "222222", Role = "system",Name="測試A" ,Country="美國",Date="2017-09-03",BirthDay="1999-06-22"} 53 }; 54 var user = list.SingleOrDefault(s => s.UserName == userName && s.Password == password); 55 if (user != null) 56 { 57 //用戶標識 58 var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); 59 identity.AddClaim(new Claim(ClaimTypes.Sid, userName)); 60 identity.AddClaim(new Claim(ClaimTypes.Name, user.Name)); 61 identity.AddClaim(new Claim(ClaimTypes.Role, user.Role)); 62 identity.AddClaim(new Claim(ClaimTypes.Country, user.Country)); 63 identity.AddClaim(new Claim("date", user.Date)); 64 65 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); 66 if (returnUrl == null) 67 { 68 returnUrl = TempData["returnUrl"]?.ToString(); 69 } 70 if (returnUrl != null) 71 { 72 return Redirect(returnUrl); 73 } 74 else 75 { 76 return RedirectToAction(nameof(HomeController.Index), "Home"); 77 } 78 } 79 else 80 { 81 const string badUserNameOrPasswordMessage = "用戶名或密碼錯誤!"; 82 return BadRequest(badUserNameOrPasswordMessage); 83 } 84 } 85 [HttpGet("logout")] 86 public async Task<IActionResult> Logout() 87 { 88 await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 89 return RedirectToAction("Index", "Home"); 90 } 91 [AllowAnonymous] 92 [HttpGet("denied")] 93 public IActionResult Denied() 94 { 95 return View(); 96 } 97 } 98 }
上面的授權策略都相對簡單,單一,使用場景也很有限,就和固定角色授權如出一轍,其實可以用更好的來例用授權,那就是自定義授權Handler,我們在《asp.net core認證與授權》一文中,是通過中間件來達到自定義解色的,現在我們換個思路,通過自定義授權Handler來實現。
首先定義一個UserPermission,即用戶許可權實體類
1 /// <summary> 2 /// 用戶許可權 3 /// </summary> 4 public class UserPermission 5 { 6 /// <summary> 7 /// 用戶名 8 /// </summary> 9 public string UserName 10 { get; set; } 11 /// <summary> 12 /// 請求Url 13 /// </summary> 14 public string Url 15 { get; set; } 16 }
接下來定義一個PermissionRequirement,為請求條件實體類
1 /// <summary> 2 /// 必要參數類 3 /// </summary> 4 public class PermissionRequirement : IAuthorizationRequirement 5 { 6 /// <summary> 7 /// 用戶許可權集合 8 /// </summary> 9 public List<UserPermission> UserPermissions { get;private set; } 10 /// <summary> 11 /// 無許可權action 12 /// </summary> 13 public string DeniedAction { get; set; } 14 /// <summary> 15 /// 構造 16 /// </summary> 17 /// <param name="deniedAction">無許可權action</param> 18 /// <param name="userPermissions">用戶許可權集合</param> 19 public PermissionRequirement(string deniedAction, List<UserPermission> userPermissions) 20 { 21 DeniedAction = deniedAction; 22 UserPermissions = userPermissions; 23 } 24 }
再定義自定義授權Hanlder,我們命名為PermissionHandler,此類必需繼承AuthorizationHandler<T>,只用實現public virtual Task HandleAsync(AuthorizationHandlerContext context),些方法是用戶請求時驗證是否授權的主方法,所以實現與自定義角色中間件的Invoke很相似。
1 using Microsoft.AspNetCore.Authorization; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Security.Claims; 5 using System.Threading.Tasks; 6 7 namespace PolicyPrivilegeManagement.Models 8 { 9 /// <summary> 10 /// 許可權授權Handler 11 /// </summary> 12 public class PermissionHandler : AuthorizationHandler<PermissionRequirement> 13 { 14 /// <summary> 15 /// 用戶許可權 16 /// </summary> 17 public List<UserPermission> UserPermissions { get; set; } 18 19 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) 20 { 21 //賦值用戶許可權 22 UserPermissions = requirement.UserPermissions; 23 //從AuthorizationHandlerContext轉成HttpContext,以便取出表求信息 24 var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; 25 //請求Url 26 var questUrl = httpContext.Request.Path.Value.ToLower(); 27 //是否經過驗證 28 var isAuthenticated = httpContext.User.Identity.IsAuthenticated; 29 if (isAuthenticated) 30 { 31 if (UserPermissions.GroupBy(g => g.Url).Where(w => w.Key.ToLower() == questUrl).Count() > 0) 32 { 33 //用戶名 34 var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Sid).Value; 35 if (UserPermissions.Where(w => w.UserName == userName && w.Url.ToLower() == questUrl).Count() > 0) 36 { 37 context.Succeed(requirement); 38 } 39 else 40 { 41 //無許可權跳轉到拒絕頁面 42 httpContext.Response.Redirect("/denied"); 43 } 44 } 45 else 46 { 47 context.Succeed(requirement); 48 } 49 } 50 return Task.CompletedTask; 51 } 52 } 53 }
此次的Startup.cs的ConfigureServices發生了變化,如下
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddMvc(); 4 services.AddAuthorization(options => 5 { 6 //自定義Requirement,userPermission可從資料庫中獲得 7 var userPermission= new List<UserPermission> { 8 new UserPermission { Url="/", UserName="gsw"}, 9 new UserPermission { Url="/home/permissionadd", UserName="gsw"}, 10 new UserPermission { Url="/", UserName="aaa"}, 11 new UserPermission { Url="/home/contact", UserName="aaa"} 12 }; 13 14 options.AddPolicy("Permission", 15 policy => policy.Requirements.Add(new PermissionRequirement("/denied", userPermission))); 16 17 }).AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>{ 18 options.LoginPath = new PathString("/login"); 19 options.AccessDeniedPath = new PathString("/denied"); 20 21 }); 22 //註入授權Handler 23 services.AddSingleton<IAuthorizationHandler, PermissionHandler>(); 24 }
HomeController中代碼如下:
1 using System.Collections.Generic; 2 using System.Diagnostics; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Mvc; 6 using PolicyPrivilegeManagement.Models; 7 using Microsoft.AspNetCore.Authorization; 8 using Microsoft.AspNetCore.Authentication; 9 using Microsoft.AspNetCore.Authentication.Cookies; 10 using System.Security.Claims; 11 12 namespace PolicyPrivilegeManagement.Controllers 13 { 14 [Authorize(Policy = "Permission")] 15 public class HomeController : Controller 16 { 17 PermissionHandler _permissionHandler; 18 public HomeController(IAuthorizationHandler permissionHandler) 19 { 20 _permissionHandler = permissionHandler as PermissionHandler; 21 } 22 public IActionResult Index() 23 { 24 return View(); 25 } 26 27 public IActionResult PermissionAdd() 28 { 29 return View(); 30 } 31 32 [HttpPost("addpermission")] 33 public IActionResult AddPermission(string url,string userName) 34 { 35 //添加許可權 36 _permissionHandler.UserPermissions.Add(new UserPermission { Url = url, UserName = userName }); 37 return Content("添加成功"); 38 } 39 40 public IActionResult Contact() 41 { 42 ViewData["Message"] = "Your contact page."; 43 44 return View(); 45 } 46 47 public IActionResult Error() 48 { 49 return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 50 } 51 [AllowAnonymous] 52 [HttpGet("login")] 53 public IActionResult Login(string returnUrl = null) 54 { 55 TempData["returnUrl"] = returnUrl; 56 return View(); 57 } 58 [AllowAnonymous] 59 [HttpPost("login")] 60 public async Task<IActionResult> Login(string userName, string password, string returnUrl = null) 61 { 62 var list = new List<dynamic> { 63 new { UserName = "gsw", Password = "111111", Role = "admin",Name="桂素偉",Country="中國",Date="2017-09-02",BirthDay="1979-06-22"}, 64 new { UserName = "aaa", Password = "222222", Role = "system",Name="測試A" ,Country="美國",Date="2017-09-03",BirthDay="1999-06-22"} 65 }; 66 var user = list.SingleOrDefault(s => s.UserName == userName && s.Password == password); 67 if (user != null) 68 { 69 //用戶標識 70 var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); 71 identity.AddClaim(new Claim(ClaimTypes.Sid, userName)); 72 identity.AddClaim(new Claim(ClaimTypes.Name, user.Name)); 73 identity.AddClaim(new Claim(ClaimTypes.Role, user.Role)); 74 identity.AddClaim(new Claim(ClaimTypes.Country, user.Country)); 75 identity.AddClaim(new Claim("date", user.Date)); 76 77 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); 78 if (returnUrl == null) 79 { 80 returnUrl = TempData["returnUrl"]?.ToString(); 81 } 82 if (returnUrl != null) 83 { 84 return Redirect(returnUrl); 85 } 86 else 87 { 88 return RedirectToAction(nameof(HomeController.Index), "Home"); 89 } 90 } 91 else 92 { 93 const string badUserNameOrPasswordMessage = "用戶名或密碼錯誤!"; 94 return BadRequest(badUserNameOrPasswordMessage); 95 } 96 } 97 [HttpGet("logout")] 98 public async Task<IActionResult> Logout() 99 { 100 await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 101 return RedirectToAction("Index", "Home"); 102 } 103 [AllowAnonymous] 104 [HttpGet("denied")] 105 public IActionResult Denied() 106 { 107 return View(); 108 } 109 } 110 }
本例設計是當用戶gsw密碼111111登錄時,是不能訪問/home/contact的,剛登錄時訪該action是不成功的,這裡我們在/home/addpermission中添加一個Action名稱:/home/contact,用戶名:gsw的信息,此時再訪問/home/contact,會發現是可以訪問的,這是因為我們熱更新了PermissionHandler中的用戶許可權集合,用戶的許可權得到了擴展和變化。
其實用中間件能達到靈活許可權的設置,用自定義授權Handler也可以,接下來比較一下兩種做法的優劣:
|
中間件 |
自定義授權Handler |
用戶許可權集合 |
靜態對象 |
實體化對象 |
熱更新時 |
用中間件名稱.用戶許可權集合更新 |
因為在Startup.cs中,PermissionHandler是依賴註放的,可以在熱更新的構造中獲取並操作 |
性能方面 |
每個action請求都會觸發Invock方法,標記[AllowAnonymous]特性的Action也會觸發 |
只有標記[Authorize]特性的Action會觸發該方法,標記[AllowAnonymous]特性的Action不會觸發,性能更優化 |