ASP.NET CORE 第十篇 JWT完美實(shí)現(xiàn)權(quán)限與接口的動(dòng)態(tài)分配

原文作者:老張的哲學(xué)

一、JWT授權(quán)驗(yàn)證雏亚,我們經(jīng)歷了哪些

看過(guò)我寫的這個(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日杈、五 || Swagger的使用 3.3 JWT權(quán)限驗(yàn)證【修改】
2遣铝、36 ║解決JWT權(quán)限驗(yàn)證過(guò)期問(wèn)題

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接口地址上寫特定的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上胶台,只需要寫上策略的名稱即可:

image

相信大家也都是這么做的,當(dāng)然我之前也是這么寫的杂抽。雖然我們?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è)接口的寫權(quán)限策略济竹,不靈活痕檬!我也是想了很久,才想到了今天的這個(gè)辦法(請(qǐng)耐心往下看)送浊。

3梦谜、將接口地址和角色授權(quán)分離

當(dāng)然上邊的方法也能實(shí)現(xiàn)我們的小需要,每個(gè)接口一個(gè)個(gè)都寫好即可袭景,但是作為強(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è)大佬寫的文章幅疼,我就使用了米奸,這里注明下:借稿作者:《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ì)不好窘俺,大家看我寫的思路即可饲帅,自己可以做擴(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í)體模型:(真是Blog.Core項(xiàng)目中,可能會(huì)有些許變動(dòng)铐料,這里只是作為說(shuō)明渐裂,如果想看真是的代碼,請(qǐng)下載最新項(xiàng)目代碼)

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í)體模型:

using System;
using System.Collections.Generic;
using System.Text;

namespace Blog.Core.Model
{
    /// <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芽丹,或者自己自定義...
    }
}

整體數(shù)據(jù)庫(kù)UML圖如下(忽略箭頭北启,沒(méi)意義):(@鐵梧桐 感謝提供,工具 PowerDesigner)

image

2拔第、Service 應(yīng)用服務(wù)接口設(shè)計(jì)
這個(gè)很簡(jiǎn)單咕村,CURD中,我只是簡(jiǎn)單寫了一個(gè)查詢?nèi)筷P(guān)系的接口蚊俺,其他的都很簡(jiǎn)單懈涛,相信自己也能搞定,IRepository.cs 泳猬、Repository.cs 和 IServices.cs 這三個(gè)我就不多寫了批钠,簡(jiǎn)單看下 Services.cs 的一個(gè)查詢?nèi)拷巧涌陉P(guān)系的方法:

using Blog.Core.Common.Attribue;
using Blog.Core.IRepository;
using Blog.Core.IServices;
using Blog.Core.Model.Models;
using Blog.Core.Services.Base;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Blog.Core.Services
{
    /// <summary>
    /// RoleModulePermissionServices 應(yīng)用服務(wù)
    /// </summary>
    public class RoleModulePermissionServices : BaseServices<RoleModulePermission>, IRoleModulePermissionServices
    {
        readonly IRoleModulePermissionRepository dal;
        readonly IModuleRepository moduleRepository;
        readonly IRoleRepository roleRepository;

        /// <summary>
        /// // 將多個(gè)倉(cāng)儲(chǔ)接口注入
        /// </summary>
        public RoleModulePermissionServices(IRoleModulePermissionRepository dal, IModuleRepository moduleRepository, IRoleRepository roleRepository)
        {
            this.dal = dal;
            this.moduleRepository = moduleRepository;
            this.roleRepository = roleRepository;
            this.baseDal = dal;
        }

        /// <summary>
        ///  獲取全部 角色接口(按鈕)關(guān)系數(shù)據(jù) 注意我使用咱們之前的AOP緩存宇植,很好的應(yīng)用上了
        /// </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;
        }

        public async Task<List<TestMuchTableResult>> QueryMuchTable()
        {
            return await dal.QueryMuchTable();
        }

        public async Task<List<RoleModulePermission>> TestModelWithChildren()
        {
            return await dal.WithChildrenModel();
        }
    }
}

我自己簡(jiǎn)單的設(shè)計(jì)了下數(shù)據(jù),如果有想我的數(shù)據(jù)的埋心,請(qǐng)留言指郁,我把這個(gè)Sql數(shù)據(jù)文件放到 Github :https://github.com/anjoy8/Blog.Core/blob/master/Blog.Core/wwwroot/權(quán)限.sql

這里設(shè)計(jì)使用外鍵,多對(duì)多的形式拷呆,可以很好的實(shí)現(xiàn)擴(kuò)展闲坎,比如接口地址API變了,但是我們使用的是id茬斧,可以很靈活的適應(yīng)改變腰懂。

image

三、基于策略授權(quán)的自定義驗(yàn)證——核心

之前咱們也使用過(guò)中間件 JwtTokenAuth 來(lái)進(jìn)行授權(quán)驗(yàn)證啥供,后來(lái)因?yàn)檫^(guò)期時(shí)間的問(wèn)題悯恍,然后使用的官方的中間件app.UseAuthentication() 库糠,今天咱們就寫一個(gè)3.0版本的驗(yàn)證方法伙狐,基于AuthorizationHandler 的權(quán)限授權(quán)處理器,具體的請(qǐng)往下看瞬欧,如果看不懂贷屎,可以直接 pull 下我的 Github 代碼即可。

