net core WebApi——April.Util更新之許可權

来源:https://www.cnblogs.com/AprilBlank/archive/2019/11/10/11832324.html

[toc] 前言 在之前已經提到過,公用類庫Util已經開源,目的一是為了簡化開發的工作量,畢竟有些常規的功能類庫重覆率還是挺高的,二是為了一起探討學習軟體開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善, 倉庫地址: "April.Util_github" , "April.Util_ ...


前言

在之前已經提到過,公用類庫Util已經開源,目的一是為了簡化開發的工作量,畢竟有些常規的功能類庫重覆率還是挺高的,二是為了一起探討學習軟體開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善,倉庫地址: April.Util_githubApril.Util_gitee,還沒關註的朋友希望可以先mark,後續會持續維護。

許可權

在之前的net core WebApi——公用庫April.Util公開及發佈中已經介紹了初次發佈的一些功能,其中包括緩存,日誌,加密,統一的配置等等,具體可以再回頭看下這篇介紹,而在其中有個TokenUtil,因為當時發佈的時候這塊兒還沒有更新上,趁著周末來整理下吧。

關於webapi的許可權,可以藉助Identity,Jwt,但是我這裡沒有藉助這些,只是自己做了個token的生成已經存儲用戶主要信息,對於許可權我想大多數人已經有了一套自己的許可權體系,所以這裡我簡單介紹下我的思路。

  1. 首先對於菜單做許可權標示,請求的控制器,請求的事件
  2. 菜單信息維護後,設置角色對應多個菜單
  3. 管理員對應多個角色
  4. 在登錄的時候根據賬號信息獲取對應管理員的角色及最終菜單,控制器,事件
  5. 處理管理員信息後自定義token,可設置token過期時間,token可以反解析(如果到期自動重新授權,我這裡沒有處理)
  6. 每次訪問介面的時候(除公開不需校驗的介面),根據請求的路徑判斷是否有當前控制器許可權(通過中間層),進入介面後判斷是否有對應許可權(通過標簽)

通過上述流程來做許可權的校驗,當然這裡只是針對單應用,如果是多應用的話,這裡還要考慮應用問題(如,一個授權認證工程主做身份校驗,多個應用工程通用一個管理)。

首先,我們需要一個可以存儲管理員的對應屬性集合AdminEntity,主要存儲基本信息,控制器集合,許可權集合,數據集合(也就是企業部門等)。

    /// <summary>
    /// 管理員實體
    /// </summary>
    public class AdminEntity
    {
        private int _ID = -1;
        private string _UserName = string.Empty;
        private string _Avator = string.Empty;
        private List<string> _Controllers = new List<string>();
        private List<string> _Permissions = new List<string>();
        private int _TokenType = 0;
        private bool _IsSuperManager = false;
        private List<int> _Depts = new List<int>();
        private int _CurrentDept = -1;
        private DateTime _ExpireTime = DateTime.Now;

        /// <summary>
        /// 主鍵
        /// </summary>
        public int ID { get => _ID; set => _ID = value; }
        /// <summary>
        /// 用戶名
        /// </summary>
        public string UserName { get => _UserName; set => _UserName = value; }
        /// <summary>
        /// 頭像
        /// </summary>
        public string Avator { get => _Avator; set => _Avator = value; }
        /// <summary>
        /// 控制器集合
        /// </summary>
        public List<string> Controllers { get => _Controllers; set => _Controllers = value; }
        /// <summary>
        /// 許可權集合
        /// </summary>
        public List<string> Permissions { get => _Permissions; set => _Permissions = value; }
        /// <summary>
        /// 訪問方式
        /// </summary>
        public int TokenType { get => _TokenType; set => _TokenType = value; }
        /// <summary>
        /// 是否為超管
        /// </summary>
        public bool IsSuperManager { get => _IsSuperManager; set => _IsSuperManager = value; }
        /// <summary>
        /// 企業集合
        /// </summary>
        public List<int> Depts { get => _Depts; set => _Depts = value; }
        /// <summary>
        /// 當前企業
        /// </summary>
        public int CurrentDept { get => _CurrentDept; set => _CurrentDept = value; }
        /// <summary>
        /// 過期時間
        /// </summary>
        public DateTime ExpireTime { get => _ExpireTime; set => _ExpireTime = value; }
    }

