從壹開始先後端分離[.NetCore] 37 ║JWT完美實現權限與接口的動態分配

緣起

本文已經有了對應的管理後臺,地址:https://github.com/anjoy8/Blog.Adminhtml

哈嘍你們好呀!又過去一週啦,這些天小夥伴們有沒有學習呀,已經有一週沒有更新文章了,不過晚上的時候,我也會看一些書和資料,這裏給你們分享下:git

一、以前簡單的寫了一個DDD+CQRS+ES的第二個系列《D3模式設計初探 與 個人計劃書》,已經基本完結了,寫的比較簡單,而後我也找到了微軟的一個官方的一個資料《CQRS Journey》,不知道有沒有哪位小夥伴看,全英文的,我還在看,由於官方已經不翻譯了(更正:樓下有小夥伴評論,已經有這本書了,《探索CQRS和事件源(微軟雲計算系列叢書)》,先買了看看,好了再反饋),因此我打算本身翻譯下,若是有想和我一塊兒的小夥伴,能夠留言,我們成立一個小組,一塊兒翻譯這個資料,主要是關於CQRS讀寫分離的和ES事件溯源的,固然是基於DDD領域驅動設計架構的基礎上,有助於本身的理解。github

二、而後就是在看《IdentityServer4.Samples》,是一個IS4的官方栗子,而後找了找資料,由於個人第三個系列教程 —— 是想作一個權限管理系統(這裏是打算搭建一個 IdentityServer4 + .net core Identity + EFCore + VueAdmin )的一個先後端分離的權限管理框架,初步打算是基於按鈕級別的權限控制,從部門到崗位,角色到用戶等都動態可控(保存到數據庫,能夠管理後臺修改),開筆時間尚未定,由於還在學習和年末公司總結了,若是有小夥伴想一塊兒開發,能夠看看上邊的這些技術,我們之後能夠合做,爲.net 開源社區作貢獻(這裏說下是徹底無償的喲)。web

好啦,廢話很少說,由於今天是不按期更新系列,因此會之間進入主題,不會有概念性的講解,立刻開始今天的內容啦!主要是如下幾個方面:sql

重要必看!剛剛下邊評論有大神提出異議,我表示他說的有道理,如下內容,本身練習玩玩兒便可,固然小項目也可使用,不過中型以上的項目,仍是要用IdentityServer4這種成熟的輪子,本文只是一個小思考,還有不少地方值得推敲和商榷,不過這不就是學習的目的麼,發表思想,提出異議,作出解決,加油!數據庫

一、實現角色和接口API保存到數據庫,實現動態分配json

二、接口中,以前的受權方法依然保留,在 BlogController.cs 中仍是使用的基於角色的受權方式。小程序

三、本文是 IS4 系列的鋪墊文章。後端

 

1、JWT受權驗證,咱們經歷了哪些

看過我寫的這個第一個系列《先後端分離》的小夥伴都知道,我用到了JWT來實現的權限驗證,目前已經達到什麼程度的驗證了呢,這裏我經歷了三個步驟:微信小程序

這裏強調下,若是你是第一次看這個文章,除非是有必定的基礎,或者是一直跟着個人代碼的,否則的話,會有點兒懵,若是不知足上邊兩個條件,請先看我以前的兩篇文章,基礎:

一、五 || Swagger的使用 3.3 JWT權限驗證【修改】

二、36 ║解決JWT權限驗證過時問題

 

一、直接在 Api 接口地址上設計 Roles 信息

這個也是最簡單,最粗暴的方法,直接這麼配置

    /// <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信息 保存到了數據庫,實現了動態化,可是具體受權的時候,仍是須要手動在API接口地址上寫特定的Role權限,這樣才能對其進行匹配和受權,若是真的有一個接口能夠被多個角色訪問,那就須要壘了不少了,不是很好。

 

二、對不一樣模塊的角色們 創建策略

鑑於上邊的問題,我考慮着對不一樣的角色創建不一樣的策略,並在 Startup.cs 啓動類中,配置服務:

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上,只須要寫上策略的名稱便可:

 