一共是四個(gè)類:

image

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同眯、PermissionItem 憑據(jù)實(shí)體

說(shuō)白了,這個(gè)就是用來(lái)存放我們用戶登錄成果后唯鸭,在httptext中存放的角色信息的须蜗,是下邊 必要參數(shù)類 PermissionRequirement 的一個(gè)屬性,很簡(jiǎn)單目溉,不細(xì)說(shuō):

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// 用戶或角色或其他憑據(jù)實(shí)體
    /// </summary>
    public class PermissionItem
    {
        /// <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<PermissionItem> 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<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 自定義授權(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.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轉(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)寫了,應(yīng)該能看懂嚼松,這里只有一點(diǎn)嫡良,就是我們自定義的這個(gè)處理器,是繼承了AuthorizationHandler 献酗,而且它還需要一個(gè)泛型類寝受,并且該泛型類必須繼承IAuthorizationRequirement 這個(gè)授權(quán)要求的接口,這樣我們就可以很方便的把我們的自定義的權(quán)限參數(shù)傳入授權(quán)處理器中罕偎。

image

好啦很澄,到了這里,我們已經(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ù),之前我們都是寫在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)橹恍枰獙懸粋€(gè)Url即可,比如:/api/values 代表了Get Post Put Delete等多個(gè)月劈。
            // 如果想寫死度迂,可以直接在這里寫藤乙。
            //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"},
            //              };

            // 如果要數(shù)據(jù)庫(kù)動(dòng)態(tài)綁定,這里先留個(gè)空惭墓,后邊處理器里動(dòng)態(tài)賦值
            var permission = new List<PermissionItem>();

            // 角色與接口的權(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
                );

            // ① 核心之一,配置授權(quán)服務(wù)钧萍,也就是具體的規(guī)則褐缠,已經(jīng)對(duì)應(yīng)的權(quán)限策略,比如公司不同權(quán)限的門禁卡
            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));
            })
            // ② 核心之二风瘦,必需要配置認(rèn)證服務(wù)队魏,這里是jwtBearer默認(rèn)認(rèn)證,比如光有卡沒(méi)用万搔,得能識(shí)別他們
            .AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            // ③ 核心之三胡桨,針對(duì)JWT的配置,比如門禁是如何識(shí)別的蟹略,是放射卡登失,還是磁卡
            .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

注意一定要配置這三個(gè)核心(.AddAuthorization意敛、.AddAuthentication、.AddJwtBearer)膛虫,否則會(huì)報(bào)錯(cuò):

image

3草姻、在登錄接口中,賦值過(guò)期時(shí)間等信息

image

雖然我們?cè)?startup 中也設(shè)置了過(guò)期時(shí)間稍刀,但是我們還需要在每一個(gè) token 的聲明列表中(claims)中撩独,配置過(guò)期時(shí)間,只不過(guò)兩個(gè)時(shí)間一樣罷了账月。

4综膀、在接口中很方便調(diào)用

這樣定義好以后,我們只需要很方便的在每一個(gè)controller上邊寫上 [Authorize("Permission")]局齿,這個(gè)驗(yàn)證特性即可剧劝,這個(gè)名字就是我們的策略名,我們就不用再想哪一個(gè)接口對(duì)應(yīng)哪些Roles了抓歼,是不是更方便了讥此!當(dāng)然如果不寫這個(gè)特性的話拢锹,不會(huì)被限制,比如那些前臺(tái)的頁(yè)面接口萄喳,就不需要被限制卒稳。

image

5、使用效果展示

咱們看看平時(shí)會(huì)遇到的4種情況他巨。

注意:下邊的演示展哭,是用的 public async Task<object> GetJWTToken3(string name, string pass) 這個(gè)新接口獲取的Token

image

你也可以直接使用我的在線地址 http://123.206.33.109:8081/swagger/index.html 來(lái)操作,具體的步驟見(jiàn)下面的這三個(gè)情況闻蛀。

接口沒(méi)有配置權(quán)限

這種情況匪傍,無(wú)論是數(shù)據(jù)庫(kù)是否配置,都會(huì)很正常的通過(guò)HTTP請(qǐng)求觉痛,從而獲取到我們的數(shù)據(jù)役衡,就比如登錄頁(yè):

image

接口設(shè)置了權(quán)限,但是數(shù)據(jù)庫(kù)沒(méi)有配置

咱們以 ValuesController 為例子


image

現(xiàn)在我們把API接口是 /api/values 的接口和角色關(guān)聯(lián)的表給邏輯刪除了薪棒,那這個(gè)時(shí)候手蝎,也就代表了,當(dāng)前接口雖然設(shè)置了權(quán)限俐芯,但是在數(shù)據(jù)庫(kù)里并沒(méi)有配置它與Role的關(guān)系:

image

那如果我們?cè)L問(wèn)的話會(huì)是怎樣:

image

首先棵介,我們看到在獲取到的四個(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ò)展。

