上一篇中搭建了贴代码文档的基础,由于贴代码文档的需求是允许大家接入,也就是有多组织的概念(其实可以用多租户模式开发,这个看后续要不要改一版版本出来) 这里就涉及一些权限的问题,如何快速的知道当前登陆用户的所属组织? 这就要求我们不得不修改下系统的鉴权机制了!
旧的鉴权逻辑
打开当前项目的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中的对应字段的完善相关
下期见