相信你們也都是這麼作的,固然我以前也是這麼寫的。雖然咱們在啓動類 Startup.cs 中,對咱們的Roles作了策略,看起來不用一一的配置 Roles 了,可是你們會發現,好像這個功能並無想象中那麼美麗,由於最關鍵的問題,咱們沒有解決,由於這樣咱們仍是須要手動一個接口一個接口的寫權限策略,不靈活!我也是想了好久,纔想到了今天的這個辦法(請耐心往下看)。

 

三、將接口地址和角色受權分離

固然上邊的方法也能實現咱們的小須要,每一個接口一個個都寫好便可,可是做爲強迫症的我,總感受會有辦法能夠把 API 接口,和 Role 權限剝離開,也能像用戶和 Role那樣,保存到數據庫,實現動態分配,就這樣我研究了微軟的官方文檔,偶然發現了微軟官方文檔的《Policy-based authorization》基於策略的受權,正好也找到了博客園一個大佬寫的文章,我就使用了,這裏註明下:借稿做者:asp.net core 2.0 web api基於JWT自定義策略受權》。

而後我在他的基礎上,配合着我們的項目,作了調整,通過測試,完美的解決了我們的問題,能夠動態的數據庫進行配置,那具體是怎麼實現的呢,請往下看。

 

2、接口地址和角色保存到數據庫

數據庫設計很差,你們看我寫的思路便可,本身能夠作擴展和優化,但願仍是本身動手。

 既然要實現動態綁定,咱們就須要把接口地址信息、角色信息保存到數據庫,那表結構是怎樣的呢,其實目前個人數據庫結構已經能夠知足了要求了,只不過須要稍微調整下,由於以前我是用EF來設計的,這裏用SqlSugar會出現一個問題,因此須要在 Blog.Core.Model 層引用 sqlSugarCore 的 Nuget 包,而後把實體 RoleModulePermission.cs 中的三個參數作下忽略處理。

一、實體模型設計

