在asp.net core中,微软提供了基于认证(Authentication)和授权(Authorization)的方式,来实现权限管理的,本篇博文,介绍基于固定角色的权限管理和自定义角色权限管理,本文内容,更适合传统行业的BS应用,而非互联网应用。
在asp.net core中,我们认证(Authentication)通常是在Login的Post Action中进行用户名或密码来验证用户是否正确,如果通过验证,即该用户就会获得一个或几个特定的角色,通过ClaimTypes.Role来存储角色,从而当一个请求到达时,用这个角色和Controller或Action上加的特性 [Authorize(Roles = "admin,system")]来授权是否有权访问该Action。本文中的自定义角色,会把验证放在中间件中进行处理。
一、固定角色:
即把角色与具体的Controller或Action直接关联起来,整个系统中的角色是固定的,每种角色可以访问那些Controller或Action也是固定的,这做法比较适合小型项目,角色分工非常明确的项目。
项目代码:
https://github.com/axzxs2001/Asp.NetCoreExperiment/tree/master/Asp.NetCoreExperiment/%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86/RolePrivilegeManagement
始于startup.cs
需要在ConfigureServices中注入Cookie的相关信息,options是CookieAuthenticationOptions,关于这个类型提供如下属性,可参考:https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie?tabs=aspnetcore2x
它提供了登录的一些信息,或登录生成Cookie的一些信息,用以后
1 public void ConfigureServices(IServiceCollection services)
2 {
3 services.AddMvc();
4 //添加认证Cookie信息
5 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
6 .AddCookie(options =>
7 {
8 options.LoginPath = new PathString("/login");
9 options.AccessDeniedPath = new PathString("/denied");
10 });
11 }
12
13 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
14 {
15 if (env.IsDevelopment())
16 {
17 app.UseDeveloperExceptionPage();
18 app.UseBrowserLink();
19 }
20 else
21 {
22 app.UseExceptionHandler("/Home/Error");
23 }
24 app.UseStaticFiles();
25 //验证中间件
26 app.UseAuthentication();
27 app.UseMvc(routes =>
28 {
29 routes.MapRoute(
30 name: "default",
31 template: "{controller=Home}/{action=Index}/{id?}");
32 });
33 }
HomeController.cs
对于Login Get的Action,把returnUrl用户想要访问的地址(有可能用户记录下想要访问的url了,但系统会转到登录页,登录成功后直接跳转到想要访问的returnUrl页)
对于Login Post的Action,验证用户密和密码,成功能,定义一个ClaimsIdentity,把用户名和角色,和用户姓名的声明都添回进来(这个角色,就是用来验证可访问action的角色 )作来该用户标识,接下来调用HttpContext.SignInAsync进行登录,注意此方法的第一个参数,必需与StartUp.cs中services.AddAuthentication的参数相同,AddAuthentication是设置登录,SigninAsync是按设置参数进行登录
对于Logout Get的Action,是退出登录
HomeController上的[Authorize(Roles=”admin,system”)]角色和权限的关系时,所有Action只有admin和system两个角色能访问到,About上的[Authorize(Roles=”admin”)]声明这个action只能admin角色访问,Contact上的[Authorize(Roles=”system”)]声明这个action只能system角色访问,如果action上声明的是[AllowAnomymous],说明不受授权管理,可以直接访问。
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 RolePrivilegeManagement.Models;
8 using System.Security.Claims;
9 using Microsoft.AspNetCore.Authentication;
10 using Microsoft.AspNetCore.Authentication.Cookies;
11 using Microsoft.AspNetCore.Authorization;
12
13 namespace RolePrivilegeManagement.Controllers
14 {
15 [Authorize(Roles = "admin,system")]
16 public class HomeController : Controller
17 {
18 public IActionResult Index()
19 {
20 return View();
21 }
22 [Authorize(Roles = "admin")]
23 public IActionResult About()
24 {
25 ViewData["Message"] = "Your application description page.";
26 return View();
27 }
28 [Authorize(Roles = "system")]
29 public IActionResult Contact()
30 {
31 ViewData["Message"] = "Your contact page.";
32 return View();
33 }
34 public IActionResult Error()
35 {
36 return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
37 }
38 [AllowAnonymous]
39 [HttpGet("login")]
40 public IActionResult Login(string returnUrl = null)
41 {
42 TempData["returnUrl"] = returnUrl;
43 return View();
44 }
45 [AllowAnonymous]
46 [HttpPost("login")]
47 public async Task<IActionResult> Login(string userName, string password, string returnUrl = null)
48 {
49 var list = new List<dynamic> {
50 new { UserName = "gsw", Password = "111111", Role = "admin" },
51 new { UserName = "aaa", Password = "222222", Role = "system" }
52 };
53 var user = list.SingleOrDefault(s => s.UserName == userName && s.Password == password);
54 if (user!=null)
55 {
56 //用户标识
57 var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
58 identity.AddClaim(new Claim(ClaimTypes.Sid, userName));
59 identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
60 identity.AddClaim(new Claim(ClaimTypes.Role, user.Role));
61 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
62 if (returnUrl == null)
63 {
64 returnUrl = TempData["returnUrl"]?.ToString();
65 }
66 if (returnUrl != null)
67 {
68 return Redirect(returnUrl);
69 }
70 else
71 {
72 return RedirectToAction(nameof(HomeController.Index), "Home");
73 }
74 }
75 else
76 {
77 const string badUserNameOrPasswordMessage = "用户名或密码错误!";
78 return BadRequest(badUserNameOrPasswordMessage);
79 }
80 }
81 [HttpGet("logout")]
82 public async Task<IActionResult> Logout()
83 {
84 await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
85 return RedirectToAction("Index", "Home");
86 }
87 [AllowAnonymous]
88 [HttpGet("denied")]
89 public IActionResult Denied()
90 {
91 return View();
92 }
93 }
94 }
前端_Layout.cshtml布局页,在登录成功后的任何页面都可以用@User.Identity.Name就可以获取用户姓名,同时用@User.Claims.SingleOrDefault(s=>s.Type== System.Security.Claims.ClaimTypes.Sid).Value可以获取用户名或角色。
1 <nav class="navbar navbar-inverse navbar-fixed-top">
2 <div class="container">
3 <div class="navbar-header">
4 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
5 <span class="sr-only">Toggle navigation</span>
6 <span class="icon-bar"></span>
7 <span class="icon-bar"></span>
8 <span class="icon-bar"></span>
9 </button>
10 <a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">RolePrivilegeManagement</a>
11 </div>
12 <div class="navbar-collapse collapse">
13 <ul class="nav navbar-nav">
14 <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
15 <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
16 <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
17 </ul>
18 <ul class="" style="float:right; margin:0;">
19 <li style="overflow:hidden;">
20 <div style="float:left;line-height:50px;margin-right:10px;">
21 <span style="color:#ffffff">当前用户:@User.Identity.Name</span>
22 </div>
23 <div style="float:left;line-height:50px;">
24 <a asp-area="" asp-controller="Home" asp-action="Logout">注销</a>
25 </div>
26 </li>
27 </ul>
28 </div>
29 </div>
30 </nav>
现在可以用chrome运行了,进行登录页后F12,查看Network—Cookies,可以看到有一个Cookie,这个是记录returnUrl的Cookie,是否记得HomeController.cs中的Login Get的Action中代码:TempData["returnUrl"] = returnUrl;这个TempData最后转成了一个Cookie返回到客户端了,如下图:
输入用户名,密码登录,再次查看Cookies,发现多了一个.AspNetCore.Cookies,即把用户验证信息加密码保存在了这个Cookie中,当跳转到别的页面时,这两个Cookie会继续在客户端和服务传送,用以验证用户角色。
二、自定义角色
系统的角色可以自定义,用户是自写到义,权限是固定的,角色对应权限可以自定义,用户对应角色也是自定义的,如下图:
https://github.com/axzxs2001/Asp.NetCoreExperiment/tree/master/Asp.NetCoreExperiment/%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86/PrivilegeManagement
自定义角色与固定角色不同之处在于多了一个中间件(关于中间件学习参看:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware),即在Configure方法中,一定要在app.UseAuthentication下面添加验证权限的中间件,因为UseAuthentication要从Cookie中加载通过验证的用户信息到Context.User中,所以一定放在加载完后才能去验用户信息(当然自己读取Cookie也可以)
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.AspNetCore.Builder;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.Extensions.Configuration;
8 using Microsoft.Extensions.DependencyInjection;
9 using Microsoft.AspNetCore.Authentication.Cookies;
10 using Microsoft.AspNetCore.Http;
11 using PrivilegeManagement.Middleware;
12
13 namespace PrivilegeManagement
14 {
15 public class Startup
16 {
17 public Startup(IConfiguration configuration)
18 {
19 Configuration = configuration;
20 }
21 public IConfiguration Configuration { get; }
22
23 public void ConfigureServices(IServiceCollection services)
24 {
25 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
26 .AddCookie(options =>
27 {
28 options.LoginPath = new PathString("/login");
29 options.AccessDeniedPath = new PathString("/denied");
30 }
31 );
32 services.AddMvc();
33 }
34
35 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
36 {
37 if (env.IsDevelopment())
38 {
39 app.UseDeveloperExceptionPage();
40 app.UseBrowserLink();
41 }
42 else
43 {
44 app.UseExceptionHandler("/Home/Error");
45 }
46
47 app.UseStaticFiles();
48 //验证中间件
49 app.UseAuthentication();
50 ////添加权限中间件, 一定要放在app.UseAuthentication后
51 app.UsePermission(new PermissionMiddlewareOption()
52 {
53 LoginAction = @"/login",
54 NoPermissionAction = @"/denied",
55 //这个集合从数据库中查出所有用户的全部权限
56 UserPerssions = new List<UserPermission>()
57 {
58 new UserPermission { Url="/", UserName="gsw"},
59 new UserPermission { Url="/home/contact", UserName="gsw"},
60 new UserPermission { Url="/home/about", UserName="aaa"},
61 new UserPermission { Url="/", UserName="aaa"}
62 }
63 });
64 app.UseMvc(routes =>
65 {
66 routes.MapRoute(
67 name: "default",
68 template: "{controller=Home}/{action=Index}/{id?}");
69 });
70 }
71 }
72 }
下面看看中间件PermissionMiddleware.cs,在Invoke中用了context.User,如上面所述,首先要调用app.UseAuthentication加载用户信息后才能在这里使用,这个中间件逻辑较简单,如果没有验证的一律放过去,不作处理,如果验证过(登录成功了),就要查看本次请求的url和这个用户可以访问的权限是否匹配,如不匹配,就跳转到拒绝页面(这个是在Startup.cs中添加中间件时,用NoPermissionAction = @"/denied"设置的)
1 using Microsoft.AspNetCore.Http;
2 using System;
3 using System.Collections.Generic;
4 using System.IO;
5 using System.Linq;
6 using System.Reflection;
7 using System.Security.Claims;
8 using System.Threading.Tasks;
9
10 namespace PrivilegeManagement.Middleware
11 {
12 /// <summary>
13 /// 权限中间件
14 /// </summary>
15 public class PermissionMiddleware
16 {
17 /// <summary>
18 /// 管道代理对象
19 /// </summary>
20 private readonly RequestDelegate _next;
21 /// <summary>
22 /// 权限中间件的配置选项
23 /// </summary>
24 private readonly PermissionMiddlewareOption _option;
25
26 /// <summary>
27 /// 用户权限集合
28 /// </summary>
29 internal static List<UserPermission> _userPermissions;
30
31 /// <summary>
32 /// 权限中间件构造
33 /// </summary>
34 /// <param name="next">管道代理对象</param>
35 /// <param name="permissionResitory">权限仓储对象</param>
36 /// <param name="option">权限中间件配置选项</param>
37 public PermissionMiddleware(RequestDelegate next, PermissionMiddlewareOption option)
38 {
39 _option = option;
40 _next = next;
41 _userPermissions = option.UserPerssions;
42 }
43 /// <summary>
44 /// 调用管道
45 /// </summary>
46 /// <param name="context">请求上下文</param>
47 /// <returns></returns>
48 public Task Invoke(HttpContext context)
49 {
50 //请求Url
51 var questUrl = context.Request.Path.Value.ToLower();
52
53 //是否经过验证
54 var isAuthenticated = context.User.Identity.IsAuthenticated;
55 if (isAuthenticated)
56 {
57 if (_userPermissions.GroupBy(g=>g.Url).Where(w => w.Key.ToLower() == questUrl).Count() > 0)
58 {
59 //用户名
60 var userName = context.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Sid).Value;
61 if (_userPermissions.Where(w => w.UserName == userName&&w.Url.ToLower()==questUrl).Count() > 0)
62 {
63 return this._next(context);
64 }
65 else
66 {
67 //无权限跳转到拒绝页面
68 context.Response.Redirect(_option.NoPermissionAction);
69 }
70 }
71 }
72 return this._next(context);
73 }
74 }
75 }
扩展中间件类PermissionMiddlewareExtensions.cs
1 using Microsoft.AspNetCore.Builder;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Threading.Tasks;
6
7 namespace PrivilegeManagement.Middleware
8 {
9 /// <summary>
10 /// 扩展权限中间件
11 /// </summary>
12 public static class PermissionMiddlewareExtensions
13 {
14 /// <summary>
15 /// 引入权限中间件
16 /// </summary>
17 /// <param name="builder">扩展类型</param>
18 /// <param name="option">权限中间件配置选项</param>
19 /// <returns></returns>
20 public static IApplicationBuilder UsePermission(
21 this IApplicationBuilder builder, PermissionMiddlewareOption option)
22 {
23 return builder.UseMiddleware<PermissionMiddleware>(option);
24 }
25 }
26 }
中间件属性PermissionMiddlewareOption.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5
6 namespace PrivilegeManagement.Middleware
7 {
8 /// <summary>
9 /// 权限中间件选项
10 /// </summary>
11 public class PermissionMiddlewareOption
12 {
13 /// <summary>
14 /// 登录action
15 /// </summary>
16 public string LoginAction
17 { get; set; }
18 /// <summary>
19 /// 无权限导航action
20 /// </summary>
21 public string NoPermissionAction
22 { get; set; }
23
24 /// <summary>
25 /// 用户权限集合
26 /// </summary>
27 public List<UserPermission> UserPerssions
28 { get; set; } = new List<UserPermission>();
29 }
30 }
中间件实体类UserPermission.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5
6 namespace PrivilegeManagement.Middleware
7 {
8 /// <summary>
9 /// 用户权限
10 /// </summary>
11 public class UserPermission
12 {
13 /// <summary>
14 /// 用户名
15 /// </summary>
16 public string UserName
17 { get; set; }
18 /// <summary>
19 /// 请求Url
20 /// </summary>
21 public string Url
22 { get; set; }
23 }
24 }
关于自定义角色,因为不需要授权时带上角色,所以可以定义一个基Controller类BaseController.cs,其他的Controller都继承BaseController,这样所有的action都可以通过中间件来验证,当然像登录,无权限提示页面还是在Action上加[AllowAnomymous]
1 using Microsoft.AspNetCore.Authorization;
2 using Microsoft.AspNetCore.Mvc;
3 namespace PrivilegeManagement.Controllers
4 {
5 [Authorize]
6 public class BaseController:Controller
7 {
8 }
9 }
HomeController.cs如下,与固定角色的HomeController.cs差异只在Controller和Action上的Authorize特性。
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 PrivilegeManagement.Models;
8 using Microsoft.AspNetCore.Authorization;
9 using System.Security.Claims;
10 using Microsoft.AspNetCore.Authentication.Cookies;
11 using Microsoft.AspNetCore.Authentication;
12
13 namespace PrivilegeManagement.Controllers
14 {
15
16 public class HomeController : BaseController
17 {
18 public IActionResult Index()
19 {
20 return View();
21 }
22
23 public IActionResult About()
24 {
25 ViewData["Message"] = "Your application description page.";
26
27 return View();
28 }
29
30 public IActionResult Contact()
31 {
32 ViewData["Message"] = "Your contact page.";
33
34 return View();
35 }
36
37 public IActionResult Error()
38 {
39 return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
40 }
41 [AllowAnonymous]
42 [HttpGet("login")]
43 public IActionResult Login(string returnUrl = null)
44 {
45 TempData["returnUrl"] = returnUrl;
46 return View();
47 }
48 [AllowAnonymous]
49 [HttpPost("login")]
50 public async Task<IActionResult> Login(string userName,string password, string returnUrl = null)
51 {
52 var list = new List<dynamic> {
53 new { UserName = "gsw", Password = "111111", Role = "admin",Name="桂素伟" },
54 new { UserName = "aaa", Password = "222222", Role = "system",Name="测试A" }
55 };
56 var user = list.SingleOrDefault(s => s.UserName == userName && s.Password == password);
57 if (user != null)
58 {
59 //用户标识
60 var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
61 identity.AddClaim(new Claim(ClaimTypes.Sid, userName));
62 identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
63 identity.AddClaim(new Claim(ClaimTypes.Role, user.Role));
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 [HttpGet("denied")]
92 public IActionResult Denied()
93 {
94 return View();
95 }
96 }
97 }
全部代码:https://github.com/axzxs2001/Asp.NetCoreExperiment/tree/master/Asp.NetCoreExperiment/%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86
****欢迎关注我的asp.net core系统课程****《asp.net core精要讲解》 https://ke.qq.com/course/265696
《asp.net core 3.0》 https://ke.qq.com/course/437517
《asp.net core项目实战》 https://ke.qq.com/course/291868
《基于.net core微服务》 https://ke.qq.com/course/299524