从零开始开发PasteDocument(贴代码PasteForm框架实战序列)(4)-密钥授权

113 阅读7分钟

上一篇中搭建了贴代码文档的基础,由于贴代码文档的需求是允许大家接入,也就是有多组织的概念(其实可以用多租户模式开发,这个看后续要不要改一版版本出来) 这里就涉及一些权限的问题,如何快速的知道当前登陆用户的所属组织? 这就要求我们不得不修改下系统的鉴权机制了!

旧的鉴权逻辑

打开当前项目的PasteDocument.Handler/public/PublicHelper.cs有如下代码

        /// <summary>
        /// 为登陆创建密钥
        /// </summary>
        /// <param name="userid"></param>
        /// <param name="grades">使用|分割角色代码,示例role|user</param>
        /// <returns></returns>
        public static string TokenBuild(this int userid, string grades)
        {
            var nowtime = DateTime.Now.ToUnixTimeSeconds();
            // timestamp_userid_grades_temptoken
            var temptoken = $"{nowtime}_{userid}_|{grades}|_{PublicString.TokenKey}".ToMd5Lower();
            //Console.WriteLine("{0}-{1}-{2}-{3}", nowtime, userid, $"|{grades}|",temptoken);
            return $"{nowtime}_{userid}_|{grades}|_{temptoken}";
        }

        /// <summary>
        /// 检查密钥是否合法
        /// </summary>
        /// <param name="utoken"></param>
        /// <returns></returns>
        public static bool TokenCheck(this string utoken)
        {
            if (!String.IsNullOrEmpty(utoken))
            {
                var strs = utoken.Split('_');
                if (strs.Length == 4)
                {
                    if ($"{strs[0]}_{strs[1]}_{strs[2]}_{PublicString.TokenKey}".ToMd5Lower() == strs[3])
                    {
                        return true;
                    }
                }
            }
            return false;
        }

看上代码,当前的密钥格式为tiemstamp_userid_gradecodes_randomcode 看不懂? 我们可以看下PasteDocument.Application/ProjectAttribute.cs中的授权过滤器

    /// <summary>
    /// 基于IAuthorizationFilter
    /// </summary>
    public class RoleAttribute : IAuthorizationFilter
    {

        /// <summary>
        /// 权限
        /// </summary>
        private string _role { set; get; }

        /// <summary>
        /// 角色
        /// </summary>
        private string _model { get; set; }

        /// <summary>
        /// 角色 单个
        /// </summary>
        private string _grade { get; set; }

        /// <summary>
        /// 
        /// </summary>
        private readonly UserHelper _userHelper;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cache"></param>
        /// <param name="model">模块</param>
        /// <param name="role">权限</param>
        /// <param name="grade">角色</param>
        public RoleAttribute(
            UserHelper cache,
            string model = "",
            string role = "", string grade = "")
        {
            _userHelper = cache;
            _model = model;
            _role = role;
            _grade = grade;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var needauth = true;
            foreach (var item in context.Filters)
            {
                if (item is RoleAttribute)
                {
                    needauth = (item == this);
                }
            }

            if (needauth)
            {

                var _action = String.Empty;
                var _controller = String.Empty;

                foreach (var ii in context.RouteData.Values)
                {
                    // action
                    // controller 可以基于这个进行动态权限校验!
                    if (ii.Key == "action")
                    {
                        _action = ii.Value.ToString();
                    }
                    if (ii.Key == "controller")
                    {
                        _controller = ii.Value.ToString();
                    }
                }

                if (String.IsNullOrEmpty(_model))
                {
                    _model = _controller;
                }
                if (String.IsNullOrEmpty(_role))
                {
                    _role = _action;
                }
                //root-root 所有权限
                //xxx-root 有这个xxx的controller的所有权限
                //角色 模块 权限 灵活授权方式 整个系统中角色不能过多,或者说一个账号不能授权过多角色
                //Console.WriteLine("PathBase:{0},Path:{1}",context.HttpContext.Request.PathBase,context.HttpContext.Request.Path);
                if (context.HttpContext.Request.Headers[PublicString.TokenHeadName].Count == 0)
                {
                    var info = new ExceptionModel();
                    info.code = "401";
                    info.success = false;
                    info.message = "当前密钥信息有误,请登陆后重试!";
                    context.Result = new ContentResult
                    {
                        StatusCode = 401,
                        ContentType = "application/json;charset=utf-8",
                        Content = Newtonsoft.Json.JsonConvert.SerializeObject(info)
                    };
                    return;
                }
                var token = context.HttpContext.Request.Headers[PublicString.TokenHeadName].ToString();
                if (token.StartsWith("Bearer"))
                {
                    token = token.Replace("Bearer ", "");
                }
                //这里可以做token的安全校验
                if (!String.IsNullOrEmpty(token) && token.TokenCheck())
                {
                    //这里可以修改下,如果model和role都是空的,则可以不执行TokenCheck 这样可以基于controller和action做游客可访问的配置
                    //这里可以做一些判断,比如get不进行redis二次校验等
                    var back = _userHelper.NewHasRole(_model, _role, _grade, token);
                    if (!back.role)
                    {
                        //throw new PasteDocumentException($"{(back.code == 401 ? "当前登录密钥失效,请重新登录" : "没有当前接口的操作权限,请确认")}", $"{back.code}");
                        var info = new ExceptionModel();
                        info.code = $"{back.code}";
                        info.success = false;
                        info.message = $"权限不足,需要权限{_model}-{_role}!";
                        context.Result = new ContentResult
                        {
                            StatusCode = back.code,
                            ContentType = "application/json;charset=utf-8",
                            Content = Newtonsoft.Json.JsonConvert.SerializeObject(info)
                        };
                    }
                }
                else
                {
                    var info = new ExceptionModel();
                    info.code = "401";
                    info.success = false;
                    info.message = "当前密钥信息有误,请登陆后重试!";
                    context.Result = new ContentResult
                    {
                        StatusCode = 401,
                        ContentType = "application/json;charset=utf-8",
                        Content = Newtonsoft.Json.JsonConvert.SerializeObject(info)
                    };
                }
            }
        }
    }