首先是接口和角色的關聯表的實體模型:(真是Blog.Core項目中,可能會有些許變更,這裏只是做爲說明,若是想看真是的代碼,請下載最新項目代碼

namespace Blog.Core.Model.Models
{
    /// <summary>
    /// 接口、角色關聯表(之後能夠把按鈕設計進來)
    /// </summary>
    public class RoleModulePermission
    {
        public int Id { get; set; }
        /// <summary>
        /// 角色ID
        /// </summary>
        public int RoleId { get; set; }
        /// <summary>
        /// 菜單ID,這裏就是api地址的信息
        /// </summary>
        public int ModuleId { get; set; }
        /// <summary>
        /// 按鈕ID
        /// </summary>
        public int? PermissionId { get; set; }
        /// <summary>
        /// 建立時間
        /// </summary>
        public DateTime? CreateTime { get; set; }
        /// <summary>
        ///獲取或設置是否禁用,邏輯上的刪除,非物理刪除
        /// </summary>
        public bool? IsDeleted { get; set; }
        
        // 等等,還有其餘屬性,其餘的能夠參考Code,或者自定義...

        // 請注意,下邊三個實體參數,只是作傳參做用,因此忽略下,否則會認爲缺乏字段
        [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接口信息保存的實體模型:

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>
        /// 圖標
        /// </summary>
        public string Icon { get; set; }
        /// <summary>
        /// 菜單編號
        /// </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,或者本身自定義...
    }
}

 

總體數據庫UML圖以下(忽略箭頭,沒意義):(@鐵梧桐 感謝提供,工具 PowerDesigner) 

 

二、Service 應用服務接口設計

這個很簡單,CURD中,我只是簡單寫了一個查詢所有關係的接口,其餘的都很簡單,相信本身也能搞定,IRepository.cs 、Repository.cs 和 IServices.cs 這三個我就很少寫了,簡單看下 Services.cs 的一個查詢所有角色接口關係的方法:

namespace Blog.Core.Services
{
    /// <summary>
    /// RoleModulePermissionServices 應用服務
    /// </summary>    
    public class RoleModulePermissionServices : BaseServices<RoleModulePermission>, IRoleModulePermissionServices
    {

        IRoleModulePermissionRepository dal;
        IModuleRepository moduleRepository;
        IRoleRepository roleRepository;

        // 將多個倉儲接口注入
        public RoleModulePermissionServices(IRoleModulePermissionRepository dal, IModuleRepository moduleRepository, IRoleRepository roleRepository)
        {
            this.dal = dal;
            this.moduleRepository = moduleRepository;
            this.roleRepository = roleRepository;
            base.baseDal = dal;
        }

        /// <summary>
        /// 獲取所有 角色接口(按鈕)關係數據 注意我使用我們以前的AOP緩存,很好的應用上了
        /// </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;
        }

    }
}

 

我本身簡單的設計了下數據,

表結構與數據,都已經經過 Codefirst+DataSeed 方式,支持MSSql、Oracle、Mysql、Sqlite等多種數據庫,具體的查看個人 Github 的 README.md

 

 

這裏設計使用外鍵,多對多的形式,能夠很好的實現擴展,好比接口地址API變了,可是咱們使用的是id,能夠很靈活的適應改變。

 

 

3、基於策略受權的自定義驗證——核心

以前我們也使用過中間件 JwtTokenAuth 來進行受權驗證,後來由於過時時間的問題,而後使用的官方的中間件app.UseAuthentication() ,今天我們就寫一個3.0版本的驗證方法,基於AuthorizationHandler 的權限受權處理器,具體的請往下看,若是看不懂,能夠直接 pull 下個人 Github 代碼便可。

一共是四個類:

 

一、JwtToken 生成令牌

這個很簡單,就是咱們以前的 Token 字符串生成類,這裏不過多作解釋,只是要注意一下下邊紅色的參數 PermissionRequirement ,數據是從Startup.cs 中注入的,下邊會說到。

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// JWTToken生成類
    /// </summary>
    public class JwtToken
    {
        /// <summary>
        /// 獲取基於JWT的Token
        /// </summary>
        /// <param name="claims">須要在登錄的時候配置</param>
        /// <param name="permissionRequirement">在startup中定義的參數</param>
        /// <returns></returns>
        public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
        {
            var now = DateTime.Now;
            // 實例化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);

            //打包返回前臺
            var responseJson = new
            {
                success = true,
                token = encodedJwt,
                expires_in = permissionRequirement.Expiration.TotalSeconds,
                token_type = "Bearer"
            };
            return responseJson;
        }
    }
}

 

二、PermissionItem 憑據實體

說白了,這個就是用來存放咱們用戶登陸成果後,在httptext中存放的角色信息的,是下邊 必要參數類 PermissionRequirement 的一個屬性,很簡單,不細說:

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// 用戶或角色或其餘憑據實體
    /// </summary>
    public class PermissionItem
    {
        /// <summary>
        /// 用戶或角色或其餘憑據名稱
        /// </summary>
        public virtual string Role { get; set; }
        /// <summary>
        /// 請求Url
        /// </summary>
        public virtual string Url { get; set; }
    }
}

 

三、PermissionRequirement 令牌必要參數類

這裏邊存放的都是 Jwt Token 的所有信息,注意它繼承了 IAuthorizationRequirement,由於咱們要設計自定義受權驗證處理器,因此必須繼承驗證要求接口,才能設計咱們本身的參數:

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// 必要參數類,
    /// 繼承 IAuthorizationRequirement,用於設計自定義權限處理器PermissionHandler
    /// 由於AuthorizationHandler 中的泛型參數 TRequirement 必須繼承 IAuthorizationRequirement
    /// </summary>
    public class PermissionRequirement : IAuthorizationRequirement
    {
        /// <summary>
        /// 用戶權限集合
        /// </summary>
        public List<PermissionItem> Permissions { get; set; }
        /// <summary>
        /// 無權限action
        /// </summary>
        public string DeniedAction { get; set; }

        /// <summary>
        /// 認證受權類型
        /// </summary>
        public string ClaimType { internal get; set; }
        /// <summary>
        /// 請求路徑
        /// </summary>
        public string LoginPath { get; set; } = "/Api/Login";
        /// <summary>
        /// 發行人
        /// </summary>
        public string Issuer { get; set; }
        /// <summary>
        /// 訂閱人
        /// </summary>
        public string Audience { get; set; }
        /// <summary>
        /// 過時時間
        /// </summary>
        public TimeSpan Expiration { get; set; }
        /// <summary>
        /// 簽名驗證
        /// </summary>
        public SigningCredentials SigningCredentials { get; set; }


        /// <summary>
        /// 構造
        /// </summary>
        /// <param name="deniedAction">拒約請求的url</param>
        /// <param name="permissions">權限集合</param>
        /// <param name="claimType">聲明類型</param>
        /// <param name="issuer">發行人</param>
        /// <param name="audience">訂閱人</param>
        /// <param name="signingCredentials">簽名驗證明體</param>
        /// <param name="expiration">過時時間</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;
        }
    }
}

 