接口設(shè)置了權(quán)限钞脂,并且數(shù)據(jù)庫(kù)也配置了

還是使用咱們的 ValueController.cs 揣云,這時(shí)候咱們把剛剛邏輯刪除的改成False:

image

然后看看我們的執(zhí)行過(guò)程:

image

發(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ò)期:

image

好啦顷编,這些簡(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è)讥电!

1、情景補(bǔ)充

有的小伙伴在研究或者使用這個(gè)方法的時(shí)候轧抗,出現(xiàn)了疑惑,主要是兩個(gè)問(wèn)題:

1瞬测、我如果后臺(tái)修改權(quán)限了横媚,想立刻或者 關(guān)閉瀏覽器下次打開(kāi)的時(shí)候更新權(quán)限咋辦?

2月趟、如果我Token的過(guò)期時(shí)間比較短灯蝴,比如一天,那如何實(shí)現(xiàn)滑動(dòng)更新孝宗,就是不會(huì)正在使用的時(shí)候穷躁,突然去登錄頁(yè)?

我也想了想因妇,大概有以下自己的想法问潭,大家可以參考一下猿诸,歡迎提出批評(píng):

1、如果后臺(tái)管理員修改了某一個(gè)人的權(quán)限狡忙,我會(huì)把每一個(gè)Token放到Redis緩存里梳虽,然后主要是 Token 的值,還有過(guò)期時(shí)間灾茁,權(quán)限等窜觉,如果管理員修改了權(quán)限(這個(gè)時(shí)候Token就不能使用了,因?yàn)檫@個(gè)Token還是之前的Roles權(quán)限)北专,然后就會(huì)更新了數(shù)據(jù)庫(kù)的Role禀挫,還會(huì)把Redis里的該Token信息給Delete掉,這樣用戶再訪問(wèn)下一個(gè)頁(yè)面的時(shí)候拓颓,我們先校驗(yàn)Redis緩存里是否有這個(gè) Token 數(shù)據(jù)特咆,如果有,還繼續(xù)往下走录粱,如果沒(méi)有了腻格,就返回401讓用戶重新登錄∩斗保可以使用一個(gè)中間件來(lái)處理當(dāng)前Token是否有效菜职。

2、上邊寫到了在net core api里增加一個(gè)中間件來(lái)判斷Token是否有效旗闽,那如果無(wú)效了或者是被管理員修改了權(quán)限酬核,導(dǎo)致 Token 被禁掉以后,又不想讓用戶重新登錄怎么辦呢适室,我就想的是在 Http.js 封裝請(qǐng)求方法中嫡意,寫一個(gè),每次用戶訪問(wèn)的之前捣辆,都判斷一下當(dāng)前 Token 是否有效的JS方法蔬螟,如果有效則繼續(xù)調(diào)用下一個(gè)接口,如果無(wú)效汽畴,這個(gè)時(shí)候就可以在后臺(tái)重新生成一個(gè) Token 并返回到前臺(tái)旧巾,保存到localstroage里,繼續(xù)用新的 Token 調(diào)用下一個(gè)接口忍些。

3鲁猩、用上邊的方法,你會(huì)感覺(jué)這樣每次都會(huì)多一次調(diào)用罢坝,會(huì)占資源廓握,你可以每天執(zhí)行一次,或者就是每次登錄的成功后,不僅把 Token 存在本地隙券,把過(guò)期時(shí)間也存下來(lái)男应,這樣每次請(qǐng)求前可以判斷是否過(guò)期,如果過(guò)期了呢是尔,就先調(diào)用重新獲取Token 的接口方法殉了,然后再往下走。

可能你會(huì)感覺(jué)很麻煩拟枚,很荒唐薪铜,不過(guò)微信小程序就是這么處理的,不信你可以去研究下恩溅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末隔箍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子脚乡,更是在濱河造成了極大的恐慌蜒滩,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奶稠,死亡現(xiàn)場(chǎng)離奇詭異俯艰,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)锌订,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門竹握,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人辆飘,你說(shuō)我怎么就攤上這事啦辐。” “怎么了蜈项?”我有些...
    開(kāi)封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵芹关,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我紧卒,道長(zhǎng)侥衬,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任常侦,我火速辦了婚禮浇冰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘聋亡。我一直安慰自己,他們只是感情好际乘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布坡倔。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪罪塔。 梳的紋絲不亂的頭發(fā)上投蝉,一...
    開(kāi)封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音征堪,去河邊找鬼瘩缆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛佃蚜,可吹牛的內(nèi)容都是我干的庸娱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼谐算,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼熟尉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起洲脂,我...
    開(kāi)封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤斤儿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后恐锦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體往果,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年一铅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了陕贮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡馅闽,死狀恐怖飘蚯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情福也,我是刑警寧澤局骤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站暴凑,受9級(jí)特大地震影響峦甩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜现喳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一凯傲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗦篱,春花似錦冰单、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)涵卵。三九已至,卻和暖如春荒叼,著一層夾襖步出監(jiān)牢的瞬間轿偎,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工被廓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坏晦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓嫁乘,卻偏偏與公主長(zhǎng)得像昆婿,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子亦渗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容