方法級別的權(quán)限控制(API級別)
Lin的定位在于實現(xiàn)一整套 CMS的解決方案责蝠,它是一個設計方案党巾,提供了不同的后端,不同的前端霜医,而且也支持不同的數(shù)據(jù)庫
目前官方團隊維護 lin-cms-vue,lin-cms-spring-boot,lin-cms-koa,lin-cms-flask
社區(qū)維護了 lin-cms-tp5,lin-cms-react,lin-cms-dotnetcore齿拂,即已支持vue,react二種前端框架,java,nodejs,python,php,c#等五種后端語言肴敛。
- 直接上lin-cms-dotnetcore的demo http://vvlog.baimocore.cn/#/post/5ecf592e-cc24-9f01-004e-6c0a548f2784
- http://cms.baimocore.cn/
- 用戶名:admin
- 密碼:123qwe
下面我們來講一下.NET Core這個項目中權(quán)限控制的實現(xiàn)创肥。
對于CMS來說,一個完善的權(quán)限模塊是必不可少的值朋,是系統(tǒng)內(nèi)置實現(xiàn)的叹侄。為了更加簡單地理解權(quán)限,我們先來理解一下ASP.NET Core有哪些權(quán)限控制昨登。
1.AuthorizeAttribute的作用趾代?
這個特性標簽授權(quán)通過屬性參數(shù)配置,可應用于控制器或操作方法上丰辣,對用戶的身份進行驗證撒强。
如果沒有授權(quán),會返回403狀態(tài)碼笙什,我們可以通過重寫飘哨,來實現(xiàn)返回JSON字符串,讓前臺提示琐凭。前提是請求中間件配置了如下二行芽隆。
app.UseAuthentication(); 認證,明確是誰在操作,認證方式如用戶名密碼,登錄后胚吁,可以得到一個token牙躺,或者寫入cookies,這樣可以確定這個用戶是誰
app.UseAuthorization(); 授權(quán)中間件腕扶,明確你是否有某個權(quán)限孽拷。在http請求時,中間件會在帶有權(quán)限特性標簽 [Authorize] 的操作半抱,進行權(quán)限判斷脓恕,包括角色,策略等窿侈。
該控制器下的操作都必須經(jīng)過身份驗證炼幔,
[Authorize]
public class AccountController : Controller
{
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}
這樣只顯示單個方法必須應用授權(quán)。
public class AccountController : Controller
{
public ActionResult Login()
{
}
[Authorize]
public ActionResult Logout()
{
}
}
如果我們通過AllowAnonymous特性標簽去掉身份驗證棉磨。Login方法無須進行驗證江掩。即可匿名訪問。
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}
- 基于角色的授權(quán)
我們可以通過給這個特性標簽加參數(shù)乘瓤,配置环形,某個方法,控制器是否有這個角色衙傀,如果有此角色才能訪問這些資源抬吟。
單個角色
[Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
}
多個角色,我們可以這樣配置,即用逗號分隔统抬。用戶有其中一個角色即可訪問火本。
[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}
當某個方法必須同時有二個角色怎么辦呢。該控制器只有同時有PowerUser聪建,和ControlPanelUser的角色才能訪問這些資源了钙畔。
[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
public class ControlPanelController : Controller
{
}
- 3.更多。請看官網(wǎng) https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/roles?view=aspnetcore-3.1
更多該特性標簽的介紹金麸,也可參考官網(wǎng),這里就不展開了挥下。
那這個角色,到底在哪配置的棚瘟??
登錄時生成的Token,是基于JWT的偎蘸,其中的Claim的type為ClaimTypes.Role(枚舉值)庄蹋,角色名稱為字符串,與特性標簽中的Roles屬性值相同蔓肯。
如
new Claim(ClaimTypes.Role, "Administrator");
有多個角色時振乏,List<Claim> 多加幾個 new Claim(ClaimTypes.Role, "PowerUser"); 也是支持的蔗包。user為用戶信息,LinGroups為當前用戶的分組(多個)
即如下代碼示例慧邮,多個分組(角色)
var claims = new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, user.Email ?? ""),
new Claim(ClaimTypes.GivenName, user.Nickname ?? ""),
new Claim(ClaimTypes.Name, user.Username ?? ""),
};
user.LinGroups?.ForEach(r =>
{
claims.Add(new Claim(ClaimTypes.Role, r.Name));
});
AuthorizeAttribute源碼
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute, IAuthorizeData
{
public AuthorizeAttribute()
{
}
public AuthorizeAttribute(string policy)
{
this.Policy = policy;
}
public string Policy { get; set; }
public string Roles { get; set; }
public string AuthenticationSchemes { get; set; }
}
我們可以看到,它繼承了Attribute耻矮,說明這是一個特性標簽忆谓,IAuthorizeData是一個接口,有這三個屬性倡缠,約束了 一個規(guī)范,即有角色Roles琢唾,有策略Policy盾饮,有身份驗證方案AuthenticationSchemes,該特性支持Class,支持方法丘损,該特性標簽支持多個共用徘钥,該特性標簽支持被繼承。
基于角色的授權(quán)和基于聲明的授權(quán)是一種預配置的策略吏饿,即固定的角色,固定的Claims驗證贞远。
我們可以基于自定義策略的實現(xiàn)更多的權(quán)限驗證或某些規(guī)則驗證笨忌。
AuthorizeAttribute能做的權(quán)限控制如下
- 基于角色級別的權(quán)限控制(多個角色,單個角色)
- 基于聲明的授權(quán):可自定義聲明特性袱结。
- 基于策略的授權(quán):
lin-cms-dotnetcore中的權(quán)限設計
說了這么多官方提供的,我們講一下lin-cms-dotnetcore中的權(quán)限設計
完整的表結(jié)構(gòu)如下
https://luoyunchong.github.io/vovo-docs/dotnetcore/lin-cms/table.html
LinCmsAuthorizeAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class LinCmsAuthorizeAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Permission { get; }
public string Module { get; }
public LinCmsAuthorizeAttribute(string permission, string module)
{
Permission = permission;
Module = module;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
ClaimsPrincipal claimsPrincipal = context.HttpContext.User;
if (!claimsPrincipal.Identity.IsAuthenticated)
{
HandlerAuthenticationFailed(context, "認證失敗溢吻,請檢查請求頭或者重新登陸", ErrorCode.AuthenticationFailed);
return;
}
IAuthorizationService authorizationService = (IAuthorizationService)context.HttpContext.RequestServices.GetService(typeof(IAuthorizationService));
AuthorizationResult authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, null, new OperationAuthorizationRequirement() { Name = Permission });
if (!authorizationResult.Succeeded)
{
HandlerAuthenticationFailed(context, $"您沒有權(quán)限:{Module}-{Permission}", ErrorCode.NoPermission);
}
}
public void HandlerAuthenticationFailed(AuthorizationFilterContext context, string errorMsg, ErrorCode errorCode)
{
context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Result = new JsonResult(new UnifyResponseDto(errorCode, errorMsg, context.HttpContext));
}
}
上面的實現(xiàn)非常簡單,LinCmsAuthorizeAttribute繼承于Attribute促王,說明是一個特性標簽,有二個屬性Permission而晒,Module倡怎,代表權(quán)限名,模塊名(用于區(qū)分哪個功能模塊)监署,然后將權(quán)限名稱轉(zhuǎn)化為OperationAuthorizationRequirement,然后調(diào)用authorizationService中的方法AuthorizeAsync來完成授權(quán)血公。
接下來缓熟,我們在控制器上使用LinCmsAuthorizeAttribute,那么我們
[Route("cms/admin/group")]
[ApiController]
public class GroupController : ControllerBase
{
private readonly IGroupService _groupService;
public GroupController(IGroupService groupService)
{
_groupService = groupService;
}
[HttpGet("all")]
[LinCmsAuthorize("查詢所有權(quán)限組","管理員")]
public Task<List<LinGroup>> GetListAsync()
{
return _groupService.GetListAsync();
}
[HttpGet("{id}")]
[LinCmsAuthorize("查詢一個權(quán)限組及其權(quán)限","管理員")]
public async Task<GroupDto> GetAsync(long id)
{
GroupDto groupDto = await _groupService.GetAsync(id);
return groupDto;
}
[HttpPost]
[LinCmsAuthorize("新建權(quán)限組","管理員")]
public async Task<UnifyResponseDto> CreateAsync([FromBody] CreateGroupDto inputDto)
{
await _groupService.CreateAsync(inputDto);
return UnifyResponseDto.Success("新建分組成功");
}
[HttpPut("{id}")]
[LinCmsAuthorize("更新一個權(quán)限組","管理員")]
public async Task<UnifyResponseDto> UpdateAsync(long id, [FromBody] UpdateGroupDto updateGroupDto)
{
await _groupService.UpdateAsync(id, updateGroupDto);
return UnifyResponseDto.Success("更新分組成功");
}
[HttpDelete("{id}")]
[LinCmsAuthorize("刪除一個權(quán)限組","管理員")]
public async Task<UnifyResponseDto> DeleteAsync(long id)
{
await _groupService.DeleteAsync(id);
return UnifyResponseDto.Success("刪除分組成功");
}
}
這樣在方法上已經(jīng)加了權(quán)限的標簽够滑,但我們怎么得到系統(tǒng)中的所有權(quán)限,讓用戶配置呢梯投。
獲取控制器及方法特性標簽况毅。本質(zhì)上,是通過反射,掃描當前程序集么鹤,會獲取到一個List味廊,我們可以在系統(tǒng)啟動時把這些數(shù)據(jù)存到數(shù)據(jù)庫中棠耕。
最新的方式是采用此方法柠新,原理都相同。name蕊退,module唯一值框咙。存入lin_permission表中痢甘,這時就有id值了。lin_group_permission就能用分組關(guān)聯(lián)了者铜。
public async Task SeedAsync()
{
List<PermissionDefinition> linCmsAttributes = ReflexHelper.GeAssemblyLinCmsAttributes();
List<LinPermission> insertPermissions = new List<LinPermission>();
List<LinPermission>allPermissions=await _permissionRepository.Select.ToListAsync();
linCmsAttributes.ForEach(r =>
{
bool exist = allPermissions.Any(u => u.Module == r.Module && u.Name == r.Permission);
if (!exist)
{
insertPermissions.Add(new LinPermission(r.Permission, r.Module));
}
});
await _permissionRepository.InsertAsync(insertPermissions);
}
實現(xiàn)方法級的權(quán)限控制源碼解析
上面的LinCmsAttribute調(diào)用了IAuthorizationService類中的方法作烟,那他是什么呢砾医。
原理可以看這個文章ASP.NET Core 認證與授權(quán)[7]:動態(tài)授權(quán)中的自定義授權(quán)過濾器
我們需要了解一下這些類/接口/抽象類
- IAuthorizationService(interface)
- AuthorizationService(class)
- IAuthorizationHandler(interface)
- AuthorizationHandler<TRequirement>(abstract class)
- PermissionAuthorizationHandler(class 自定義的類,繼承AuthorizationHandler)
總結(jié)調(diào)用鏈如下
LinCmsAuthorizeAttribute(繼承了IAsyncAuthorizationFilter的特性標簽)
調(diào)用了---->
IAuthorizationService中的AuthorizeAsync方法
調(diào)用了---->
IAuthorizationHandler中的HandleAsync
調(diào)用了---->
AuthorizationHandler中的HandleRequirementAsync抽象方法
相當于調(diào)用---->
PermissionAuthorizationHandler類中的實現(xiàn)方法HandleRequirementAsync
調(diào)用了---->
IPermissionService類中的CheckPermissionAsync方法。
調(diào)用了---->
IAuditBaseRepository<LinPermission,long>
IAuditBaseRepository<LinGroupPermission, long>
使用FreeSql,判斷當前用戶所在分組是否擁有此權(quán)限压恒。
IAuthorizationService是什么呢错邦。我們可以理解為,驗證當前用戶是否擁有對應的資源權(quán)限伦吠。系統(tǒng)默認實現(xiàn)了該方法
public interface IAuthorizationService
{
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
}
AuthorizationService是什么呢.他實現(xiàn)了IAuthorizationService接口.
通過源碼我們知道魂拦,它調(diào)用 await authorizationHandler.HandleAsync(authContext);
public async Task<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user,
object resource,
IEnumerable<IAuthorizationRequirement> requirements)
{
if (requirements == null)
throw new ArgumentNullException(nameof (requirements));
AuthorizationHandlerContext authContext = this._contextFactory.CreateContext(requirements, user, resource);
foreach (IAuthorizationHandler authorizationHandler in await this._handlers.GetHandlersAsync(authContext))
{
await authorizationHandler.HandleAsync(authContext);
if (!this._options.InvokeHandlersAfterFailure)
{
if (authContext.HasFailed)
break;
}
}
AuthorizationResult authorizationResult = this._evaluator.Evaluate(authContext);
if (authorizationResult.Succeeded)
this._logger.UserAuthorizationSucceeded();
else
this._logger.UserAuthorizationFailed();
return authorizationResult;
}
IAuthorizationHandler 僅一個接口芯勘。
public interface IAuthorizationHandler
{
/// <summary>
/// Makes a decision if authorization is allowed.
/// </summary>
/// <param name="context">The authorization information.</param>
Task HandleAsync(AuthorizationHandlerContext context);
}
AuthorizationHandler,它繼承IAuthorizationHandler
而且他是一個抽象類刨晴,默認實現(xiàn)了HandleAsync方法,子類只用實現(xiàn)HandleRequirementAsync即可狈癞。
public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler
where TRequirement : IAuthorizationRequirement
{
public virtual async Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (TRequirement requirement in context.Requirements.OfType<TRequirement>())
await this.HandleRequirementAsync(context, requirement);
}
protected abstract Task HandleRequirementAsync(
AuthorizationHandlerContext context,
TRequirement requirement);
}
我們就可以繼承AuthorizationHandler蝶桶,子類實現(xiàn)從數(shù)據(jù)庫中取數(shù)據(jù)做對比,其中泛型參數(shù)使用系統(tǒng)內(nèi)置的一個只有Name的類OperationAuthorizationRequirement真竖,當然恢共,如果我們需要更多的參數(shù),可以繼承IAuthorizationRequirement讨韭,增加更多的參數(shù)。
判斷當前用戶是否不為null,當調(diào)用CheckPermissionAsync狰闪,判斷是否有此權(quán)限濒生。
public class PermissionAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement>
{
private readonly IPermissionService _permissionService;
public PermissionAuthorizationHandler(IPermissionService permissionService)
{
_permissionService = permissionService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement)
{
Claim userIdClaim = context.User?.FindFirst(_ => _.Type == ClaimTypes.NameIdentifier);
if (userIdClaim != null)
{
if (await _permissionService.CheckPermissionAsync(requirement.Name))
{
context.Succeed(requirement);
}
}
}
}
另外我們還需要把這個Handler注入到我們的DI中罪治,在ConfigureServices中替換如下服務
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
其中的PermssionAppService中的實現(xiàn),檢查當前登錄的用戶的是否有此權(quán)限
public async Task<bool> CheckPermissionAsync(string permission)
{
long[] groups = _currentUser.Groups;
LinPermission linPermission = await _permissionRepository.Where(r => r.Name == permission).FirstAsync();
bool existPermission = await _groupPermissionRepository.Select
.AnyAsync(r => groups.Contains(r.GroupId) && r.PermissionId == linPermission.Id);
return existPermission;
}