四、PermissionHandler 自定義受權處理器,核心!

 咱們先看代碼:

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// 權限受權處理器 繼承AuthorizationHandler ,而且須要一個權限必要參數 /// </summary>
    public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
        /// <summary>
        /// 驗證方案提供對象
        /// </summary>
        public IAuthenticationSchemeProvider Schemes { get; set; }

        /// <summary>
        /// services 層注入
        /// </summary>
        public IRoleModulePermissionServices _roleModulePermissionServices { get; set; }

        /// <summary>
        /// 構造函數注入
        /// </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緩存,只是減小與數據庫的訪問次數,而又保證是最新的數據
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轉成HttpContext,以便取出表頭信息 var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //請求Url var questUrl = httpContext.Request.Path.Value.ToLower(); //判斷請求是否中止 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; } } //判斷請求是否擁有憑據,即有沒有登陸 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; //權限中是否存在請求的url if (requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key?.ToLower() == questUrl).Count() > 0) { // 獲取當前用戶的角色信息 var currentUserRoles = (from item in httpContext.User.Claims where item.Type == requirement.ClaimType select item.Value).ToList(); //驗證權限 if (currentUserRoles.Count <= 0 || requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role) && w.Url.ToLower() == questUrl).Count() <= 0) { context.Fail(); return; // 能夠在這裏設置跳轉頁面,不過仍是會訪問當前接口地址的 httpContext.Response.Redirect(requirement.DeniedAction); } } else { context.Fail(); return; } //判斷過時時間 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; } } //判斷沒有登陸時,是否訪問登陸的url,而且是Post請求,而且是form表單提交類型,不然爲失敗 if (!questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType)) { context.Fail(); return; } context.Succeed(requirement); } } }

 

基本的解釋上邊已經寫了,應該能看懂,這裏只有一點,就是咱們自定義的這個處理器,是繼承了AuthorizationHandler ,並且它還須要一個泛型類,而且該泛型類必須繼承IAuthorizationRequirement 這個受權要求的接口,這樣咱們就能夠很方便的把咱們的自定義的權限參數傳入受權處理器中。

 

好啦,到了這裏,咱們已經設計好了處理器,那如何配置在啓動服務中呢,請繼續看。

 

4、配置受權服務與使用

 這裏主要是在咱們的啓動類 Startup.cs 中的服務配置,其實和以前的差很少,只是作了簡單的封裝,你們必定都能看的懂:

一、將JWT密鑰等信息封裝到配置文件

在接口層的 appsettings.json 文件中,配置咱們的jwt令牌信息:

"Audience": {
    "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs",
    "Issuer": "Blog.Core",
    "Audience": "wr"
  }

 

二、修改JWT服務註冊方法

