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

方法級別的權(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#等五種后端語言肴敛。

下面我們來講一下.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()
    {
    }
}
  1. 基于角色的授權(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
{
}

更多該特性標簽的介紹金麸,也可參考官網(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;
}

更多參考

開源地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末规阀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子歧胁,更是在濱河造成了極大的恐慌,老刑警劉巖喊巍,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件箍鼓,死亡現(xiàn)場離奇詭異款咖,居然都是意外死亡奄喂,警方通過查閱死者的電腦和手機海洼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來域帐,“玉大人是整,你說我怎么就攤上這事「∪耄” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵陋率,是天一觀的道長。 經(jīng)常有香客問我筒愚,道長,這世上最難降的妖魔是什么句伶? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任陆淀,我火速辦了婚禮,結(jié)果婚禮上楚堤,老公的妹妹穿的比我還像新娘含懊。我一直安慰自己,他們只是感情好岔乔,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布雏门。 她就那樣靜靜地躺著掸掏,像睡著了一般宙帝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上息裸,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天沪编,我揣著相機與錄音,去河邊找鬼访圃。 笑死相嵌,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的饭宾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼徽鼎,長吁一口氣:“原來是場噩夢啊……” “哼弹惦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起石抡,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤助泽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后侠讯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暑刃,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年宵膨,在試婚紗的時候發(fā)現(xiàn)自己被綠了炸宵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡土全,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瑞凑,到底是詐尸還是另有隱情概页,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布技掏,位于F島的核電站项鬼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏涧衙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望稚虎。 院中可真熱鬧偎捎,春花似錦、人聲如沸茴她。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽己沛。三九已至距境,卻和暖如春垮卓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诬滩。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工灭将, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人愚臀。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓矾利,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舶斧。 傳聞我的和親對象是個殘疾皇子察皇,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355