緣起
本文相關視頻講解:https://www.bilibili.com/video/BV1sE411y7ot?p=4
本文已經有了對應的管理后台,地址:https://github.com/anjoy8/Blog.Admin
哈嘍大家好呀!又過去一周啦,這些天小伙伴們有沒有學習呀,已經有一周沒有更新文章了,不過晚上的時候,我也會看一些書和資料,這里給大家分享下:
1、之前簡單的寫了一個DDD+CQRS+ES的第二個系列《D3模式設計初探 與 我的計划書》,已經基本完結了,寫的比較簡單,然后我也找到了微軟的一個官方的一個資料《CQRS Journey》,不知道有沒有哪位小伙伴看,全英文的,我還在看,因為官方已經不翻譯了(更正:樓下有小伙伴評論,已經有這本書了,《探索CQRS和事件源(微軟雲計算系列叢書)》,先買了看看,好了再反饋),所以我打算自己翻譯下,如果有想和我一起的小伙伴,可以留言,咱們成立一個小組,一起翻譯這個資料,主要是關於CQRS讀寫分離的和ES事件溯源的,當然是基於DDD領域驅動設計架構的基礎上,有助於自己的理解。
2、然后就是在看《IdentityServer4.Samples》,是一個IS4的官方栗子,然后找了找資料,因為我的第三個系列教程 —— 是想做一個權限管理系統(這里是打算搭建一個 IdentityServer4 + .net core Identity + EFCore + VueAdmin )的一個前后端分離的權限管理框架,初步打算是基於按鈕級別的權限控制,從部門到崗位,角色到用戶等都動態可控(保存到數據庫,可以管理后台修改),開筆時間還沒有定,因為還在學習和年底公司總結了,如果有小伙伴想一起開發,可以看看上邊的這些技術,咱們以后可以合作,為.net 開源社區做貢獻(這里說下是完全無償的喲)。
好啦,廢話不多說,因為今天是不定期更新系列,所以會之間進入主題,不會有概念性的講解,馬上開始今天的內容啦!主要是以下幾個方面:
重要必看!剛剛下邊評論有大神提出異議,我表示他說的有道理,以下內容,自己練習玩玩兒即可,當然小項目也可以使用,不過中型以上的項目,還是要用IdentityServer4這種成熟的輪子,本文只是一個小思考,還有很多地方值得推敲和商榷,不過這不就是學習的目的么,發表思想,提出異議,做出解決,加油!
1、實現角色和接口API保存到數據庫,實現動態分配。
2、接口中,之前的授權方法依然保留,在 BlogController.cs 中還是使用的基於角色的授權方式。
3、本文是 IS4 系列的鋪墊文章。
一、JWT授權驗證,我們經歷了哪些
看過我寫的這個第一個系列《前后端分離》的小伙伴都知道,我用到了JWT來實現的權限驗證,目前已經達到什么程度的驗證了呢,這里我經歷了三個步驟:
這里強調下,如果你是第一次看這個文章,除非是有一定的基礎,或者是一直跟着我的代碼的,不然的話,會有點兒懵,如果不滿足上邊兩個條件,請先看我之前的兩篇文章,基礎:
1、直接在 Api 接口地址上設計 Roles 信息
這個也是最簡單,最粗暴的方法,直接這么配置
/// <summary> /// Values控制器 /// </summary> [Route("api/[controller]")] [ApiController] [Authorize(Roles = "Admin,Client")] [Authorize(Roles = "Admin")] [Authorize(Roles = "Client")] [Authorize(Roles = "Other")] public class ValuesController : ControllerBase { }
雖然我們把 用戶信息 和 角色Rols信息 保存到了數據庫,實現了動態化,但是具體授權的時候,還是需要手動在API接口地址上寫特定的Role權限,這樣才能對其進行匹配和授權,如果真的有一個接口可以被多個角色訪問,那就需要壘了很多了,不是很好。
2、對不同模塊的角色們 建立策略
鑒於上邊的問題,我考慮着對不同的角色建立不同的策略,並在 Startup.cs 啟動類中,配置服務:
services.AddAuthorization(options => { options.AddPolicy("Client", policy => policy.RequireRole("Client").Build()); options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build()); options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System")); options.AddPolicy("SystemOrAdminOrOther", policy => policy.RequireRole("Admin", "System", "Other")); })
然后在我們的接口api上,只需要寫上策略的名稱即可:

相信大家也都是這么做的,當然我之前也是這么寫的。雖然我們在啟動類 Startup.cs 中,對我們的Roles做了策略,看起來不用一一的配置 Roles 了,但是大家會發現,好像這個功能並沒有想象中那么美麗,因為最關鍵的問題,我們沒有解決,因為這樣我們還是需要手動一個接口一個接口的寫權限策略,不靈活!我也是想了很久,才想到了今天的這個辦法(請耐心往下看)。
3、將接口地址和角色授權分離
當然上邊的方法也能實現我們的小需要,每個接口一個個都寫好即可,但是作為強迫症的我,總感覺會有辦法可以把 API 接口,和 Role 權限剝離開,也能像用戶和 Role那樣,保存到數據庫,實現動態分配,就這樣我研究了微軟的官方文檔,偶然發現了微軟官方文檔的《Policy-based authorization》基於策略的授權,正好也找到了博客園一個大佬寫的文章,我就使用了,這里注明下:借稿作者:《asp.net core 2.0 web api基於JWT自定義策略授權》。
然后我在他的基礎上,配合着咱們的項目,做了調整,經過測試,完美的解決了咱們的問題,可以動態的數據庫進行配置,那具體是怎么實現的呢,請往下看。
二、接口地址和角色保存到數據庫
數據庫設計不好,大家看我寫的思路即可,自己可以做擴展和優化,希望還是自己動手。
既然要實現動態綁定,我們就需要把接口地址信息、角色信息保存到數據庫,那表結構是怎樣的呢,其實目前我的數據庫結構已經可以滿足了要求了,只不過需要稍微調整下,因為之前我是用EF來設計的,這里用SqlSugar會出現一個問題,所以需要在 Blog.Core.Model 層引用 sqlSugarCore 的 Nuget 包,然后把實體 RoleModulePermission.cs 中的三個參數做下忽略處理。
1、實體模型設計
首先是接口和角色的關聯表的實體模型:(真是Blog.Core項目中,可能會有些許變動,這里只是作為說明,如果想看真是的代碼,請下載最新項目代碼)
namespace Blog.Core.Model.Models { /// <summary> /// 接口、角色關聯表(以后可以把按鈕設計進來) /// </summary> public class RoleModulePermission { public int Id { get; set; } /// <summary> /// 角色ID /// </summary> public int RoleId { get; set; } /// <summary> /// 菜單ID,這里就是api地址的信息 /// </summary> public int ModuleId { get; set; } /// <summary> /// 按鈕ID /// </summary> public int? PermissionId { get; set; } /// <summary> /// 創建時間 /// </summary> public DateTime? CreateTime { get; set; } /// <summary> ///獲取或設置是否禁用,邏輯上的刪除,非物理刪除 /// </summary> public bool? IsDeleted { get; set; } // 等等,還有其他屬性,其他的可以參考Code,或者自定義... // 請注意,下邊三個實體參數,只是做傳參作用,所以忽略下,不然會認為缺少字段 [SugarColumn(IsIgnore = true)] public virtual Role Role { get; set; } [SugarColumn(IsIgnore = true)] public virtual Module Module { get; set; } [SugarColumn(IsIgnore = true)] public virtual Permission Permission { get; set; } } }
然后就是API接口信息保存的實體模型:
namespace Blog.Core.Model.Models { /// <summary> /// 接口API地址信息表 /// </summary> public class Module { public int Id { get; set; } /// <summary> /// 父ID /// </summary> public int? ParentId { get; set; } /// <summary> /// 名稱 /// </summary> public string Name { get; set; } /// <summary> /// API鏈接地址 /// </summary> public string LinkUrl { get; set; } /// <summary> /// 控制器名稱 /// </summary> public string Controller { get; set; } /// <summary> /// Action名稱 /// </summary> public string Action { get; set; } /// <summary> /// 圖標 /// </summary> public string Icon { get; set; } /// <summary> /// 菜單編號 /// </summary> public string Code { get; set; } /// <summary> /// 排序 /// </summary> public int OrderSort { get; set; } /// <summary> /// /描述 /// </summary> public string Description { get; set; } /// <summary> /// 是否激活 /// </summary> public bool Enabled { get; set; } // 等等其他屬性,具體的可以看我的Code,或者自己自定義... } }
整體數據庫UML圖如下(忽略箭頭,沒意義):(@鐵梧桐 感謝提供,工具 PowerDesigner)

2、Service 應用服務接口設計
這個很簡單,CURD中,我只是簡單寫了一個查詢全部關系的接口,其他的都很簡單,相信自己也能搞定,IRepository.cs 、Repository.cs 和 IServices.cs 這三個我就不多寫了,簡單看下 Services.cs 的一個查詢全部角色接口關系的方法:
namespace Blog.Core.Services { /// <summary> /// RoleModulePermissionServices 應用服務 /// </summary> public class RoleModulePermissionServices : BaseServices<RoleModulePermission>, IRoleModulePermissionServices { IRoleModulePermissionRepository dal; IModuleRepository moduleRepository; IRoleRepository roleRepository; // 將多個倉儲接口注入 public RoleModulePermissionServices(IRoleModulePermissionRepository dal, IModuleRepository moduleRepository, IRoleRepository roleRepository) { this.dal = dal; this.moduleRepository = moduleRepository; this.roleRepository = roleRepository; base.baseDal = dal; } /// <summary> /// 獲取全部 角色接口(按鈕)關系數據 注意我使用咱們之前的AOP緩存,很好的應用上了 /// </summary> /// <returns></returns> [Caching(AbsoluteExpiration = 10)] public async Task<List<RoleModulePermission>> GetRoleModule() { var roleModulePermissions = await dal.Query(a => a.IsDeleted == false); if (roleModulePermissions.Count > 0) { foreach (var item in roleModulePermissions) { item.Role = await roleRepository.QueryByID(item.RoleId); item.Module = await moduleRepository.QueryByID(item.ModuleId); } } return roleModulePermissions; } } }
我自己簡單的設計了下數據,
表結構與數據,都已經通過 Codefirst+DataSeed 方式,支持MSSql、Oracle、Mysql、Sqlite等多種數據庫,具體的查看我的 Github 的 README.md
這里設計使用外鍵,多對多的形式,可以很好的實現擴展,比如接口地址API變了,但是我們使用的是id,可以很靈活的適應改變。

三、基於策略授權的自定義驗證——核心
之前咱們也使用過中間件 JwtTokenAuth 來進行授權驗證,后來因為過期時間的問題,然后使用的官方的中間件app.UseAuthentication() ,今天咱們就寫一個3.0版本的驗證方法,基於AuthorizationHandler 的權限授權處理器,具體的請往下看,如果看不懂,可以直接 pull 下我的 Github 代碼即可。
一共是四個類:

1、JwtToken 生成令牌
這個很簡單,就是我們之前的 Token 字符串生成類,這里不過多做解釋,只是要注意一下下邊紅色的參數 PermissionRequirement ,數據是從Startup.cs 中注入的,下邊會說到。
namespace Blog.Core.AuthHelper { /// <summary> /// JWTToken生成類 /// </summary> public class JwtToken { /// <summary> /// 獲取基於JWT的Token /// </summary> /// <param name="claims">需要在登陸的時候配置</param> /// <param name="permissionRequirement">在startup中定義的參數</param> /// <returns></returns> public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement) { var now = DateTime.Now; // 實例化JwtSecurityToken var jwt = new JwtSecurityToken( issuer: permissionRequirement.Issuer, audience: permissionRequirement.Audience, claims: claims, notBefore: now, expires: now.Add(permissionRequirement.Expiration), signingCredentials: permissionRequirement.SigningCredentials ); // 生成 Token var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); //打包返回前台 var responseJson = new { success = true, token = encodedJwt, expires_in = permissionRequirement.Expiration.TotalSeconds, token_type = "Bearer" }; return responseJson; } } }
2、PermissionItem 憑據實體
說白了,這個就是用來存放我們用戶登錄成果后,在httptext中存放的角色信息的,是下邊 必要參數類 PermissionRequirement 的一個屬性,很簡單,不細說:
namespace Blog.Core.AuthHelper { /// <summary> /// 用戶或角色或其他憑據實體 /// </summary> public class PermissionItem { /// <summary> /// 用戶或角色或其他憑據名稱 /// </summary> public virtual string Role { get; set; } /// <summary> /// 請求Url /// </summary> public virtual string Url { get; set; } } }
3、PermissionRequirement 令牌必要參數類
這里邊存放的都是 Jwt Token 的全部信息,注意它繼承了 IAuthorizationRequirement,因為我們要設計自定義授權驗證處理器,所以必須繼承驗證要求接口,才能設計我們自己的參數:
namespace Blog.Core.AuthHelper { /// <summary> /// 必要參數類, /// 繼承 IAuthorizationRequirement,用於設計自定義權限處理器PermissionHandler /// 因為AuthorizationHandler 中的泛型參數 TRequirement 必須繼承 IAuthorizationRequirement /// </summary> public class PermissionRequirement : IAuthorizationRequirement { /// <summary> /// 用戶權限集合 /// </summary> public List<PermissionItem> Permissions { get; set; } /// <summary> /// 無權限action /// </summary> public string DeniedAction { get; set; } /// <summary> /// 認證授權類型 /// </summary> public string ClaimType { internal get; set; } /// <summary> /// 請求路徑 /// </summary> public string LoginPath { get; set; } = "/Api/Login"; /// <summary> /// 發行人 /// </summary> public string Issuer { get; set; } /// <summary> /// 訂閱人 /// </summary> public string Audience { get; set; } /// <summary> /// 過期時間 /// </summary> public TimeSpan Expiration { get; set; } /// <summary> /// 簽名驗證 /// </summary> public SigningCredentials SigningCredentials { get; set; } /// <summary> /// 構造 /// </summary> /// <param name="deniedAction">拒約請求的url</param> /// <param name="permissions">權限集合</param> /// <param name="claimType">聲明類型</param> /// <param name="issuer">發行人</param> /// <param name="audience">訂閱人</param> /// <param name="signingCredentials">簽名驗證實體</param> /// <param name="expiration">過期時間</param> public PermissionRequirement(string deniedAction, List<PermissionItem> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials, TimeSpan expiration) { ClaimType = claimType; DeniedAction = deniedAction; Permissions = permissions; Issuer = issuer; Audience = audience; Expiration = expiration; SigningCredentials = signingCredentials; } } }
4、PermissionHandler 自定義授權處理器,核心!
我們先看代碼:
namespace Blog.Core.AuthHelper { /// <summary> /// 權限授權處理器 繼承AuthorizationHandler ,並且需要一個權限必要參數 /// </summary> public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { /// <summary> /// 驗證方案提供對象 /// </summary> public IAuthenticationSchemeProvider Schemes { get; set; } /// <summary> /// services 層注入 /// </summary> public IRoleModulePermissionServices _roleModulePermissionServices { get; set; } /// <summary> /// 構造函數注入 /// </summary> /// <param name="schemes"></param> /// <param name="roleModulePermissionServices"></param> public PermissionHandler(IAuthenticationSchemeProvider schemes, IRoleModulePermissionServices roleModulePermissionServices) { Schemes = schemes; _roleModulePermissionServices = roleModulePermissionServices; } // 重寫異步處理程序 protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { // 將最新的角色和接口列表更新,
// 注意這里我用到了AOP緩存,只是減少與數據庫的訪問次數,而又保證是最新的數據
var data = await _roleModulePermissionServices.GetRoleModule(); var list = (from item in data where item.IsDeleted == false orderby item.Id select new PermissionItem { Url = item.Module?.LinkUrl, Role = item.Role?.Name, }).ToList(); requirement.Permissions = list; //從AuthorizationHandlerContext轉成HttpContext,以便取出表頭信息 var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //請求Url var questUrl = httpContext.Request.Path.Value.ToLower(); //判斷請求是否停止 var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) { var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name) as IAuthenticationRequestHandler; if (handler != null && await handler.HandleRequestAsync()) { context.Fail(); return; } } //判斷請求是否擁有憑據,即有沒有登錄 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); if (defaultAuthenticate != null) { var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name); //result?.Principal不為空即登錄成功 if (result?.Principal != null) { httpContext.User = result.Principal; //權限中是否存在請求的url if (requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key?.ToLower() == questUrl).Count() > 0) { // 獲取當前用戶的角色信息 var currentUserRoles = (from item in httpContext.User.Claims where item.Type == requirement.ClaimType select item.Value).ToList(); //驗證權限 if (currentUserRoles.Count <= 0 || requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role) && w.Url.ToLower() == questUrl).Count() <= 0) { context.Fail(); return; // 可以在這里設置跳轉頁面,不過還是會訪問當前接口地址的 httpContext.Response.Redirect(requirement.DeniedAction); } } else { context.Fail(); return; } //判斷過期時間 if ((httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) != null && DateTime.Parse(httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now) { context.Succeed(requirement); } else { context.Fail(); return; } return; } } //判斷沒有登錄時,是否訪問登錄的url,並且是Post請求,並且是form表單提交類型,否則為失敗 if (!questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType)) { context.Fail(); return; } context.Succeed(requirement); } } }
基本的解釋上邊已經寫了,應該能看懂,這里只有一點,就是我們自定義的這個處理器,是繼承了AuthorizationHandler ,而且它還需要一個泛型類,並且該泛型類必須繼承IAuthorizationRequirement 這個授權要求的接口,這樣我們就可以很方便的把我們的自定義的權限參數傳入授權處理器中。

好啦,到了這里,我們已經設計好了處理器,那如何配置在啟動服務中呢,請繼續看。
四、配置授權服務與使用
這里主要是在我們的啟動類 Startup.cs 中的服務配置,其實和之前的差不多,只是做了簡單的封裝,大家一定都能看的懂:
1、將JWT密鑰等信息封裝到配置文件
在接口層的 appsettings.json 文件中,配置我們的jwt令牌信息:
"Audience": { "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", "Issuer": "Blog.Core", "Audience": "wr" }
2、修改JWT服務注冊方法
在啟動類 Startup.cs 中的服務方法ConfigureServices 中,修改我們的JWT Token 服務注冊方法:
#region JWT Token Service //讀取配置文件 var audienceConfig = Configuration.GetSection("Audience"); var symmetricKeyAsBase64 = audienceConfig["Secret"]; var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); var signingKey = new SymmetricSecurityKey(keyByteArray); // 令牌驗證參數,之前我們都是寫在AddJwtBearer里的,這里提出來了 var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true,//驗證發行人的簽名密鑰 IssuerSigningKey = signingKey, ValidateIssuer = true,//驗證發行人 ValidIssuer = audienceConfig["Issuer"],//發行人 ValidateAudience = true,//驗證訂閱人 ValidAudience = audienceConfig["Audience"],//訂閱人 ValidateLifetime = true,//驗證生命周期 ClockSkew = TimeSpan.Zero,//這個是定義的過期的緩存時間 RequireExpirationTime = true,//是否要求過期 }; var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); // 注意使用RESTful風格的接口會更好,因為只需要寫一個Url即可,比如:/api/values 代表了Get Post Put Delete等多個。 // 如果想寫死,可以直接在這里寫。 //var permission = new List<PermissionItem> { // new PermissionItem { Url="/api/values", Role="Admin"}, // new PermissionItem { Url="/api/values", Role="System"}, // new PermissionItem { Url="/api/claims", Role="Admin"}, // }; // 如果要數據庫動態綁定,這里先留個空,后邊處理器里動態賦值 var permission = new List<PermissionItem>(); // 角色與接口的權限要求參數 var permissionRequirement = new PermissionRequirement( "/api/denied",// 拒絕授權的跳轉地址(目前無用) permission,//這里還記得么,就是我們上邊說到的角色地址信息憑據實體類 Permission ClaimTypes.Role,//基於角色的授權 audienceConfig["Issuer"],//發行人 audienceConfig["Audience"],//訂閱人 signingCredentials,//簽名憑據 expiration: TimeSpan.FromSeconds(60*2)//接口的過期時間,注意這里沒有了緩沖時間,你也可以自定義,在上邊的TokenValidationParameters的 ClockSkew ); // ① 核心之一,配置授權服務,也就是具體的規則,已經對應的權限策略,比如公司不同權限的門禁卡 services.AddAuthorization(options => { options.AddPolicy("Client", policy => policy.RequireRole("Client").Build()); options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build()); options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System")); // 自定義基於策略的授權權限 options.AddPolicy("Permission", policy => policy.Requirements.Add(permissionRequirement)); }) // ② 核心之二,必需要配置認證服務,這里是jwtBearer默認認證,比如光有卡沒用,得能識別他們 .AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) // ③ 核心之三,針對JWT的配置,比如門禁是如何識別的,是放射卡,還是磁卡 .AddJwtBearer(o => { o.TokenValidationParameters = tokenValidationParameters; }); // 依賴注入,將自定義的授權處理器 匹配給官方授權處理器接口,這樣當系統處理授權的時候,就會直接訪問我們自定義的授權處理器了。 services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
// 將授權必要類注入生命周期內 services.AddSingleton(permissionRequirement); #endregion
注意一定要配置這三個核心(.AddAuthorization、.AddAuthentication、.AddJwtBearer),否則會報錯:

3、在登錄接口中,賦值過期時間等信息

雖然我們在 startup 中也設置了過期時間,但是我們還需要在每一個 token 的聲明列表中(claims)中,配置過期時間,只不過兩個時間一樣罷了。
4、在接口中很方便調用
這樣定義好以后,我們只需要很方便的在每一個controller上邊寫上 [Authorize("Permission")],這個驗證特性即可,這個名字就是我們的策略名,我們就不用再想哪一個接口對應哪些Roles了,是不是更方便了!當然如果不寫這個特性的話,不會被限制,比如那些前台的頁面接口,就不需要被限制。

5、使用效果展示
咱們看看平時會遇到的4種情況。
注意:下邊的演示,是用的 public async Task<object> GetJWTToken3(string name, string pass) 這個新接口獲取的Token

你也可以直接使用我的在線地址 http://123.206.33.109:8081/swagger/index.html 來操作,具體的步驟見下面的這三個情況。
接口沒有配置權限
這種情況,無論是數據庫是否配置,都會很正常的通過HTTP請求,從而獲取到我們的數據,就比如登錄頁:

接口設置了權限,但是數據庫沒有配置
咱們以 ValuesController 為例子
現在我們把API接口是 /api/values 的接口和角色關聯的表給邏輯刪除了,那這個時候,也就代表了,當前接口雖然設置了權限,但是在數據庫里並沒有配置它與Role的關系:

那如果我們訪問的話會是怎樣:

首先,我們看到在獲取到的四個角色接口信息中,已經沒有了api/values 的相關信息,然后我們去訪問該接口,就看到了報錯403,當然你也可以自定義錯誤,就是在 PermissionHandler.cs 自定義權限授權處理程序里,可以自己擴展。
接口設置了權限,並且數據庫也配置了
還是使用咱們的 ValueController.cs ,這時候咱們把剛剛邏輯刪除的改成False:

然后看看我們的執行過程:

發現我們已經很成功的對接口進行了權限控制了,你可以在后台做成界面的形式,對其進行配置等,當然很豐富的了。這里要說一下,如果你用的是RESTful風格的接口,配置 api/values 會把CURD四個權限全部賦過去,如果你想對某一個角色只有Read和Create(讀取和添加)的權限的話,你可以這么操作:
1、不用RESTful風格,直接每一個接口都配置進去,比如這樣,api/values/Read、api/values/Create 等;
2、繼續使用RESTful風格接口,但是需要在(角色和API地址的)數據庫表中,再增加一個 ActionName 這樣類似的字段,對接口進行區分限制就行,具體的,我會在下一個系列說到;
最后經過了兩分鍾,令牌過期:

好啦,這些簡單的授權功能已經夠咱們使用了,還能在數據庫里動態配置,不是么?
五、思考
到這里,咱們的這個項目已經完全能實現權限的動態分配了,當然這里是沒有后台界面的,你可以自己建立一個MVC項目來實驗,也可以建立一個Vue管理后台來分配,都是很簡單的,我個人感覺已經很完美了,咱們的項目基本也成型了。
但是這些都是咱們自己造的輪子,那如果我們用一直很高調的 IdentityServer4 這個已經成熟的輪子來實現接口級別的動態授權是怎么做呢?
請看我的下一個系列吧(.NetCore API + IS4+EFCore+VueAdmin)~~~ 提前祝大家聖誕節快樂!
1、情景補充
有的小伙伴在研究或者使用這個方法的時候,出現了疑惑,主要是兩個問題:
1、我如果后台修改權限了,想立刻或者 關閉瀏覽器下次打開的時候更新權限咋辦?
2、如果我Token的過期時間比較短,比如一天,那如何實現滑動更新,就是不會正在使用的時候,突然去登錄頁?
我也想了想,大概有以下自己的想法,大家可以參考一下,歡迎提出批評:
1、如果后台管理員修改了某一個人的權限,我會把每一個Token放到Redis緩存里,然后主要是 Token 的值,還有過期時間,權限等,如果管理員修改了權限(這個時候Token就不能使用了,因為這個Token還是之前的Roles權限),然后就會更新了數據庫的Role,還會把Redis里的該Token信息給Delete掉,這樣用戶再訪問下一個頁面的時候,我們先校驗Redis緩存里是否有這個 Token 數據,如果有,還繼續往下走,如果沒有了,就返回401讓用戶重新登錄。可以使用一個中間件來處理當前Token是否有效。
2、上邊寫到了在net core api里增加一個中間件來判斷Token是否有效,那如果無效了或者是被管理員修改了權限,導致 Token 被禁掉以后,又不想讓用戶重新登錄怎么辦呢,我就想的是在 Http.js 封裝請求方法中,寫一個,每次用戶訪問的之前,都判斷一下當前 Token 是否有效的JS方法,如果有效則繼續調用下一個接口,如果無效,這個時候就可以在后台重新生成一個 Token 並返回到前台,保存到localstroage里,繼續用新的 Token 調用下一個接口。
3、用上邊的方法,你會感覺這樣每次都會多一次調用,會占資源,你可以每天執行一次,或者就是每次登錄的成功后,不僅把 Token 存在本地,把過期時間也存下來,這樣每次請求前可以判斷是否過期,如果過期了呢,就先調用重新獲取Token 的接口方法,然后再往下走。
可能你會感覺很麻煩,很荒唐,不過微信小程序就是這么處理的,不信你可以去研究下。
六、Github & Gitee
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core
-- END