从上面的代码可以看到这个鉴权机制是非常灵活的,支持model-role,支持gradecode,支持controller-action的3个模式,或者说混合模式

以上的逻辑可以满足几乎的系统权限要求,配置也很灵活,如果你要使用controller-action模式,则需要修改下代码 然后关键点在于权限不需要预先埋入(或者说埋入的时候配置model,role为空) 然后就可以在权限表去新增对应的controller-aciton了 如果有特殊的需求,你还可以做反向配置,就是默认有权限,配置的表示禁止当前权限!!!

在需要的地方如下标注特性即可

    /// <summary>
    /// 权限列表
    ///</summary>
    [TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "data", "view" })]
    public class RoleInfoAppService : PasteDocumentAppService
    {
    	//其他代码
    }

或者

        /// <summary>
        /// 读取AddDto的数据模型
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "root", "root" })]
        public PasteBuilderHelper.VoloModelInfo ReadAddModel()
        {
			//其他代码
        }

以上是当前的鉴权逻辑和规则,但是在当前项目中不太适用,所以我们要改造他!!!

新鉴权需求

1.保持整体逻辑,不要大改,因为当前的鉴权模式最够的小巧,也足够的灵活 比如你可以配置所有的get请求,只要验证密钥是否合法即可,不需要检查缓存中是否还存活 2.能够从鉴权字符串中知道当前处于哪个组织中,结合篇1的介绍,用户的权限维度是按照组织划分的,下面可以细分到项目 3.为啥鉴权字符串不添加项目的信息? 考虑到一个组织往往有多个项目,而实际需要权限的地方一般是编辑修改等,至于查询权限在读取的时候可以一并查询到 而组织作为一个租户模式,有切换组织这个动作,切换后表示用户当前的身份是某一个组织内的 4.保持角色代码这个信息,因为有些地方可以使用到

改造一.权限绑定表

在原来的UserGrade表修改后,如下

    /// <summary>
    /// 用户绑定角色 解绑的直接删除数据
    /// </summary>
    public class UserGrade : Entity<int>
    {
        /// <summary>
        /// 用户ID
        /// </summary>
        public int UserId { get; set; }

        /// <summary>
        /// 角色ID
        /// </summary>
        public int GradeId { get; set; }

        /// <summary>
        /// 组织
        /// </summary>
        public int CompanyId { get; set; }

        /// <summary>
        /// 项目
        /// </summary>
        public int ProjectId { get; set; }

    }