在啓動類 Startup.cs 中的服務方法ConfigureServices 中,修改咱們的JWT Token 服務註冊方法:

      #region JWT Token Service
            //讀取配置文件
            var audienceConfig = Configuration.GetSection("Audience");
            var symmetricKeyAsBase64 = audienceConfig["Secret"];
            var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
            var signingKey = new SymmetricSecurityKey(keyByteArray);

            // 令牌驗證參數,以前咱們都是寫在AddJwtBearer裏的,這裏提出來了
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,//驗證發行人的簽名密鑰
                IssuerSigningKey = signingKey,
                ValidateIssuer = true,//驗證發行人
                ValidIssuer = audienceConfig["Issuer"],//發行人
                ValidateAudience = true,//驗證訂閱人
                ValidAudience = audienceConfig["Audience"],//訂閱人
                ValidateLifetime = true,//驗證生命週期
                ClockSkew = TimeSpan.Zero,//這個是定義的過時的緩存時間
                RequireExpirationTime = true,//是否要求過時

            };
            var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);

            // 注意使用RESTful風格的接口會更好,由於只須要寫一個Url便可,好比:/api/values 表明了Get Post Put Delete等多個。
            // 若是想寫死,能夠直接在這裏寫。
            //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"},
            //              };

            // 若是要數據庫動態綁定,這裏先留個空,後邊處理器裏動態賦值
            var permission = new List<PermissionItem>();

            // 角色與接口的權限要求參數
            var permissionRequirement = new PermissionRequirement(
                "/api/denied",// 拒絕受權的跳轉地址(目前無用)
                permission,//這裏還記得麼,就是咱們上邊說到的角色地址信息憑據實體類 Permission
                ClaimTypes.Role,//基於角色的受權
                audienceConfig["Issuer"],//發行人
                audienceConfig["Audience"],//訂閱人
                signingCredentials,//簽名憑據
                expiration: TimeSpan.FromSeconds(60*2)//接口的過時時間,注意這裏沒有了緩衝時間,你也能夠自定義,在上邊的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"));

                // 自定義基於策略的受權權限
                options.AddPolicy("Permission",
                         policy => policy.Requirements.Add(permissionRequirement));
            })
 // ② 核心之二,必須要配置認證服務,這裏是jwtBearer默認認證,好比光有卡沒用,得能識別他們
            .AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
 // ③ 核心之三,針對JWT的配置,好比門禁是如何識別的,是放射卡,仍是磁卡
            .AddJwtBearer(o =>
            {
                o.TokenValidationParameters = tokenValidationParameters;
            });


            // 依賴注入,將自定義的受權處理器 匹配給官方受權處理器接口,這樣當系統處理受權的時候,就會直接訪問咱們自定義的受權處理器了。
            services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
       // 將受權必要類注入生命週期內 services.AddSingleton(permissionRequirement);
#endregion

 注意必定要配置這三個核心(.AddAuthorization、.AddAuthentication、.AddJwtBearer),不然會報錯:

三、在登陸接口中,賦值過時時間等信息 

 雖然咱們在 startup 中也設置了過時時間,可是咱們還須要在每個 token 的聲明列表中(claims)中,配置過時時間,只不過兩個時間同樣罷了。

 

四、在接口中很方便調用

這樣定義好之後,咱們只須要很方便的在每個controller上邊寫上 [Authorize("Permission")],這個驗證特性便可,這個名字就是咱們的策略名,咱們就不用再想哪個接口對應哪些Roles了,是否是更方便了!固然若是不寫這個特性的話,不會被限制,好比那些前臺的頁面接口,就不須要被限制。

 

 

五、使用效果展現

我們看看平時會遇到的4種狀況。

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

 

你也能夠直接使用個人在線地址 http://123.206.33.109:8081/swagger/index.html 來操做,具體的步驟見下面的這三個狀況。

 

接口沒有配置權限

這種狀況,不管是數據庫是否配置,都會很正常的經過HTTP請求,從而獲取到咱們的數據,就好比登陸頁:

 

接口設置了權限,可是數據庫沒有配置

我們以 ValuesController 爲例子

 

 如今咱們把API接口是 /api/values 的接口和角色關聯的表給邏輯刪除了,那這個時候,也就表明了,當前接口雖然設置了權限,可是在數據庫裏並無配置它與Role的關係:

 

那若是咱們訪問的話會是怎樣:

 

