在《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(requirement.DeniedAction );
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不会触发,性能更优化 |
最后,把授权策略做了个NuGet的包,大家可在asp.net core 2.0的项目中查询 AuthorizePolicy引用使用这个包,包对应的github地址:https://github.com/axzxs2001/AuthorizePolicy,欢迎大家提出建议,来共同完善这个授权策略。