之後我們來完成TokenUtil這塊兒,首先是生成我們的token串,因為考慮到需要反解析,所以這裡採用的是字元串加解密,當然這個加密串具體是什麼可以自定義,目前我這裡設置的是固定需要兩個參數{id},{ts},目的是為了保證加密串的唯一,當然也是為了過期無感知重新授權準備的。

    public class TokenUtil
    {
        /// <summary>
        /// 設置token
        /// </summary>
        /// <returns></returns>
        public static string GetToken(AdminEntity user, out string expiretimstamp)
        {
            string id = user.ID.ToString();
            double exp = 0;
            switch ((AprilEnums.TokenType)user.TokenType)
            {
                case AprilEnums.TokenType.Web:
                    exp = AprilConfig.WebExpire;
                    break;
                case AprilEnums.TokenType.App:
                    exp = AprilConfig.AppExpire;
                    break;
                case AprilEnums.TokenType.MiniProgram:
                    exp = AprilConfig.MiniProgramExpire;
                    break;
                case AprilEnums.TokenType.Other:
                    exp = AprilConfig.OtherExpire;
                    break;
            }
            DateTime date = DateTime.Now.AddHours(exp);
            user.ExpireTime = date;
            double timestamp = DateUtil.ConvertToUnixTimestamp(date);
            expiretimstamp = timestamp.ToString();
            string token = AprilConfig.TokenSecretFormat.Replace("{id}", id).Replace("{ts}", expiretimstamp);
            token = EncryptUtil.EncryptDES(token, EncryptUtil.SecurityKey);
            //LogUtil.Debug($"用戶{id}獲取token:{token}");
            Add(token, user);
            //處理多點登錄
            SetUserToken(token, user.ID);
            return token;
        }

        /// <summary>
        /// 通過token獲取當前人員信息
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public static AdminEntity GetUserByToken(string token = "")
        {
            if (string.IsNullOrEmpty(token))
            {
                token = GetTokenByContent();
            }
            if (!string.IsNullOrEmpty(token))
            {
                
                AdminEntity admin = Get(token);
                if (admin != null)
                {
                    //校驗時間
                    if (admin.ExpireTime > DateTime.Now)
                    {
                        if (AprilConfig.AllowSliding)
                        {
                            //延長時間
                            admin.ExpireTime = DateTime.Now.AddMinutes(30);
                            //更新
                            Add(token, admin);
                        }
                        return admin;
                    }
                    else
                    {
                        //已經過期的就不再延長了,當然後續根據情況改進吧
                        return null;
                    }
                }
            }
            return null;
        }
        /// <summary>
        /// 通過用戶請求信息獲取Token信息
        /// </summary>
        /// <returns></returns>
        public static string GetTokenByContent()
        {
            string token = "";
            //判斷header
            var headers = AprilConfig.HttpCurrent.Request.Headers;
            if (headers.ContainsKey("token"))
            {
                token = headers["token"].ToString();
            }
            if (string.IsNullOrEmpty(token))
            {
                token = CookieUtil.GetString("token");
            }
            if (string.IsNullOrEmpty(token))
            {
                AprilConfig.HttpCurrent.Request.Query.TryGetValue("token", out StringValues temptoken);
                if (temptoken != StringValues.Empty)
                {
                    token = temptoken.ToString();
                }
            }
            return token;
        }
        /// <summary>
        /// 移除Token
        /// </summary>
        /// <param name="token"></param>
        public static void RemoveToken(string token = "")
        {
            if (string.IsNullOrEmpty(token))
            {
                token = GetTokenByContent();
            }
            if (!string.IsNullOrEmpty(token))
            {
                Remove(token);
            }
        }

        #region 多個登錄
        /// <summary>
        /// 多個登錄設置緩存
        /// </summary>
        /// <param name="token"></param>
        /// <param name="userid"></param>
        public static void SetUserToken(string token, int userid)
        {
            Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken");
            if (dicusers == null)
            {
                dicusers = new Dictionary<int, List<string>>();
            }
            List<string> listtokens = new List<string>();
            if (dicusers.ContainsKey(userid))
            {
                listtokens = dicusers[userid];
                if (listtokens.Count <= 0)
                {
                    listtokens.Add(token);
                }
                else
                {
                    if (!AprilConfig.AllowMuiltiLogin)
                    {
                        foreach (var item in listtokens)
                        {
                            RemoveToken(item);
                        }
                        listtokens.Add(token);
                    }
                    else
                    {
                        bool isAdd = true;
                        foreach (var item in listtokens)
                        {
                            if (item == token)
                            {
                                isAdd = false;
                            }
                        }
                        if (isAdd)
                        {
                            listtokens.Add(token);
                        }
                    }
                }
            }
            else
            {

                listtokens.Add(token);
                dicusers.Add(userid, listtokens);
            }
            CacheUtil.Add("UserToken", dicusers, new TimeSpan(6, 0, 0), true);
        }
        /// <summary>
        /// 多個登錄刪除緩存
        /// </summary>
        /// <param name="userid"></param>
        public static void RemoveUserToken(int userid)
        {
            Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken");
            if (dicusers != null && dicusers.Count > 0)
            {
                if (dicusers.ContainsKey(userid))
                {
                    //刪除所有token
                    var listtokens = dicusers[userid];
                    foreach (var token in listtokens)
                    {
                        RemoveToken(token);
                    }
                    dicusers.Remove(userid);
                }
            }
        }
        /// <summary>
        /// 多個登錄獲取
        /// </summary>
        /// <param name="userid"></param>
        /// <returns></returns>
        public static List<string> GetUserToken(int userid)
        {
            Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken");
            List<string> lists = new List<string>();
            if (dicusers != null && dicusers.Count > 0)
            {
                foreach (var item in dicusers)
                {
                    if (item.Key == userid)
                    {
                        lists = dicusers[userid];
                        break;
                    }
                }
            }
            return lists;
        }
        #endregion

        #region 私有方法(這塊兒還需要改進)

        private static void Add(string token,AdminEntity admin)
        {
            switch (AprilConfig.TokenCacheType)
            {
                //不推薦Cookie
                case AprilEnums.TokenCacheType.Cookie:
                    CookieUtil.Add(token, admin);
                    break;
                case AprilEnums.TokenCacheType.Cache:
                    CacheUtil.Add(token, admin, new TimeSpan(0, 30, 0));
                    break;
                case AprilEnums.TokenCacheType.Session:
                    SessionUtil.Add(token, admin);
                    break;
                case AprilEnums.TokenCacheType.Redis:
                    RedisUtil.Add(token, admin);
                    break;
            }
        }

        private static AdminEntity Get(string token)
        {
            AdminEntity admin = null;
            switch (AprilConfig.TokenCacheType)
            {
                case AprilEnums.TokenCacheType.Cookie:
                    admin = CookieUtil.Get<AdminEntity>(token);
                    break;
                case AprilEnums.TokenCacheType.Cache:
                    admin = CacheUtil.Get<AdminEntity>(token);
                    break;
                case AprilEnums.TokenCacheType.Session:
                    admin = SessionUtil.Get<AdminEntity>(token);
                    break;
                case AprilEnums.TokenCacheType.Redis:
                    admin = RedisUtil.Get<AdminEntity>(token);
                    break;
            }
            return admin;
        }

        private static void Remove(string token)
        {
            switch (AprilConfig.TokenCacheType)
            {
                case AprilEnums.TokenCacheType.Cookie:
                    CookieUtil.Remove(token);
                    break;
                case AprilEnums.TokenCacheType.Cache:
                    CacheUtil.Remove(token);
                    break;
                case AprilEnums.TokenCacheType.Session:
                    SessionUtil.Remove(token);
                    break;
                case AprilEnums.TokenCacheType.Redis:
                    RedisUtil.Remove(token);
                    break;
            }
        }
        #endregion
    }