添加了字段CompanyId和ProjectId,4个字段组合唯一,如果你拥有组织1的ProjectId=0和ProjectId=5的权限,在涉及项目ProjectId=5的项目的时候,读取的是组织1的ProjectId=5的权限,如果这个时候你需要读取ProjectId=3的权限,由于表中只有0和5,则会向下兼容,也就是读取ProjectId=0的角色对应的权限!

对上操作后,对应的就是如果给用户绑定角色了! 1.在项目列表中,选定某一个角色后,为某一个用户授权!授权的是目标用户的组织内这个项目的权限 2.在用户列表页直接为某一个用户授权,授权的是当前组织内的ProjectId=0的权限

注意对应的UserGrade的Dto也要进行修改

using PasteDocument.Application.Contracts;
using PasteDocument.markmodels;
using PasteFormHelper;
using Volo.Abp.Application.Dtos;

namespace PasteDocument.usermodels
{
    ///<summary>
    ///用户绑定角色 解绑的直接删除数据
    ///</summary>
    public class UserGradeAddDto
    {
        ///<summary>
        ///用户ID
        ///</summary>
        [ColumnDataType("outer", "userInfo", "extendUser", "id", "userName")]
        public int UserId { get; set; }

        ///<summary>
        ///角色ID
        ///</summary>
        [ColumnDataType("outer", "gradeInfo", "extendGrade", "id", "name")]
        public int GradeId { get; set; }

        /// <summary>
        /// 项目
        /// </summary>
        [PasteOuter("projectInfo", "extendProject", "id", "name")]
        public int ProjectId { get; set; }

    }

    ///<summary>
    ///用户角色 用户绑定角色,或者用户绑定项目的角色
    ///</summary>
    public class UserGradeUpdateDto : UserGradeAddDto
    {
        /// <summary>
        /// ID
        /// </summary>
        [PasteHidden]
        public int Id { get; set; }

        /// <summary>
        /// 用户
        /// </summary>
        [ColumnDataType("hidden")]
        public UserShortModel ExtendUser { get; set; }

        /// <summary>
        /// 角色
        /// </summary>
        [ColumnDataType("hidden")]
        public GradeShortModel ExtendGrade { get; set; }

        /// <summary>
        /// 项目
        /// </summary>
        [ColumnDataType("hidden")]
        public ProjectShort ExtendProject { get; set; }

    }

    ///<summary>
    ///用户绑定角色 解绑的直接删除数据
    ///</summary>
    public class UserGradeDto : EntityDto<int>
    {
        ///<summary>
        ///用户ID
        ///</summary>
        public int UserId { get; set; }

        ///<summary>
        ///角色ID
        ///</summary>
        public int GradeId { get; set; }

    }

    ///<summary>
    ///用户绑定角色 解绑的直接删除数据
    ///</summary>
    public class UserGradeListDto : EntityDto<int>
    {
        ///<summary>
        ///用户ID
        ///</summary>
        public int UserId { get; set; }

        /// <summary>
        /// 用户
        /// </summary>
        [PasteOuterDisplay("extendUser?.userName || ''")]
        public UserShortModel ExtendUser { get; set; }

        ///<summary>
        ///角色ID
        ///</summary>
        public int GradeId { get; set; }

        /// <summary>
        /// 角色
        /// </summary>
        [PasteOuterDisplay("extendGrade?.name || ''")]
        public GradeShortModel ExtendGrade { get; set; }

        /// <summary>
        /// 项目ID
        /// </summary>
        public int ProjectId { get; set; }

        /// <summary>
        /// 项目
        /// </summary>
        [PasteOuterDisplay("extendProject?.name || ''")]
        public ProjectShort ExtendProject { get; set; }
    }

    ///<summary>
    /// 查询
    ///</summary>
    public class InputQueryUserGrade : InputSearchBase
    {

    }
}

改造二·密钥串生成

