授权详细设计
使用场景
提供接口供其他系统使用,用于判断在某些情况下的某些人(或者系统)是否有权限调用当前函数。
名词字典
场景:业务规划的场景内容(比如用户、消息)
操作:业务规划的场景内操作(比如用户的增删改查、消息的增删改查的各个函数)
请求对象:希望调用当前函数的对象(可以是自然人:张三、李四;可以是角色:帖子的拥有者)
角色:在授权系统中充当中间数据载体的数据(操作可以赋予角色权利,请求对象可以与角色关联,角色可以继承角色)
角色类型:个人、角色、群组(个人:直接给单个人建立的角色,系统自动维护的;角色:管理员创建的角色,可以进行继承;群组:为组织关系自动创建的角色;)
授权状态:拒绝、授权、未设置
角色继承:一个角色可以继承其他几个角色,间接获取他们的授权。(如果继承自多个角色则合并他们的授权)
合并授权:多个授权项之间合并(合并规则为拒绝>授权>未设置)
覆盖授权:当前角色可以不遵从父级授权的结果,直接设置授权状态
数据字典
根据设计实现的简单说明可以将数据库简单设计到如下状态,不过这里有部分数据设计的可以简化,实现表现层内容的时候遇到了比较难实现的问题,当然最后还是实现了,不过实现的过程异常痛苦。
因为本项目是作为基础项目,其他的所有项目的基础授权模块,所以本项目并没有集成到任何项目中,而是通过UDP开放端口的方式来提供相关的调用。
租户/应用对象(Tenant)
Id:
Name:名称
DisplayName:显示名称
Password:密码
OriginalToken:原始Token
Status:状态(是否禁用)
ParentId:父对象Id
角色(Role)
Id:
Name:名称
RoleType:角色类型(个人、角色、群组)
TenantId:租户Id
角色继承(RoleMapping)
Id:
RoleId:角色Id
ParentId:父对象Id
TenantId:租户Id
请求对象(RequestObject)
Id:
RequestId:请求Id(一般为自然人Id或者场景角色Id)
TenantId:租户Id(应用Id)
DisplayName:显示名称
请求角色映射表(RequestRoleMapping)
Id:
RequestObjectId:请求对象Id
AuthorizationRoleId:授权角色Id
TenantId:租户Id
响应对象(ResponseObject)
Id:
Name:名称
DisplayName:显示名称
TenantId:租户对象
响应角色映射表(ResponseRoleMapping)
Id:
ResponseObjectId:响应对象Id
RoleId:角色Id
TenantId:租户Id
实现思路
基础原则
- 本项目是所有项目的基础服务项目,所以本项目采用多租户(/应用)的模式,租户与租户之间的数据相互隔离,互不干涉,不允许出现,夸租户间的角色授权。
- 请求对象与响应对象之间,通过角色隔离开,通过角色的方式来间接授权。如果业务上需要将授权直接设置给个人,则应该建立一个个人角色,然后将角色与个人进行数据关联。
基础数据设置
租户数据
租户、应用是数据的模拟分块,中间的数据不会造成相互影响。可以简单理解为数据模块的各自的沙盒模块。
请求
为了适应实际的场景而创建的对应数据模块。因为兼容各个子系统的数据,所以使用了租户Id与请求Id共同确定对象,请求Id可以是子系统中合适的数据(自然人Id或者其他方便的Id)。
角色
是实现授权模块的重要模块中间中间数据载体。是一种身份的象征,可以代指个人,也可以代指某种可以继承的角色,也可以是团队身份的象征。
角色继承
因为将授权项设置的非常庞大的时候,那么这里边的授权也会变得比较复杂。某些授权可能需要被继承来的比较好。至于为什么是这样我后面会提到。
响应
需要被验证授权的内容部分。必须说某一个消息,比如说某一个帖子等等
请求与角色的映射
因为授权实际上是授到了角色身上所以,我们验证授权的时候,也是验证了角色的授权,至于你有没有权限,那么就理所当然的被认为是你是否存在这个角色,或者说你所拥有的所有角色最后权限合并究竟是一个什么结果。请求与角色的映射关系就是表示,你究竟拥有那些角色关系的数据结构。
响应与角色的映射
前面已经提到了,授权实际上是响应的对象给某一个角色添加了某些权限项的具体授权。实际上这句话表面上看是对的,不过有一个细节需要强调一下,实际上响应对象并不是直接给角色进行了授权,而是给响应对象与角色的映射关系进行了授权。说出来有点绕。但是现在的实现是这样,不过也可以在授权项里边直接绑定响应对象Id跟授权对象Id来表示这层关系,不过为了减少数据量这个地方多出来一张表。不过我觉得可能前面说的这种方式可能更容易实现一些。
概述
来简单总结一下授权的过程是怎样的。其实授权的过程就是请求与角色进行绑定。响应与角色绑定,并且在响应与角色的绑定上添加某些具体授权项的过程。计算具体授权结果则是,通过请求获取到全部的角色,然后将角色对于某一响应计算出所有的授权结果,然后获得合适的授权结果的过程。
权限合并
先解释一下什么叫做权限合并,就是讲多个平级角色的权限合并的过程。那么什么叫做平级角色?实际上这个是抽象的概念,如果我同时拥有两个角色,我改怎么处理?比如说,你跟我是同事,也是我的朋友,如果我希望我的朋友能够看到我的朋友圈。那么这个时候你会有两个角色,一个是同事,一个是朋友。这两个身份就是平级的角色。还有其他的情况,比如说,一个角色继承自多个角色,那么在他这一级,他直接继承的父角色都是平级。横跨多层的角色也是一层一层的数据累加下来的。
提到了权限合并就不得不提权限里的一条比较重要的原则,叫做拒绝优先。意思就是说,如果我有两个角色,一个是拒绝,一个是通过,那么我的权限就是拒绝的。
角色继承的必要性
其实没有角色继承这个服务依然可以实现,那么角色继承存在的意义究竟是什么。我来举个例子简单说明一下。比如说一篇博客设置权限是这样的,比如登录用户可以查看,一级用户可以点赞,二级用户可以回复,主人可以编辑删除。那么问题来了一级用户不只是可以点赞吧,因为他是登录用户,所以他也拥有登录用户的权限,二级用户可以回复也可以查看。主人呢可以编辑可以删除可以查看。这其中是有继承关系的,当然如果说没有继承关系也可以实现,因为你登录,我自然是可以给你添加一个登录用户的,你自然而然的拥有了查看的权限。但是这样处理会多出来很多权限的映射。如果一级、二级、主人都是继承自登录用户的话,你的权限映射则会少很多。再举一个例子,比如你们有两个部门,这两个部门存在竞争关系,他们相互之间不允许对方的人查看自己的数据。但是现在出现了一个很尴尬的问题,比如你是这两个部门的领导,那么根据权限合并的拒绝优先原则,你实际上是看不到AB两部门的数据。但是业务上你应该是可以看到数据的。那么现在怎么办,应该是创建一个跨部门领导给你,然后将这个角色的数据设置为两边都可以看到,覆盖掉原本的拒绝设置。
核心代码
设置相关的代码都不重要,随便搞,是那么个意思就行了。这个项目里边最核心的代码就是获取所有用户,然后组建继承树,然后通过合并继承权限的方式计算请求对于响应的所有授权项的代码。
/// <summary>
/// 获取请求对象对应具体场景的所有授权状态
/// </summary>
/// <param name="tenantid"></param>
/// <param name="scene"></param>
/// <param name="requestid"></param>
/// <returns></returns>
public override Dictionary<string, AuthorizationStatus> GetAuthorizationStatus(long tenantid, string scene, long requestid)
{
var rpo = _ResponseObjectService.GetAppropriate(tenantid, scene);
if (rpo == null)
return null;
var r_id = _RequestObjectService.GetFirstOrDefaultId(new ListFilter { { "TenantId", tenantid }, { "RequestId", requestid } });
// 请求对象与角色相对应的关系
var rqo_role_mapping = _RequestRoleMappingService.GetListBySingleFilter("RequestObjectId", r_id);
// 响应对象与角色对应的关系
var rpo_role_mapping = _ResponseRoleMappingService.GetListBySingleFilter("ResponseObjectId", rpo.Id);
TreeNode root_tree = new TreeNode();
Dictionary<long, TreeNode> tree_mapping = new Dictionary<long, TreeNode>();
TreeNode TryGetTreeNode(long id)
{
if (tree_mapping.ContainsKey(id))
return tree_mapping[id];
var node = new TreeNode();
node.Id = id;
tree_mapping[id] = node;
return node;
}
// 组装树形结构
foreach (var item in rqo_role_mapping)
{
var all_parent = _RoleMappingService.GetAllParentId(item.AuthorizationRoleId);
foreach (var pitem in all_parent)
{
var temp_child = TryGetTreeNode(pitem.RoleId);
var temp_parent = TryGetTreeNode(pitem.ParentId);
if (!temp_child.ParentNode.Contains(temp_parent))
temp_child.ParentNode.Add(temp_parent);
}
root_tree.ParentNode.Add(TryGetTreeNode(item.AuthorizationRoleId));
}
// 组装授权说明
foreach (var item in tree_mapping)
{
var ai_list = _AuthorizationItemService.GetAllByResponseAndRole(rpo.Id, item.Value.Id);
foreach (var ai_item in ai_list)
{
var start_status = GetAppropriateStatus(item.Value.AuthorizationStatus, ai_item.Name);
item.Value.AuthorizationStatus[ai_item.Name] = MergeAuthorizationStatus((AuthorizationStatus)ai_item.Status, start_status);
}
}
void InheritAuthorization(TreeNode node)
{
// 优先处理父级
if (node.ParentNode.Count > 0)
{
foreach (var item in node.ParentNode)
{
InheritAuthorization(item);
}
}
// 获取所有授权子项
List<string> all_keys = new List<string>();
foreach (var item in node.ParentNode)
{
all_keys.AddRange(item.AuthorizationStatus.Keys);
}
all_keys.Distinct();
// 合并所有父级授权
foreach (var item in all_keys)
{
if (node.AuthorizationStatus.ContainsKey(item) && node.AuthorizationStatus[item] != AuthorizationStatus.Undefined)
continue;
List<AuthorizationStatus> s_list = new List<AuthorizationStatus>();
foreach (var p_item in node.ParentNode)
{
s_list.Add(GetAppropriateStatus(p_item.AuthorizationStatus, item));
}
node.AuthorizationStatus[item] = MergeAuthorizationStatus(s_list.ToArray());
}
}
InheritAuthorization(root_tree);
return root_tree.AuthorizationStatus;
}