緣起
哈嘍大家好呀护戳!又過(guò)去一周啦侧纯,這些天小伙伴們有沒(méi)有學(xué)習(xí)呀匀谣,已經(jīng)有一周沒(méi)有更新文章了,不過(guò)晚上的時(shí)候府适,我也會(huì)看一些書(shū)和資料即舌,這里給大家分享下:
1佣盒、之前簡(jiǎn)單的寫(xiě)了一個(gè)DDD+CQRS+ES的第二個(gè)系列《D3模式設(shè)計(jì)初探 與 我的計(jì)劃書(shū)》,已經(jīng)基本完結(jié)了顽聂,寫(xiě)的比較簡(jiǎn)單肥惭,然后我也找到了微軟的一個(gè)官方的一個(gè)資料《CQRS Journey》,不知道有沒(méi)有哪位小伙伴看紊搪,全英文的蜜葱,我還在看,因?yàn)楣俜揭呀?jīng)不翻譯了(更正:樓下有小伙伴評(píng)論耀石,已經(jīng)有這本書(shū)了牵囤,《探索CQRS和事件源(微軟云計(jì)算系列叢書(shū))》,先買(mǎi)了看看滞伟,好了再反饋)揭鳞,所以我打算自己翻譯下,如果有想和我一起的小伙伴梆奈,可以留言野崇,咱們成立一個(gè)小組,一起翻譯這個(gè)資料亩钟,主要是關(guān)于CQRS讀寫(xiě)分離的和ES事件溯源的乓梨,當(dāng)然是基于DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)架構(gòu)的基礎(chǔ)上鳖轰,有助于自己的理解。
2扶镀、然后就是在看《IdentityServer4.Samples》蕴侣,是一個(gè)IS4的官方栗子,然后找了找資料狈惫,因?yàn)槲业牡谌齻€(gè)系列教程 —— 是想做一個(gè)權(quán)限管理系統(tǒng)(這里是打算搭建一個(gè) IdentityServer4 + .net core Identity + EFCore + VueAdmin )的一個(gè)前后端分離的權(quán)限管理框架睛蛛,初步打算是基于按鈕級(jí)別的權(quán)限控制,從部門(mén)到崗位胧谈,角色到用戶等都動(dòng)態(tài)可控(保存到數(shù)據(jù)庫(kù),可以管理后臺(tái)修改)荸频,開(kāi)筆時(shí)間還沒(méi)有定菱肖,因?yàn)檫€在學(xué)習(xí)和年底公司總結(jié)了,如果有小伙伴想一起開(kāi)發(fā)旭从,可以看看上邊的這些技術(shù)稳强,咱們以后可以合作,為.net 開(kāi)源社區(qū)做貢獻(xiàn)(這里說(shuō)下是完全無(wú)償?shù)膯眩?/p>
好啦和悦,廢話不多說(shuō)退疫,因?yàn)榻裉焓遣欢ㄆ诟孪盗校詴?huì)之間進(jìn)入主題鸽素,不會(huì)有概念性的講解褒繁,馬上開(kāi)始今天的內(nèi)容啦!主要是以下幾個(gè)方面:
重要必看馍忽!剛剛下邊評(píng)論有大神提出異議棒坏,我表示他說(shuō)的有道理,以下內(nèi)容遭笋,自己練習(xí)玩玩兒即可坝冕,當(dāng)然小項(xiàng)目也可以使用,不過(guò)中型以上的項(xiàng)目瓦呼,還是要用IdentityServer4這種成熟的輪子喂窟,本文只是一個(gè)小思考,還有很多地方值得推敲和商榷央串,不過(guò)這不就是學(xué)習(xí)的目的么磨澡,發(fā)表思想,提出異議蹋辅,做出解決钱贯,加油!
1侦另、實(shí)現(xiàn)角色和接口API保存到數(shù)據(jù)庫(kù)秩命,實(shí)現(xiàn)動(dòng)態(tài)分配尉共。
2、接口中弃锐,之前的授權(quán)方法依然保留袄友,在 BlogController.cs 中還是使用的基于角色的授權(quán)方式。
3霹菊、本文是 IS4 系列的鋪墊文章剧蚣。
一、JWT授權(quán)驗(yàn)證旋廷,我們經(jīng)歷了哪些
看過(guò)我寫(xiě)的這個(gè)第一個(gè)系列《前后端分離》的小伙伴都知道鸠按,我用到了JWT來(lái)實(shí)現(xiàn)的權(quán)限驗(yàn)證,目前已經(jīng)達(dá)到什么程度的驗(yàn)證了呢饶碘,這里我經(jīng)歷了三個(gè)步驟:
這里強(qiáng)調(diào)下目尖,如果你是第一次看這個(gè)文章,除非是有一定的基礎(chǔ)扎运,或者是一直跟著我的代碼的瑟曲,不然的話,會(huì)有點(diǎn)兒懵豪治,如果不滿足上邊兩個(gè)條件洞拨,請(qǐng)先看我之前的兩篇文章,基礎(chǔ):
1、直接在 Api 接口地址上設(shè)計(jì) Roles 信息
這個(gè)也是最簡(jiǎn)單齿椅,最粗暴的方法琉挖,直接這么配置
/// <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信息 **保存到了數(shù)據(jù)庫(kù),實(shí)現(xiàn)了動(dòng)態(tài)化涣脚,但是具體授權(quán)的時(shí)候示辈,還是需要手動(dòng)在API接口地址上寫(xiě)特定的Role權(quán)限,這樣才能對(duì)其進(jìn)行匹配和授權(quán)遣蚀,如果真的有一個(gè)接口可以被多個(gè)角色訪問(wèn)矾麻,那就需要壘了很多了,不是很好芭梯。
2险耀、對(duì)不同模塊的角色們 建立策略
鑒于上邊的問(wèn)題,我考慮著對(duì)不同的角色建立不同的策略玖喘,并在 Startup.cs 啟動(dòng)類中甩牺,配置服務(wù):
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上,只需要寫(xiě)上策略的名稱即可:
相信大家也都是這么做的累奈,當(dāng)然我之前也是這么寫(xiě)的贬派。雖然我們?cè)趩?dòng)類 Startup.cs 中急但,對(duì)我們的Roles做了策略,看起來(lái)不用一一的配置 Roles 了搞乏,但是大家會(huì)發(fā)現(xiàn)波桩,好像這個(gè)功能并沒(méi)有想象中那么美麗,因?yàn)樽铌P(guān)鍵的問(wèn)題请敦,我們沒(méi)有解決镐躲,因?yàn)檫@樣我們還是需要手動(dòng)一個(gè)接口一個(gè)接口的寫(xiě)權(quán)限策略,不靈活侍筛!我也是想了很久萤皂,才想到了今天的這個(gè)辦法(請(qǐng)耐心往下看)。
3勾笆、將接口地址和角色授權(quán)分離
當(dāng)然上邊的方法也能實(shí)現(xiàn)我們的小需要敌蚜,每個(gè)接口一個(gè)個(gè)都寫(xiě)好即可,但是作為強(qiáng)迫癥的我窝爪,總感覺(jué)會(huì)有辦法可以把 API 接口,和 Role 權(quán)限剝離開(kāi)齐媒,也能像用戶和 Role那樣蒲每,保存到數(shù)據(jù)庫(kù),實(shí)現(xiàn)動(dòng)態(tài)分配喻括,就這樣我研究了微軟的官方文檔邀杏,偶然發(fā)現(xiàn)了微軟官方文檔的《Policy-based authorization》基于策略的授權(quán),正好也找到了博客園一個(gè)大佬寫(xiě)的文章唬血,我就使用了望蜡,這里注明下:借稿作者:《asp.net core 2.0 web api基于JWT自定義策略授權(quán)》。
然后我在他的基礎(chǔ)上拷恨,配合著咱們的項(xiàng)目脖律,做了調(diào)整,經(jīng)過(guò)測(cè)試腕侄,完美的解決了咱們的問(wèn)題小泉,可以動(dòng)態(tài)的數(shù)據(jù)庫(kù)進(jìn)行配置,那具體是怎么實(shí)現(xiàn)的呢冕杠,請(qǐng)往下看微姊。
二、接口地址和角色保存到數(shù)據(jù)庫(kù)
數(shù)據(jù)庫(kù)設(shè)計(jì)不好分预,大家看我寫(xiě)的思路即可兢交,自己可以做擴(kuò)展和優(yōu)化,希望還是自己動(dòng)手笼痹。
既然要實(shí)現(xiàn)動(dòng)態(tài)綁定配喳,我們就需要把接口地址信息酪穿、角色信息保存到數(shù)據(jù)庫(kù),那表結(jié)構(gòu)是怎樣的呢界逛,其實(shí)目前我的數(shù)據(jù)庫(kù)結(jié)構(gòu)已經(jīng)可以滿足了要求了昆稿,只不過(guò)需要稍微調(diào)整下,因?yàn)橹拔沂怯肊F來(lái)設(shè)計(jì)的息拜,這里用SqlSugar會(huì)出現(xiàn)一個(gè)問(wèn)題溉潭,所以需要在 Blog.Core.Model 層引用 sqlSugarCore 的 Nuget 包,然后把實(shí)體 RoleModulePermission.cs 中的三個(gè)參數(shù)做下忽略處理少欺。
1喳瓣、實(shí)體模型設(shè)計(jì)
首先是接口和角色的關(guān)聯(lián)表的實(shí)體模型:
namespace Blog.Core.Model.Models
{
/// <summary>
/// 接口、角色關(guān)聯(lián)表(以后可以把按鈕設(shè)計(jì)進(jìn)來(lái))
/// </summary>
public class RoleModulePermission
{
public int Id { get; set; }
/// <summary>
/// 角色I(xiàn)D
/// </summary>
public int RoleId { get; set; }
/// <summary>
/// 菜單ID赞别,這里就是api地址的信息
/// </summary>
public int ModuleId { get; set; }
/// <summary>
/// 按鈕ID
/// </summary>
public int? PermissionId { get; set; }
/// <summary>
/// 創(chuàng)建時(shí)間
/// </summary>
public DateTime? CreateTime { get; set; }
/// <summary>
///獲取或設(shè)置是否禁用畏陕,邏輯上的刪除,非物理刪除
/// </summary>
public bool? IsDeleted { get; set; }
// 等等仿滔,還有其他屬性惠毁,其他的可以參考Code,或者自定義...
// 請(qǐng)注意崎页,下邊三個(gè)實(shí)體參數(shù)鞠绰,只是做傳參作用,所以忽略下飒焦,不然會(huì)認(rèn)為缺少字段
[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接口信息保存的實(shí)體模型:
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>
/// 圖標(biāo)
/// </summary>
public string Icon { get; set; }
/// <summary>
/// 菜單編號(hào)
/// </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,或者自己自定義...
}
}
2牺荠、Service 應(yīng)用服務(wù)接口設(shè)計(jì)
這個(gè)很簡(jiǎn)單翁巍,CURD中,我只是簡(jiǎn)單寫(xiě)了一個(gè)查詢?nèi)筷P(guān)系的接口休雌,其他的都很簡(jiǎn)單灶壶,相信自己也能搞定,IRepository.cs 挑辆、Repository.cs 和 IServices.cs 這三個(gè)我就不多寫(xiě)了例朱,簡(jiǎn)單看下 Services.cs 的一個(gè)查詢?nèi)拷巧涌陉P(guān)系的方法:
namespace Blog.Core.Services
{ /// <summary>
/// RoleModulePermissionServices 應(yīng)用服務(wù) /// </summary>
public class RoleModulePermissionServices : BaseServices<RoleModulePermission>, IRoleModulePermissionServices
{
IRoleModulePermissionRepository dal;
IModuleRepository moduleRepository;
IRoleRepository roleRepository; // 將多個(gè)倉(cāng)儲(chǔ)接口注入
public RoleModulePermissionServices(IRoleModulePermissionRepository dal, IModuleRepository moduleRepository, IRoleRepository roleRepository)
{ this.dal = dal; this.moduleRepository = moduleRepository; this.roleRepository = roleRepository; base.baseDal = dal;
} /// <summary>
/// 獲取全部 角色接口(按鈕)關(guān)系數(shù)據(jù) 注意我使用咱們之前的AOP緩存,很好的應(yīng)用上了 /// </summary>
/// <returns></returns>
[Caching(AbsoluteExpiration = 10)] public async Task<List<RoleModulePermission>> GeRoleModule()
{ 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;
}
}
}
我自己簡(jiǎn)單的設(shè)計(jì)了下數(shù)據(jù)鱼蝉,如果有想我的數(shù)據(jù)的洒嗤,請(qǐng)留言,我把這個(gè)Sql數(shù)據(jù)文件放到 Github :
這里設(shè)計(jì)使用外鍵魁亦,多對(duì)多的形式渔隶,可以很好的實(shí)現(xiàn)擴(kuò)展,比如接口地址API變了,但是我們使用的是id间唉,可以很靈活的適應(yīng)改變绞灼。
三、基于策略授權(quán)的自定義驗(yàn)證——核心
之前咱們也使用過(guò)中間件 JwtTokenAuth 來(lái)進(jìn)行授權(quán)驗(yàn)證呈野,后來(lái)因?yàn)檫^(guò)期時(shí)間的問(wèn)題低矮,然后使用的官方的中間件app.UseAuthentication() ,今天咱們就寫(xiě)一個(gè)3.0版本的驗(yàn)證方法被冒,基于AuthorizationHandler 的權(quán)限授權(quán)處理器军掂,具體的請(qǐng)往下看,如果看不懂昨悼,可以直接 pull 下我的 Github 代碼即可蝗锥。
一共是四個(gè)類:
1、JwtToken 生成令牌
這個(gè)很簡(jiǎn)單率触,就是我們之前的 Token 字符串生成類终议,這里不過(guò)多做解釋葱蝗,只是要注意一下下邊紅色的參數(shù) PermissionRequirement 两曼,數(shù)據(jù)是從Startup.cs 中注入的陆馁,下邊會(huì)說(shuō)到。
namespace Blog.Core.AuthHelper
{
/// <summary>
/// JWTToken生成類 /// </summary>
public class JwtToken
{ /// <summary>
/// 獲取基于JWT的Token /// </summary>
/// <param name="claims">需要在登陸的時(shí)候配置</param>
/// <param name="permissionRequirement">在startup中定義的參數(shù)</param>
/// <returns></returns>
public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
{
var now = DateTime.Now; // 實(shí)例化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); //打包返回前臺(tái)
var responseJson = new {
success = true,
token = encodedJwt,
expires_in = permissionRequirement.Expiration.TotalSeconds,
token_type = "Bearer" }; return responseJson;
}
}
}
2击狮、Permission 憑據(jù)實(shí)體
說(shuō)白了佛析,這個(gè)就是用來(lái)存放我們用戶登錄成果后,在httptext中存放的角色信息的彪蓬,是下邊 必要參數(shù)類 PermissionRequirement 的一個(gè)屬性档冬,很簡(jiǎn)單酷誓,不細(xì)說(shuō):
namespace Blog.Core.AuthHelper
{
/// <summary>
/// 用戶或角色或其他憑據(jù)實(shí)體 /// </summary>
public class Permission
{
/// <summary>
/// 用戶或角色或其他憑據(jù)名稱 /// </summary>
public virtual string Role { get; set; } /// <summary>
/// 請(qǐng)求Url /// </summary>
public virtual string Url { get; set; }
}
}
3盐数、PermissionRequirement 令牌必要參數(shù)類
這里邊存放的都是 Jwt Token 的全部信息,注意它繼承了 IAuthorizationRequirement谜诫,因?yàn)槲覀円O(shè)計(jì)自定義授權(quán)驗(yàn)證處理器喻旷,所以必須繼承驗(yàn)證要求接口牢屋,才能設(shè)計(jì)我們自己的參數(shù):
namespace Blog.Core.AuthHelper
{
/// <summary>
/// 必要參數(shù)類伟阔, /// 繼承 IAuthorizationRequirement,用于設(shè)計(jì)自定義權(quán)限處理器PermissionHandler /// 因?yàn)锳uthorizationHandler 中的泛型參數(shù) TRequirement 必須繼承 IAuthorizationRequirement /// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
/// <summary>
/// 用戶權(quán)限集合 /// </summary>
public List<Permission> Permissions { get; set; } /// <summary>
/// 無(wú)權(quán)限action /// </summary>
public string DeniedAction { get; set; } /// <summary>
/// 認(rèn)證授權(quán)類型 /// </summary>
public string ClaimType { internal get; set; } /// <summary>
/// 請(qǐng)求路徑 /// </summary>
public string LoginPath { get; set; } = "/Api/Login"; /// <summary>
/// 發(fā)行人 /// </summary>
public string Issuer { get; set; } /// <summary>
/// 訂閱人 /// </summary>
public string Audience { get; set; } /// <summary>
/// 過(guò)期時(shí)間 /// </summary>
public TimeSpan Expiration { get; set; } /// <summary>
/// 簽名驗(yàn)證 /// </summary>
public SigningCredentials SigningCredentials { get; set; } /// <summary>
/// 構(gòu)造 /// </summary>
/// <param name="deniedAction">拒約請(qǐng)求的url</param>
/// <param name="permissions">權(quán)限集合</param>
/// <param name="claimType">聲明類型</param>
/// <param name="issuer">發(fā)行人</param>
/// <param name="audience">訂閱人</param>
/// <param name="signingCredentials">簽名驗(yàn)證實(shí)體</param>
/// <param name="expiration">過(guò)期時(shí)間</param>
public PermissionRequirement(string deniedAction, List<Permission> 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 自定義授權(quán)處理器灾部,核心赌髓!
我們先看代碼:
namespace Blog.Core.AuthHelper
{
/// <summary>
/// 權(quán)限授權(quán)處理器 繼承AuthorizationHandler 锁蠕,并且需要一個(gè)權(quán)限必要參數(shù) /// </summary>
public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { /// <summary>
/// 驗(yàn)證方案提供對(duì)象 /// </summary>
public IAuthenticationSchemeProvider Schemes { get; set; } /// <summary>
/// services 層注入 /// </summary>
public IRoleModulePermissionServices _roleModulePermissionServices { get; set; } /// <summary>
/// 構(gòu)造函數(shù)注入 /// </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緩存舌仍,只是減少與數(shù)據(jù)庫(kù)的訪問(wèn)次數(shù)铸豁,而又保證是最新的數(shù)據(jù)
var data = await _roleModulePermissionServices.GeRoleModule(); var list = (from item in data where item.IsDeleted == false
orderby item.Id select new Permission
{
Url = item.Module?.LinkUrl,
Role = item.Role?.Name,
}).ToList();
requirement.Permissions = list; //從AuthorizationHandlerContext轉(zhuǎn)成HttpContext推姻,以便取出表頭信息
var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //請(qǐng)求Url
var questUrl = httpContext.Request.Path.Value.ToLower(); //判斷請(qǐng)求是否停止
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;
}
} //判斷請(qǐng)求是否擁有憑據(jù)增炭,即有沒(méi)有登錄
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; //權(quán)限中是否存在請(qǐng)求的url
if (requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key?.ToLower() == questUrl).Count() > 0)
{ // 獲取當(dāng)前用戶的角色信息
var currentUserRoles = (from item in httpContext.User.Claims where item.Type == requirement.ClaimType select item.Value).ToList(); //驗(yàn)證權(quán)限
if (currentUserRoles.Count <= 0 || requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role) && w.Url.ToLower() == questUrl).Count() <= 0)
{
context.Fail(); return; // 可以在這里設(shè)置跳轉(zhuǎn)頁(yè)面隙姿,不過(guò)還是會(huì)訪問(wèn)當(dāng)前接口地址的
httpContext.Response.Redirect(requirement.DeniedAction);
}
} else {
context.Fail(); return;
} //判斷過(guò)期時(shí)間
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;
}
} //判斷沒(méi)有登錄時(shí)队丝,是否訪問(wèn)登錄的url,并且是Post請(qǐng)求欲鹏,并且是form表單提交類型赔嚎,否則為失敗
if (!questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType))
{
context.Fail(); return;
}
context.Succeed(requirement);
}
}
}
基本的解釋上邊已經(jīng)寫(xiě)了侠畔,應(yīng)該能看懂损晤,這里只有一點(diǎn),就是我們自定義的這個(gè)處理器喘落,是繼承了AuthorizationHandler 揖盘,而且它還需要一個(gè)泛型類,并且該泛型類必須繼承IAuthorizationRequirement 這個(gè)授權(quán)要求的接口鹿蜀,這樣我們就可以很方便的把我們的自定義的權(quán)限參數(shù)傳入授權(quán)處理器中茴恰。
好啦往枣,到了這里分冈,我們已經(jīng)設(shè)計(jì)好了處理器雕沉,那如何配置在啟動(dòng)服務(wù)中呢坡椒,請(qǐng)繼續(xù)看倔叼。
四丈攒、配置授權(quán)服務(wù)與使用
這里主要是在我們的啟動(dòng)類 Startup.cs 中的服務(wù)配置肥印,其實(shí)和之前的差不多深碱,只是做了簡(jiǎn)單的封裝敷硅,大家一定都能看的懂:
1绞蹦、將JWT密鑰等信息封裝到配置文件
在接口層的 appsettings.json 文件中幽七,配置我們的jwt令牌信息:
"Audience": { "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs", "Issuer": "Blog.Core", "Audience": "wr" }
2澡屡、修改JWT服務(wù)注冊(cè)方法
在啟動(dòng)類 Startup.cs 中的服務(wù)方法ConfigureServices 中驶鹉,修改我們的JWT Token 服務(wù)注冊(cè)方法:
#region JWT Token Service
//讀取配置文件
var audienceConfig = Configuration.GetSection("Audience"); var symmetricKeyAsBase64 = audienceConfig["Secret"]; var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); var signingKey = new SymmetricSecurityKey(keyByteArray); // 令牌驗(yàn)證參數(shù)室埋,之前我們都是寫(xiě)在AddJwtBearer里的,這里提出來(lái)了
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,//驗(yàn)證發(fā)行人的簽名密鑰
IssuerSigningKey = signingKey,
ValidateIssuer = true,//驗(yàn)證發(fā)行人
ValidIssuer = audienceConfig["Issuer"],//發(fā)行人
ValidateAudience = true,//驗(yàn)證訂閱人
ValidAudience = audienceConfig["Audience"],//訂閱人
ValidateLifetime = true,//驗(yàn)證生命周期
ClockSkew = TimeSpan.Zero,//這個(gè)是定義的過(guò)期的緩存時(shí)間
RequireExpirationTime = true,//是否要求過(guò)期
}; var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); // 注意使用RESTful風(fēng)格的接口會(huì)更好,因?yàn)橹恍枰獙?xiě)一個(gè)Url即可疹尾,比如:/api/values 代表了Get Post Put Delete等多個(gè)纳本。 // 如果想寫(xiě)死繁成,可以直接在這里寫(xiě)巾腕。 //var permission = new List<Permission> { // new Permission { Url="/api/values", Role="Admin"}, // new Permission { Url="/api/values", Role="System"}, // new Permission { Url="/api/claims", Role="Admin"}, // }; // 如果要數(shù)據(jù)庫(kù)動(dòng)態(tài)綁定尊搬,這里先留個(gè)空佛寿,后邊處理器里動(dòng)態(tài)賦值
var permission = new List<Permission>(); // 角色與接口的權(quán)限要求參數(shù)
var permissionRequirement = new PermissionRequirement( "/api/denied",// 拒絕授權(quán)的跳轉(zhuǎn)地址(目前無(wú)用)
permission,//這里還記得么冀泻,就是我們上邊說(shuō)到的角色地址信息憑據(jù)實(shí)體類 Permission
ClaimTypes.Role,//基于角色的授權(quán)
audienceConfig["Issuer"],//發(fā)行人
audienceConfig["Audience"],//訂閱人
signingCredentials,//簽名憑據(jù)
expiration: TimeSpan.FromSeconds(60*2)//接口的過(guò)期時(shí)間,注意這里沒(méi)有了緩沖時(shí)間肢专,你也可以自定義鸟召,在上邊的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")); // 自定義基于策略的授權(quán)權(quán)限
options.AddPolicy("Permission",
policy => policy.Requirements.Add(permissionRequirement));
})
.AddAuthentication(x => {
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o => {
o.TokenValidationParameters = tokenValidationParameters;
});
// 依賴注入压状,將自定義的授權(quán)處理器 匹配給官方授權(quán)處理器接口跟继,這樣當(dāng)系統(tǒng)處理授權(quán)的時(shí)候镣丑,就會(huì)直接訪問(wèn)我們自定義的授權(quán)處理器了金吗。
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
// 將授權(quán)必要類注入生命周期內(nèi)
services.AddSingleton(permissionRequirement); #endregion
3摇庙、在接口中很方便調(diào)用
這樣定義好以后卫袒,我們只需要很方便的在每一個(gè)controller上邊寫(xiě)上 [Authorize("Permission")]夕凝,這個(gè)驗(yàn)證特性即可码秉,這個(gè)名字就是我們的策略名转砖,我們就不用再想哪一個(gè)接口對(duì)應(yīng)哪些Roles了堪藐,是不是更方便了礁竞!當(dāng)然如果不寫(xiě)這個(gè)特性的話模捂,不會(huì)被限制,比如那些前臺(tái)的頁(yè)面接口岖食,就不需要被限制泡垃。
4蔑穴、使用效果展示
咱們看看平時(shí)會(huì)遇到的4種情況存和。
注意:下邊的演示纵朋,是用的 public async Task<object> GetJWTToken3(string name, string pass) 這個(gè)新接口獲取的Token
你也可以直接使用我的在線地址 http://123.206.33.109:8081/swagger/index.html 來(lái)操作倡蝙,具體的步驟見(jiàn)下面的這三個(gè)情況寺鸥。
1胆建、接口沒(méi)有配置權(quán)限
這種情況,無(wú)論是數(shù)據(jù)庫(kù)是否配置凉驻,都會(huì)很正常的通過(guò)HTTP請(qǐng)求涝登,從而獲取到我們的數(shù)據(jù)胀滚,就比如登錄頁(yè):
2、接口設(shè)置了權(quán)限剑刑,但是數(shù)據(jù)庫(kù)沒(méi)有配置
咱們以 ValuesController 為例子
現(xiàn)在我們把API接口是 /api/values 的接口和角色關(guān)聯(lián)的表給邏輯刪除了施掏,那這個(gè)時(shí)候,也就代表了抖苦,當(dāng)前接口雖然設(shè)置了權(quán)限锌历,但是在數(shù)據(jù)庫(kù)里并沒(méi)有配置它與Role的關(guān)系:
那如果我們?cè)L問(wèn)的話會(huì)是怎樣:
首先,我們看到在獲取到的四個(gè)角色接口信息中卤材,已經(jīng)沒(méi)有了api/values 的相關(guān)信息,然后我們?nèi)ピL問(wèn)該接口帆精,就看到了報(bào)錯(cuò)403卓练,當(dāng)然你也可以自定義錯(cuò)誤襟企,就是在 PermissionHandler.cs 自定義權(quán)限授權(quán)處理程序里整吆,可以自己擴(kuò)展表蝙。
3府蛇、接口設(shè)置了權(quán)限务荆,并且數(shù)據(jù)庫(kù)也配置了
還是使用咱們的 ValueController.cs 函匕,這時(shí)候咱們把剛剛邏輯刪除的改成False:
然后看看我們的執(zhí)行過(guò)程:
發(fā)現(xiàn)我們已經(jīng)很成功的對(duì)接口進(jìn)行了權(quán)限控制了,你可以在后臺(tái)做成界面的形式蚪黑,對(duì)其進(jìn)行配置等盅惜,當(dāng)然很豐富的了。這里要說(shuō)一下忌穿,如果你用的是RESTful風(fēng)格的接口抒寂,配置 api/values 會(huì)把CURD四個(gè)權(quán)限全部賦過(guò)去,如果你想對(duì)某一個(gè)角色只有Read和Create(讀取和添加)的權(quán)限的話掠剑,你可以這么操作:
1、不用RESTful風(fēng)格朴译,直接每一個(gè)接口都配置進(jìn)去井佑,比如這樣,api/values/Read动分、api/values/Create 等毅糟;
2、繼續(xù)使用RESTful風(fēng)格接口澜公,但是需要在(角色和API地址的)數(shù)據(jù)庫(kù)表中姆另,再增加一個(gè) ActionName 這樣類似的字段,對(duì)接口進(jìn)行區(qū)分限制就行坟乾,具體的迹辐,我會(huì)在下一個(gè)系列說(shuō)到;
最后經(jīng)過(guò)了兩分鐘甚侣,令牌過(guò)期:
好啦明吩,這些簡(jiǎn)單的授權(quán)功能已經(jīng)夠咱們使用了,還能在數(shù)據(jù)庫(kù)里動(dòng)態(tài)配置殷费,不是么印荔?
五、思考
到這里详羡,咱們的這個(gè)項(xiàng)目已經(jīng)完全能實(shí)現(xiàn)權(quán)限的動(dòng)態(tài)分配了仍律,當(dāng)然這里是沒(méi)有后臺(tái)界面的,你可以自己建立一個(gè)MVC項(xiàng)目來(lái)實(shí)驗(yàn)实柠,也可以建立一個(gè)Vue管理后臺(tái)來(lái)分配水泉,都是很簡(jiǎn)單的,我個(gè)人感覺(jué)已經(jīng)很完美了,咱們的項(xiàng)目基本也成型了草则。
但是這些都是咱們自己造的輪子钢拧,那如果我們用一直很高調(diào)的 IdentityServer4 這個(gè)已經(jīng)成熟的輪子來(lái)實(shí)現(xiàn)接口級(jí)別的動(dòng)態(tài)授權(quán)是怎么做呢?
請(qǐng)看我的下一個(gè)系列吧(.NetCore API + IS4+EFCore+VueAdmin)~~~ 提前祝大家圣誕節(jié)快樂(lè)炕横!
六源内、Github & Gitee
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core
-- END