net core WebApi——April.Util更新之權限

前言

在以前已經提到過,公用類庫Util已經開源,目的一是爲了簡化開發的工做量,畢竟有些常規的功能類庫重複率仍是挺高的,二是爲了一塊兒探討學習軟件開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善,倉庫地址: April.Util_githubApril.Util_gitee,還沒關注的朋友但願能夠先mark,後續會持續維護。git

權限

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

關於webapi的權限,能夠藉助Identity,Jwt,可是我這裏沒有藉助這些,只是本身作了個token的生成已經存儲用戶主要信息,對於權限我想大多數人已經有了一套本身的權限體系,因此這裏我簡單介紹下個人思路。web

  1. 首先對於菜單作權限標示,請求的控制器,請求的事件
  2. 菜單信息維護後,設置角色對應多個菜單
  3. 管理員對應多個角色
  4. 在登陸的時候根據帳號信息獲取對應管理員的角色及最終菜單,控制器,事件
  5. 處理管理員信息後自定義token,可設置token過時時間,token能夠反解析(若是到期自動從新受權,我這裏沒有處理)
  6. 每次訪問接口的時候(除公開不需校驗的接口),根據請求的路徑判斷是否有當前控制器權限(經過中間層),進入接口後判斷是否有對應權限(經過標籤)

經過上述流程來作權限的校驗,固然這裏只是針對單應用,若是是多應用的話,這裏還要考慮應用問題(如,一個受權認證工程主作身份校驗,多個應用工程通用一個管理)。api

首先,咱們須要一個能夠存儲管理員的對應屬性集合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版本的東西,也能夠看代碼倉庫中的示例工程。async

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來完成登陸受權這塊兒,至於數據的劃分,畢竟這個東西不是通用的,因此只是點出來而沒有去整合,若是有好的建議或者本身整合的通用類庫也能夠跟我交流。

相關文章
相關標籤/搜索