中間層

當然這也在之前已經提到過net core Webapi基礎工程搭建(七)——小試AOP及常規測試_Part 1,當時還覺得這個叫做攔截器,too young too simple,至於使用方法這裡就不多說了,可以參考之前2.2版本的東西,也可以看代碼倉庫中的示例工程。

    public class AprilAuthorizationMiddleware
    {
        private readonly RequestDelegate next;

        public AprilAuthorizationMiddleware(RequestDelegate next)
        {
            this.next = next;
        }

        public Task Invoke(HttpContext context)
        {
            if (context.Request.Method != "OPTIONS")
            {
                string path = context.Request.Path.Value;
                if (!AprilConfig.AllowUrl.Contains(path))
                {
                    //獲取管理員信息
                    AdminEntity admin = TokenUtil.GetUserByToken();
                    if (admin == null)
                    {
                        //重新登錄
                        return ResponseUtil.HandleResponse(-2, "未登錄");
                    }
                    if (!admin.IsSuperManager)
                    {
                        //格式統一為/api/Controller/Action,相容多級如/api/Controller1/ConrolerInnerName/xxx/Action
                        string[] strValues = System.Text.RegularExpressions.Regex.Split(path, "/");

                        string controller = "";
                        bool isStartApi = false;
                        if (path.StartsWith("/api"))
                        {
                            isStartApi = true;
                        }
                        for (int i = 0; i < strValues.Length; i++)
                        {
                            //為空,為api,或者最後一個
                            if (string.IsNullOrEmpty(strValues[i]) || i == strValues.Length - 1)
                            {
                                continue;
                            }
                            if (isStartApi && strValues[i] == "api")
                            {
                                continue;
                            }
                            if (!string.IsNullOrEmpty(controller))
                            {
                                controller += "/";
                            }
                            controller += strValues[i];
                        }
                        if (string.IsNullOrEmpty(controller))
                        {
                            controller = strValues[strValues.Length - 1];
                        }
                        if (!admin.Controllers.Contains(controller.ToLower()))
                        {
                            //無權訪問
                            return ResponseUtil.HandleResponse(401, "無權訪問");
                        }

                    }
                }
            }
            return next.Invoke(context);
        }
    }