首先,咱們看到在獲取到的四個角色接口信息中,已經沒有了api/values 的相關信息,而後咱們去訪問該接口,就看到了報錯403,固然你也能夠自定義錯誤,就是在 PermissionHandler.cs 自定義權限受權處理程序裏,能夠本身擴展。

 

接口設置了權限,而且數據庫也配置了

仍是使用我們的 ValueController.cs ,這時候我們把剛剛邏輯刪除的改爲False:

 

 

而後看看咱們的執行過程:

 

 

發現咱們已經很成功的對接口進行了權限控制了,你能夠在後臺作成界面的形式,對其進行配置等,固然很豐富的了。這裏要說一下,若是你用的是RESTful風格的接口,配置 api/values 會把CURD四個權限所有賦過去,若是你想對某一個角色只有Read和Create(讀取和添加)的權限的話,你能夠這麼操做:

一、不用RESTful風格,直接每個接口都配置進去,好比這樣,api/values/Read、api/values/Create 等;

二、繼續使用RESTful風格接口,可是須要在(角色和API地址的)數據庫表中,再增長一個 ActionName 這樣相似的字段,對接口進行區分限制就行,具體的,我會在下一個系列說到;

 

最後通過了兩分鐘,令牌過時:

 

好啦,這些簡單的受權功能已經夠我們使用了,還能在數據庫裏動態配置,不是麼?

 

5、思考

到這裏,我們的這個項目已經徹底能實現權限的動態分配了,固然這裏是沒有後臺界面的,你能夠本身創建一個MVC項目來實驗,也能夠創建一個Vue管理後臺來分配,都是很簡單的,我我的感受已經很完美了,我們的項目基本也成型了。

可是這些都是我們本身造的輪子,那若是咱們用一直很高調的 IdentityServer4 這個已經成熟的輪子來實現接口級別的動態受權是怎麼作呢?

請看個人下一個系列吧(.NetCore API + IS4+EFCore+VueAdmin)~~~ 提早祝你們聖誕節快樂!

 

一、情景補充

有的小夥伴在研究或者使用這個方法的時候,出現了疑惑,主要是兩個問題:

一、我若是後臺修改權限了,想馬上或者 關閉瀏覽器下次打開的時候更新權限咋辦?

二、若是我Token的過時時間比較短,好比一天,那如何實現滑動更新,就是不會正在使用的時候,忽然去登陸頁?

我也想了想,大概有如下本身的想法,你們能夠參考一下,歡迎提出批評:

一、若是後臺管理員修改了某一我的的權限,我會把每個Token放到Redis緩存裏,而後主要是 Token 的值,還有過時時間,權限等,若是管理員修改了權限(這個時候Token就不能使用了,由於這個Token仍是以前的Roles權限),而後就會更新了數據庫的Role,還會把Redis裏的該Token信息給Delete掉,這樣用戶再訪問下一個頁面的時候,咱們先校驗Redis緩存裏是否有這個 Token 數據,若是有,還繼續往下走,若是沒有了,就返回401讓用戶從新登陸。可使用一箇中間件來處理當前Token是否有效。

 

二、上邊寫到了在net core api裏增長一箇中間件來判斷Token是否有效,那若是無效了或者是被管理員修改了權限,致使 Token 被禁掉之後,又不想讓用戶從新登陸怎麼辦呢,我就想的是在 Http.js 封裝請求方法中,寫一個,每次用戶訪問的以前,都判斷一下當前 Token 是否有效的JS方法,若是有效則繼續調用下一個接口,若是無效,這個時候就能夠在後臺從新生成一個 Token 並返回到前臺,保存到localstroage裏,繼續用新的 Token 調用下一個接口。

 

三、用上邊的方法,你會感受這樣每次都會多一次調用,會佔資源,你能夠天天執行一次,或者就是每次登陸的成功後,不只把 Token 存在本地,把過時時間也存下來,這樣每次請求前能夠判斷是否過時,若是過時了呢,就先調用從新獲取Token 的接口方法,而後再往下走。

 可能你會感受很麻煩,很荒唐,不過微信小程序就是這麼處理的,不信你能夠去研究下。

 

6、Github & Gitee

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

 

-- END

相關文章
相關標籤/搜索