在《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
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthorization(options => { //基于角色的策略 options.AddPolicy("RequireClaim", policy => policy.RequireRole("admin", "system")); //基于用户名 //options.AddPolicy("RequireClaim", policy => policy.RequireUserName("桂素伟")); //基于Claim //options.AddPolicy("RequireClaim", policy => policy.RequireClaim(ClaimTypes.Country,"中国")); //自定义值 // options.AddPolicy("RequireClaim", policy => policy.RequireClaim("date","2017-09-02")); }).AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>{ options.LoginPath = new PathString("/login"); options.AccessDeniedPath = new PathString("/denied"); }); }
HomeController.cs
using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using PolicyPrivilegeManagement.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; namespace PolicyPrivilegeManagement.Controllers { [Authorize(Policy = "RequireClaim")] public class HomeController : Controller { PermissionHandler _permissionHandler; public HomeController(IAuthorizationHandler permissionHandler) { _permissionHandler = permissionHandler as PermissionHandler; } public IActionResult Index() { return View(); } public IActionResult PermissionAdd() { return View(); } public IActionResult Contact() { ViewData["Message"] = "Your contact page."; return View(); } public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } [AllowAnonymous] [HttpGet("login")] public IActionResult Login(string returnUrl = null) { TempData["returnUrl"] = returnUrl; return View(); } [AllowAnonymous] [HttpPost("login")] public async Task<IActionResult> Login(string userName, string password, string returnUrl = null) { var list = new List<dynamic> { new { UserName = "gsw", Password = "111111", Role = "admin",Name="桂素伟",Country="中国",Date="2017-09-02",BirthDay="1979-06-22"}, new { UserName = "aaa", Password = "222222", Role = "system",Name="测试A" ,Country="美国",Date="2017-09-03",BirthDay="1999-06-22"} }; var user = list.SingleOrDefault(s => s.UserName == userName && s.Password == password); if (user != null) { //用户标识 var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); identity.AddClaim(new Claim(ClaimTypes.Sid, userName)); identity.AddClaim(new Claim(ClaimTypes.Name, user.Name)); identity.AddClaim(new Claim(ClaimTypes.Role, user.Role)); identity.AddClaim(new Claim(ClaimTypes.Country, user.Country)); identity.AddClaim(new Claim("date", user.Date)); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); if (returnUrl == null) { returnUrl = TempData["returnUrl"]?.ToString(); } if (returnUrl != null) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); } } else { const string badUserNameOrPasswordMessage = "用户名或密码错误!"; return BadRequest(badUserNameOrPasswordMessage); } } [HttpGet("logout")] public async Task<IActionResult> Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index", "Home"); } [AllowAnonymous] [HttpGet("denied")] public IActionResult Denied() { return View(); } } }
上面的授权策略都相对简单,单一,使用场景也很有限,就和固定角色授权如出一辙,其实可以用更好的来例用授权,那就是自定义授权Handler,我们在《asp.net core认证与授权》一文中,是通过中间件来达到自定义解色的,现在我们换个思路,通过自定义授权Handler来实现。
首先定义一个UserPermission,即用户权限实体类
/// <summary> /// 用户权限 /// </summary> public class UserPermission { /// <summary> /// 用户名 /// </summary> public string UserName { get; set; } /// <summary> /// 请求Url /// </summary> public string Url { get; set; } }
接下来定义一个PermissionRequirement,为请求条件实体类
/// <summary> /// 必要参数类 /// </summary> public class PermissionRequirement : IAuthorizationRequirement { /// <summary> /// 用户权限集合 /// </summary> public List<UserPermission> UserPermissions { get;private set; } /// <summary> /// 无权限action /// </summary> public string DeniedAction { get; set; } /// <summary> /// 构造 /// </summary> /// <param name="deniedAction">无权限action</param> /// <param name="userPermissions">用户权限集合</param> public PermissionRequirement(string deniedAction, List<UserPermission> userPermissions) { DeniedAction = deniedAction; UserPermissions = userPermissions; } }
再定义自定义授权Hanlder,我们命名为PermissionHandler,此类必需继承AuthorizationHandler<T>,只用实现public virtualTask HandleAsync(AuthorizationHandlerContext context),些方法是用户请求时验证是否授权的主方法,所以实现与自定义角色中间件的Invoke很相似。
using Microsoft.AspNetCore.Authorization; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; namespace PolicyPrivilegeManagement.Models { /// <summary> /// 权限授权Handler /// </summary> public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { /// <summary> /// 用户权限 /// </summary> public List<UserPermission> UserPermissions { get; set; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { //赋值用户权限 UserPermissions = requirement.UserPermissions; //从AuthorizationHandlerContext转成HttpContext,以便取出表求信息 var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //请求Url var questUrl = httpContext.Request.Path.Value.ToLower(); //是否经过验证 var isAuthenticated = httpContext.User.Identity.IsAuthenticated; if (isAuthenticated) { if (UserPermissions.GroupBy(g => g.Url).Where(w => w.Key.ToLower() == questUrl).Count() > 0) { //用户名 var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Sid).Value; if (UserPermissions.Where(w => w.UserName == userName && w.Url.ToLower() == questUrl).Count() > 0) { context.Succeed(requirement); } else { //无权限跳转到拒绝页面 httpContext.Response.Redirect("/denied"); } } else { context.Succeed(requirement); } } return Task.CompletedTask; } } }
此次的Startup.cs的ConfigureServices发生了变化,如下
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthorization(options => { //自定义Requirement,userPermission可从数据库中获得 var userPermission = new List<UserPermission> { new UserPermission { Url="/", UserName="gsw"}, new UserPermission { Url="/home/permissionadd", UserName="gsw"}, new UserPermission { Url="/", UserName="aaa"}, new UserPermission { Url="/home/contact", UserName="aaa"} }; options.AddPolicy("Permission", policy => policy.Requirements.Add(new PermissionRequirement("/denied", userPermission))); }).AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>{ options.LoginPath = new PathString("/login"); options.AccessDeniedPath = new PathString("/denied"); }); //注入授权Handler services.AddSingleton<IAuthorizationHandler, PermissionHandler>(); }
HomeController中代码如下:
using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using PolicyPrivilegeManagement.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; namespace PolicyPrivilegeManagement.Controllers { [Authorize(Policy = "Permission")] public class HomeController : Controller { PermissionHandler _permissionHandler; public HomeController(IAuthorizationHandler permissionHandler) { _permissionHandler = permissionHandler as PermissionHandler; } public IActionResult Index() { return View(); } public IActionResult PermissionAdd() { return View(); } [HttpPost("addpermission")] public IActionResult AddPermission(string url,string userName) { //添加权限 _permissionHandler.UserPermissions.Add(new UserPermission { Url = url, UserName = userName }); return Content("添加成功"); } public IActionResult Contact() { ViewData["Message"] = "Your contact page."; return View(); } public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } [AllowAnonymous] [HttpGet("login")] public IActionResult Login(string returnUrl = null) { TempData["returnUrl"] = returnUrl; return View(); } [AllowAnonymous] [HttpPost("login")] public async Task<IActionResult> Login(string userName, string password, string returnUrl = null) { var list = new List<dynamic> { new { UserName = "gsw", Password = "111111", Role = "admin",Name="桂素伟",Country="中国",Date="2017-09-02",BirthDay="1979-06-22"}, new { UserName = "aaa", Password = "222222", Role = "system",Name="测试A" ,Country="美国",Date="2017-09-03",BirthDay="1999-06-22"} }; var user = list.SingleOrDefault(s => s.UserName == userName && s.Password == password); if (user != null) { //用户标识 var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); identity.AddClaim(new Claim(ClaimTypes.Sid, userName)); identity.AddClaim(new Claim(ClaimTypes.Name, user.Name)); identity.AddClaim(new Claim(ClaimTypes.Role, user.Role)); identity.AddClaim(new Claim(ClaimTypes.Country, user.Country)); identity.AddClaim(new Claim("date", user.Date)); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); if (returnUrl == null) { returnUrl = TempData["returnUrl"]?.ToString(); } if (returnUrl != null) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); } } else { const string badUserNameOrPasswordMessage = "用户名或密码错误!"; return BadRequest(badUserNameOrPasswordMessage); } } [HttpGet("logout")] public async Task<IActionResult> Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index", "Home"); } [AllowAnonymous] [HttpGet("denied")] public IActionResult Denied() { return View(); } } }
本例设计是当用户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不会触发,性能更优化 |
本文出自 “桂素伟” 博客,请务必保留此出处http://axzxs.blog.51cto.com/730810/1962246
原文地址:http://axzxs.blog.51cto.com/730810/1962246