Ok,我們先來看下Login中的操作以及實現效果吧。

        [HttpPost]
        public async Task<ResponseDataEntity> Login(LoginFormEntity formEntity)
        {
            if (string.IsNullOrEmpty(formEntity.LoginName) || string.IsNullOrEmpty(formEntity.Password))
            {
                return ResponseUtil.Fail("請輸入賬號密碼");
            }
            if (formEntity.LoginName == "admin")
            {
                //這裡實際應該通過db獲取管理員
                string password = EncryptUtil.MD5Encrypt(formEntity.Password, AprilConfig.SecurityKey);
                if (password == "B092956160CB0018")
                {
                    //獲取管理員相關許可權,同樣是db獲取,這裡只做展示
                    AdminEntity admin = new AdminEntity
                    {
                        UserName = "超級管理員",
                        Avator = "",
                        IsSuperManager = true,
                        TokenType = (int)AprilEnums.TokenType.Web
                    };
                    string token = TokenUtil.GetToken(admin, out string expiretimestamp);
                    int expiretime = 0;
                    int.TryParse(expiretimestamp, out expiretime);
                    //可以考慮記錄登錄日誌等其他信息
                    return ResponseUtil.Success("", new { username = admin.UserName, avator = admin.Avator, token = token, expire = expiretime });
                }
            }
            else if (formEntity.LoginName == "test")
            {
                //這裡做許可權演示
                AdminEntity admin = new AdminEntity
                {
                    UserName = "測試",
                    Avator = "",
                    TokenType = (int)AprilEnums.TokenType.Web
                };
                admin.Controllers.Add("weatherforecast");
                admin.Permissions.Add("weatherforecast_log");//控制器_事件(Add,Update...)
                string token = TokenUtil.GetToken(admin, out string expiretimestamp);
                int expiretime = 0;
                int.TryParse(expiretimestamp, out expiretime);
                //可以考慮記錄登錄日誌等其他信息
                return ResponseUtil.Success("", new { username = admin.UserName, avator = admin.Avator, token = token, expire = expiretime });
            }
            //這裡其實已經可以考慮驗證碼相關了,但是這是示例工程,後續可持續關註我,會有基礎工程(帶許可權)的實例公開
            return ResponseUtil.Fail("賬號密碼錯誤");
        }

