lin-cms-dotnetcore.是如何方法級(jí)別的權(quán)限控制(API級(jí)別)的

方法級(jí)別的權(quán)限控制(API級(jí)別)

Lin的定位在于實(shí)現(xiàn)一整套 CMS的解決方案,它是一個(gè)設(shè)計(jì)方案,提供了不同的后端,不同的前端,而且也支持不同的數(shù)據(jù)庫

目前官方團(tuán)隊(duì)維護(hù) lin-cms-vue,lin-cms-spring-boot,lin-cms-koa,lin-cms-flask
社區(qū)維護(hù)了 lin-cms-tp5,lin-cms-react,lin-cms-dotnetcore,即已支持vue,react二種前端框架,java,nodejs,python,php,c#等五種后端語言。

下面我們來講一下.NET Core這個(gè)項(xiàng)目中權(quán)限控制的實(shí)現(xiàn)。

對(duì)于CMS來說,一個(gè)完善的權(quán)限模塊是必不可少的,是系統(tǒng)內(nèi)置實(shí)現(xiàn)的。為了更加簡單地理解權(quán)限,我們先來理解一下ASP.NET Core有哪些權(quán)限控制。

1.AuthorizeAttribute的作用?

這個(gè)特性標(biāo)簽授權(quán)通過屬性參數(shù)配置,可應(yīng)用于控制器或操作方法上,對(duì)用戶的身份進(jìn)行驗(yàn)證。

如果沒有授權(quán),會(huì)返回403狀態(tài)碼,我們可以通過重寫,來實(shí)現(xiàn)返回JSON字符串,讓前臺(tái)提示。前提是請(qǐng)求中間件配置了如下二行。

  • app.UseAuthentication(); 認(rèn)證,明確是誰在操作,認(rèn)證方式如用戶名密碼,登錄后,可以得到一個(gè)token,或者寫入cookies,這樣可以確定這個(gè)用戶是誰

  • app.UseAuthorization(); 授權(quán)中間件,明確你是否有某個(gè)權(quán)限。在http請(qǐng)求時(shí),中間件會(huì)在帶有權(quán)限特性標(biāo)簽 [Authorize] 的操作,進(jìn)行權(quán)限判斷,包括角色,策略等。

該控制器下的操作都必須經(jīng)過身份驗(yàn)證,

[Authorize]
public class AccountController : Controller
{
    public ActionResult Login()
    {
    }

    public ActionResult Logout()
    {
    }
}

這樣只顯示單個(gè)方法必須應(yīng)用授權(quán)。

public class AccountController : Controller
{
   public ActionResult Login()
   {
   }

   [Authorize]
   public ActionResult Logout()
   {
   }
}

如果我們通過AllowAnonymous特性標(biāo)簽去掉身份驗(yàn)證。Login方法無須進(jìn)行驗(yàn)證。即可匿名訪問。

[Authorize]
public class AccountController : Controller
{
    [AllowAnonymous]
    public ActionResult Login()
    {
    }

    public ActionResult Logout()
    {
    }
}
  1. 基于角色的授權(quán)

我們可以通過給這個(gè)特性標(biāo)簽加參數(shù),配置,某個(gè)方法,控制器是否有這個(gè)角色,如果有此角色才能訪問這些資源。

單個(gè)角色

[Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
}

多個(gè)角色,我們可以這樣配置,即用逗號(hào)分隔。用戶有其中一個(gè)角色即可訪問。

[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}

當(dāng)某個(gè)方法必須同時(shí)有二個(gè)角色怎么辦呢。該控制器只有同時(shí)有PowerUser,和ControlPanelUser的角色才能訪問這些資源了。

[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
public class ControlPanelController : Controller
{
}

更多該特性標(biāo)簽的介紹,也可參考官網(wǎng),這里就不展開了。

那這個(gè)角色,到底在哪配置的??

登錄時(shí)生成的Token,是基于JWT的,其中的Claim的type為ClaimTypes.Role(枚舉值),角色名稱為字符串,與特性標(biāo)簽中的Roles屬性值相同。

new Claim(ClaimTypes.Role, "Administrator");

有多個(gè)角色時(shí),List<Claim> 多加幾個(gè) new Claim(ClaimTypes.Role, "PowerUser"); 也是支持的。user為用戶信息,LinGroups為當(dāng)前用戶的分組(多個(gè))

即如下代碼示例,多個(gè)分組(角色)

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,說明這是一個(gè)特性標(biāo)簽,IAuthorizeData是一個(gè)接口,有這三個(gè)屬性,約束了 一個(gè)規(guī)范,即有角色Roles,有策略Policy,有身份驗(yàn)證方案AuthenticationSchemes,該特性支持Class,支持方法,該特性標(biāo)簽支持多個(gè)共用,該特性標(biāo)簽支持被繼承。

基于角色的授權(quán)和基于聲明的授權(quán)是一種預(yù)配置的策略,即固定的角色,固定的Claims驗(yàn)證。

我們可以基于自定義策略的實(shí)現(xiàn)更多的權(quán)限驗(yàn)證或某些規(guī)則驗(yàn)證。

AuthorizeAttribute能做的權(quán)限控制如下

  • 基于角色級(jí)別的權(quán)限控制(多個(gè)角色,單個(gè)角色)
  • 基于聲明的授權(quán):可自定義聲明特性。
  • 基于策略的授權(quán):

lin-cms-dotnetcore中的權(quán)限設(shè)計(jì)

說了這么多官方提供的,我們講一下lin-cms-dotnetcore中的權(quán)限設(shè)計(jì)

完整的表結(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, "認(rèn)證失敗,請(qǐng)檢查請(qǐng)求頭或者重新登陸", 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));
        }
    }