修改密钥生成的规则,如下

        /// <summary>
        /// 创建密钥
        /// </summary>
        /// <param name="userid"></param>
        /// <param name="companyid">当前率属于组织</param>
        /// <param name="gradecode">当前角色代码</param>
        /// <returns></returns>
        public static string TokenBuild(this int userid,int companyid, string gradecode)
        {
            // timestamp_userid_companyid_gradecode_temptoken
            var nowtime = DateTime.Now.ToUnixTimeSeconds();
            var temptoken = $"{nowtime}_{userid}_{companyid}_{gradecode}_{PublicString.TokenKey}".ToMd5Lower();
            return $"{nowtime}_{userid}_{companyid}_{gradecode}_{temptoken}";
        }

        /// <summary>
        /// 检查密钥是否合法
        /// </summary>
        /// <param name="utoken"></param>
        /// <returns></returns>
        public static bool TokenCheck(this string utoken)
        {
            if (!String.IsNullOrEmpty(utoken))
            {
                var strs = utoken.Split('_');
                //if (strs.Length == 4)
                //{
                //    if ($"{strs[0]}_{strs[1]}_{strs[2]}_{PublicString.TokenKey}".ToMd5Lower() == strs[3])
                //    {
                //        return true;
                //    }
                //}
                if (strs.Length == 5)
                {
                    if ($"{strs[0]}_{strs[1]}_{strs[2]}_{strs[3]}_{PublicString.TokenKey}".ToMd5Lower() == strs[4])
                    {
                        return true;
                    }
                }
            }
            return false;
        }

看上面代码,关键点在于PublicString.TokenKey,部署项目的时候把这个信息修改成你自己的,这样就可以做到密钥不一样了,至于要不要定期修改,我觉得没那个必要,毕竟这个项目本身就是半公开的

改造三·获取当前登陆的用户ID,组织ID等

        /// <summary>
        /// 获取登录用户的ID
        /// </summary>
        /// <returns></returns>
        protected int ReadCurrentUserIdThrwoException()
        {

            var token = ReadToken();
            if (!string.IsNullOrEmpty(token))
            {
                if (token.TokenCheck() && token.Contains('_'))
                {
                    //这里一般需要添加密钥校验
                    int.TryParse(token.Split('_')[1], out var userid);
                    return userid;
                }
                else
                {
                    throw new PasteCodeException("密钥信息有误,请重新登陆", 401);
                }
            }
            else
            {
                throw new PasteCodeException("密钥信息有误,请重新登陆", 401);
            }
        }

        /// <summary>
        /// 获取当前登陆的组织
        /// </summary>
        /// <returns></returns>
        protected int CurrentCompanyId()
        {

            var token = ReadToken();
            if (!string.IsNullOrEmpty(token))
            {
                if (token.TokenCheck() && token.Contains('_'))
                {
                    //这里一般需要添加密钥校验
                    int.TryParse(token.Split('_')[2], out var companyid);
                    return companyid;
                }
            }
            return 0;
        }

        /// <summary>
        /// 获取当前登陆的组织
        /// </summary>
        /// <returns></returns>
        protected int CurrentCompanyIdThrwoException()
        {

            var token = ReadToken();
            if (!string.IsNullOrEmpty(token))
            {
                if (token.TokenCheck() && token.Contains('_'))
                {
                    //这里一般需要添加密钥校验
                    int.TryParse(token.Split('_')[2], out var companyid);
                    return companyid;
                }
                else
                {
                    throw new PasteCodeException("密钥信息有误,请重新登陆", 401);
                }
            }
            else
            {
                throw new PasteCodeException("密钥信息有误,请重新登陆", 401);
            }
        }

改造四·修改获取密钥的方式

由于本系统的页面采用的是非分离方式(用户端,非管理端),所以密钥由原来的Header中获取修改成cookie和header双兼容模式获取,其中cookie的权限最高(因为这个东西服务端可以直接配置,浏览器会配合设置新的值)

        /// <summary>
        /// 读取密钥
        /// </summary>
        /// <returns></returns>
        private string ReadToken()
        {
            var token = "";
            if (_httpContext.Request.Cookies.ContainsKey(PublicString.TokenHeadName))
            {
                token = _httpContext.Request.Cookies[PublicString.TokenHeadName];
            }
            if (string.IsNullOrEmpty(token))
            {
                if (_httpContext.Request.Headers.ContainsKey(PublicString.TokenHeadName))
                {
                    token = _httpContext.Request.Headers[PublicString.TokenHeadName].ToString();
                }
            }
            return token;
        }

大体的本次的改造就算完成,下一篇的内容,就是和当前选定组织有关,然后是Dto中的对应字段的完善相关

下期见