可能乍一看會先吐槽下,明明是非同步介面還用同步的方法,沒有非同步的實現空浪費記憶體xxx,因為db考慮是要搞非同步,所以這裡示例就這樣先寫了,主要是領會精神,咳咳。

來試下效果吧,首先我們隨便訪問個白名單外的介面。

測試

然後我們通過賬號登陸Login介面(直接寫死了,admin,123456),獲取到token。
登錄

然後我們來訪問介面。
測試

是不是還是未登錄,沒錯,因為沒有token的傳值,當然我這裡是通過query傳值,支持header,token,query。

測試

這裡因為是超管,所以許可權隨意搞,無所謂,接下來展示下普通用戶的許可權標示。

目前可以通過標簽AprilPermission,把當前的控制器與對應事件的許可權作為參數傳遞,之後根據當前管理員信息做校驗。

    public class AprilPermissionAttribute : Attribute, IActionFilter
    {

        public string Permission;
        public string Controller;
        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="_controller">控制器</param>
        /// <param name="_permission">介面事件</param>
        public AprilPermissionAttribute(string _controller, string _permission)
        {
            Permission = _permission;
            Controller = _controller;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            LogUtil.Debug("AprilPermission OnActionExecuted");
        }
        public void OnActionExecuting(ActionExecutingContext context)
        {
            AdminEntity admin = TokenUtil.GetUserByToken();
            if (admin == null || admin.ExpireTime <= DateTime.Now)
            {
                context.Result = new ObjectResult(new { msg = "未登錄", code = -2 });
            }
            if (!admin.IsSuperManager)
            {
                string controller_permission = $"{Controller}_{Permission}";
                if (!admin.Controllers.Contains(Controller) || !admin.Permissions.Contains(controller_permission))
                {
                    context.Result = new ObjectResult(new { msg = "無權訪問", code = 401 });
                }
            }
        }
    }

針對幾個介面做了調整,附上標簽後判斷許可權,我們來測試下登錄test,密碼隨意。

測試
測試
測試
測試

至此許可權相關的功能也統一起來,當然如果有個性化的還是需要調整的,後續也是會不斷的更新改動。

小結

許可權還是稍微麻煩點兒啊,通過中間層,標簽以及TokenUtil來完成登錄授權這塊兒,至於數據的劃分,畢竟這個東西不是通用的,所以只是點出來而沒有去整合,如果有好的建議或者自己整合的通用類庫也可以跟我交流。


您的分享是我們最大的動力!