上面的實(shí)現(xiàn)非常簡單,LinCmsAuthorizeAttribute繼承于Attribute,說明是一個(gè)特性標(biāo)簽,有二個(gè)屬性Permission,Module,代表權(quán)限名,模塊名(用于區(qū)分哪個(gè)功能模塊),然后將權(quán)限名稱轉(zhuǎn)化為OperationAuthorizationRequirement,然后調(diào)用authorizationService中的方法AuthorizeAsync來完成授權(quán)。

接下來,我們?cè)诳刂破魃鲜褂肔inCmsAuthorizeAttribute,那么我們

[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("查詢一個(gè)權(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("更新一個(gè)權(quán)限組","管理員")]
    public async Task<UnifyResponseDto> UpdateAsync(long id, [FromBody] UpdateGroupDto updateGroupDto)
    {
        await _groupService.UpdateAsync(id, updateGroupDto);
        return UnifyResponseDto.Success("更新分組成功");
    }

    [HttpDelete("{id}")]
    [LinCmsAuthorize("刪除一個(gè)權(quán)限組","管理員")]
    public async Task<UnifyResponseDto> DeleteAsync(long id)
    {
        await _groupService.DeleteAsync(id);
        return UnifyResponseDto.Success("刪除分組成功");
    }

}

這樣在方法上已經(jīng)加了權(quán)限的標(biāo)簽,但我們?cè)趺吹玫较到y(tǒng)中的所有權(quán)限,讓用戶配置呢。
獲取控制器及方法特性標(biāo)簽。本質(zhì)上,是通過反射,掃描當(dāng)前程序集,會(huì)獲取到一個(gè)List,我們可以在系統(tǒng)啟動(dòng)時(shí)把這些數(shù)據(jù)存到數(shù)據(jù)庫中。

最新的方式是采用此方法,原理都相同。name,module唯一值。存入lin_permission表中,這時(shí)就有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);
 }

實(shí)現(xiàn)方法級(jí)的權(quán)限控制源碼解析

上面的LinCmsAttribute調(diào)用了IAuthorizationService類中的方法,那他是什么呢。
原理可以看這個(gè)文章ASP.NET Core 認(rèn)證與授權(quán)[7]:動(dòng)態(tài)授權(quán)中的自定義授權(quán)過濾器

我們需要了解一下這些類/接口/抽象類

  • IAuthorizationService(interface)
  • AuthorizationService(class)
  • IAuthorizationHandler(interface)
  • AuthorizationHandler<TRequirement>(abstract class)
  • PermissionAuthorizationHandler(class 自定義的類,繼承AuthorizationHandler)

總結(jié)調(diào)用鏈如下

LinCmsAuthorizeAttribute(繼承了IAsyncAuthorizationFilter的特性標(biāo)簽)
調(diào)用了---->
IAuthorizationService中的AuthorizeAsync方法
調(diào)用了---->
IAuthorizationHandler中的HandleAsync
調(diào)用了---->
AuthorizationHandler中的HandleRequirementAsync抽象方法
相當(dāng)于調(diào)用---->
PermissionAuthorizationHandler類中的實(shí)現(xiàn)方法HandleRequirementAsync
調(diào)用了---->
IPermissionService類中的CheckPermissionAsync方法。
調(diào)用了---->
IAuditBaseRepository<LinPermission,long>
IAuditBaseRepository<LinGroupPermission, long>
使用FreeSql,判斷當(dāng)前用戶所在分組是否擁有此權(quán)限。

IAuthorizationService是什么呢。我們可以理解為,驗(yàn)證當(dāng)前用戶是否擁有對(duì)應(yīng)的資源權(quán)限。系統(tǒng)默認(rèn)實(shí)現(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是什么呢.他實(shí)現(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 僅一個(gè)接口。

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
而且他是一個(gè)抽象類,默認(rèn)實(shí)現(xiàn)了HandleAsync方法,子類只用實(shí)現(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,子類實(shí)現(xiàn)從數(shù)據(jù)庫中取數(shù)據(jù)做對(duì)比,其中泛型參數(shù)使用系統(tǒng)內(nèi)置的一個(gè)只有Name的類OperationAuthorizationRequirement,當(dāng)然,如果我們需要更多的參數(shù),可以繼承IAuthorizationRequirement,增加更多的參數(shù)。

判斷當(dāng)前用戶是否不為null,當(dāng)調(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);
                }
            }
        }
    }

另外我們還需要把這個(gè)Handler注入到我們的DI中,在ConfigureServices中替換如下服務(wù)

services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();

其中的PermssionAppService中的實(shí)現(xiàn),檢查當(dāng)前登錄的用戶的是否有此權(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;
}

更多參考

開源地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容