• net core WebApi——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来完成登录授权这块儿,至于数据的划分,毕竟这个东西不是通用的,所以只是点出来而没有去整合,如果有好的建议或者自己整合的通用类库也可以跟我交流。

  • 相关阅读:
    Object detection overview
    CMU: A Baseline for 3D Multi-Object Tracking
    A Review of Visual Trackers and Analysis of its Application to Mobile Robot
    js回顾学习笔记
    写给.NET开发者的Python教程(二):基本类型和变量
    钽电容 Case B 和 MLCC 1210 区别
    linux sed命令就是这么简单
    shell 字符串处理汇总(查找,替换等等)
    linux shell 字符串操作详解 (长度,读取,替换,截取,连接,对比,删除,位置 )
    php 给定时间戳 加一月 如果下一月天数不够就返回下一月最后一天
  • 原文地址:https://www.cnblogs.com/AprilBlank/p/11832324.html
Copyright © 2020-2023  润新知