更多相關文章
  • Eclipse部署多模塊項目到tomcat,啟動時找不到jar的解決方法。 ...
  • 在使用redis時,一般會設置一個過期時間,當然也有不設置過期時間的,也就是永久不過期。當設置了過期時間,redis是如何判斷是否過期,以及根據什麼策略來進行刪除的。 設置過期時間 expire key time(以秒為單位) 這是最常用的方式setex(String key, int second ...
  • scp是secure copy的簡寫,用於在Linux下進行遠程拷貝文件的命令,和它類似的命令有cp,不過cp只是在本機進行拷貝不能跨伺服器,而且scp傳輸是加密的。可能會稍微影響一下速度。當你伺服器硬碟變為只讀 read only system時,用scp可以幫你把文件移出來。另外,scp還非常不 ...
  • 你可能想創建一個在應用的任何地方都可以訪問的函數,這個教程將幫你實現 👏 很多教程都會說,你在 composer.json 這個文件中通過添加一個自動載入的文件,就可以實現這個需求。但我認為這不是一個好的方式,當你在 helpers.php 文件中添加了更多的函數時,可讀性將變得很差。 下麵我將介 ...
  • 在說正題之前先解釋一下交換機模式是個籠統的稱呼,它不是一個單獨的模式(包括了訂閱模式,路由模式和主題模式),交換機模式是一個比較常用的模式,主要是為了實現數據的同步。 首先,說一下訂閱模式,就和字面上的意思差不多主要就是一個生產者,多個消費者,同一個消息被多個消費者獲取,先看一下官網的圖示 整體執行 ...
  • 當創建隊列jobs、監聽器或訂閱伺服器以推送到隊列中時,您可能會開始認為,一旦分派,隊列工作器決定如何處理您的邏輯就完全由您自己決定了。 嗯……並不是說你不能從作業內部與隊列工作器交互,但是通常情況下,哪怕你做了,也是沒必要的。 這個神奇的騷操作的出現是因為“InteractsWithQueue”這 ...
  • 環境:MacOS 10.13 MAMAP Prophp 7.0.33 + xdebugVisual Studio Code前言我所理解的 POP Chain:利用魔術方法並巧妙構造特殊屬性調用一系列函數或類方法以執行某種敏感操作的調用堆棧反序列化常用魔法函數前言我所理解的 POP Chain:利用魔 ...
  • 【ASP.NET Core學習】在ASP.NET Core 種使用Entity Framework Core介紹,包括如何添加Entity Framwork Core,創建模型和遷移到資料庫,查詢數據,保存數據,使用事務,處理併發衝突 ...
一周排行
  • 前言 上一篇文章介紹IOptions的註冊,本章我們繼續往下看 IOptions IOptions是一個介面裡面只有一個Values屬性,該介面通過OptionsManager實現 OptionsManager OptionsManager實現了IOptions和IOptionsSnapshot,他 ...
  • 在 EF 里有個 `ShadowProperty` (陰影屬性/影子屬性)的概念,你可以通過 FluentAPI 的方式來定義一個不在 .NET model 里定義的屬性,只能通過 EF 里的 `Change Tracker` 來操作這種屬性。 在導出 Excel 的時候,可能希望導出的列並不... ...
  • 使用NPOI操作Excel,無需Office COM組件 部分代碼來自於:https://docs.microsoft.com/zh-tw/previous-versions/ee818993(v=msdn.10)?redirectedfrom=MSDN using System.Data; usi ...
  • Spire.Cloud.Word.Sdk提供了介面SetBackgroudColor()、SetBackgroudImage()、DeleteBackground()、GetBackgroudColor()用於設置、刪除及讀取Word文檔背景。本文將以C#程式為例演示如何來調用API介面實現以上內容 ...
  • 說明:在同一視窗打開鏈接,只要稍加改造就可以實現,這裡實現的是在新Tab頁打開鏈接,並且支持帶type="POST" target="_blank"的鏈接 github和bitbucket上相關問題: 1、WPF empty POST data when using custom popup htt ...
  • 前言 公司項目需要做個畫線縮放,我司稱之為瞳距縮放,簡而言之就是:2張圖,從第一張圖畫一條線,再從第二個圖畫一條線,第二條線以第一條為基準,延長到一致的長度,並同比縮放圖片;文字太枯燥,請先實例圖 例子1:以皮卡丘為例,我要把路飛的拳頭縮放到皮卡丘頭那麼大 例子2:以皮卡丘的基準,縮小路飛,與其身高 ...
  • 9月份的時候,微軟宣佈正式發佈C 8.0,作為.NET Core 3.0發行版的一部分。C 8.0的新特性之一就是預設介面實現。在本文中,我們將一起來聊聊預設介面實現。 作者:依樂祝 原文鏈接:https://www.cnblogs.com/yilezhu/p/12034584.html 提前說下: ...
  • 對於地圖坐標偏移,以leaflet為例,有如下解決辦法 方法1、修改leaflet源碼,解決地圖坐標偏移問題 方法2、將點位真實的經緯度經過偏移演算法,添加到加密的地圖上 方法3、直接對離線地圖瓦片進行糾偏 方法1需要修改源碼 方法2有缺陷,地圖依然是偏移的,如果把地圖經緯度顯示出來,經緯度也是不對的 ...
  • 引用類庫 1.Install-Package Microsoft.Extensions.Caching.Memory MemoryCacheOptions 緩存配置 1.ExpirationScanFrequency 獲取或設置對過期項的連續掃描之間的最短時間間隔 2.SizeLimit 緩存是沒有 ...
  • 原文:https://blogs.msdn.microsoft.com/mazhou/2017/12/12/c-7-series-part-7-ref-returns/ 背景 有兩種方法可以將一個值傳遞給一個方法: 例如,FCL(.NET Framework Class Library)中的Arra ...
x