本篇随笔继续介绍ABP开发框架的权限控制管理内容,包括用户、角色、机构、权限等方面,以及该框架在Winform方面的应用集成。
1、ABP框架的权限控制管理内容
我们知道,权限管理一般都会涉及到用户、组织机构、角色,以及权限功能等方面的内容,ABP框架的基础内容也是涉及到这几方面的内容,其中它们之间的关系基本上是多对多的关系,它们的关系如下所示。
不过在官网下载的框架里面,包含权限管理这些应用服务层和展示层的内容并不完整,只是简单的包括了用户和角色的基础管理,而且很多权限管理所需要的基础功能并没有提供。
根据ABP框架提供的基础数据库表,我们可以进一步整理权限管理几个重要概念和真实数据库表之间的对应关系,基于这个基础上,我们可以完善整个权限管理模块内容。
上图是ABP基础框架中权限模块里面包含的一些主对象表和中间表,中间表主要用来存储两个对象之间的多对多关系,如角色包含多个用户,用户属于多个机构,机构包含多个角色等等。
2、基于ABP框架的权限管理模块
1)组织机构管理
组织机构主要就是一个层级的对象关系,一般包含但不限于公司、部门、工作组等的定义,其中组织机构包含用户成员和角色成员的关系,如下界面所示。
组织机构包含的成员可以添加多个人员记录,添加界面如下所示。
2)角色管理 --角色表(abproles)
角色信息没有层级关系,可以通过列表展示。
其中角色包含权限分配和角色成员的维护,如下是角色编辑界面,包含角色基本信息、权限、成员管理等。
角色的权限包含系统可以用的权限,并可以勾选为角色设置所需的功能点,如下界面所示。
用户成员则和机构的用户管理一样,可以指定多个用户。
3)用户管理 ----用户表(abpusers)
用户管理只需要管理用户基本的信息即可,我们如果需要分配角色可以在角色管理里面统一处理。当然,创建用户的时候,也可以ABP框架的收费版本界面一样,为用户指定角色和机构信息。
我这里主要是维护用户信息即可,用户列表界面如下所示。
用户编辑或者查看界面,除了可以看用户基础信息外,可以查看用户包所属的机构(多个),或者所属的角色(多个)
当然可以查看这个用户本身拥有的权限功能点,如下界面所示。
4)用户角色表(abpuserroles)
具体权限授权记录表(abppermissiongrants
5)权限功能
严格来说,ABP框架并没有统一管理好权限功能点的,它没有任何表来存储这个功能集合,而是通过派生AuthorizationProvider的子类来定义权限功能点,这种需要通过指定AuthorizationProvider的子类的方式创建功能点,需要每次系统模块增加功能点的时候,编码一下,然后增加自己的功能点,如下界面所示。
这种方式可能能够满足大多数的需要,不过我如果需要增量开发,或者动态增加某些功能点的时候,就有点不方便了。
我在这个基础上引入了一个权限功能的表用来存储功能点的,然后提供管理界面来动态维护这些功能点。如下界面所示。
这样我可以动态添加或者批量添加所需要的功能点,并且和整个权限管理模块串联起来,形成一个完整的控制体系。
这些概念主要还是来源于我的Winform开发框架和混合式开发框架里面的控制思路,以及界面展示的处理。
这样我们就可以管理自己的权限功能点,并可以为指定的角色配置相关的控制功能点,如下表所示是角色的权限集合(系统中间表),也就是给角色分配的功能点,依旧是在原来的系统表里面存储。
3、权限控制在业务模块界面中的使用
我们拥有了用户、角色、机构、权限功能以及它们之间的关系后,我们可以按照一个完善的权限系统来创建对应的用户角色权限关系,并通过在客户端对界面权限的判断和服务端对操作权限的判断,实现完整的控制处理。
服务端由ABP框架内置权限进行管理,通过在AppService里面定义好增删改查等权限点,如引用服务层的基类设置了几个权限点的属性。
我们在子类里面指定这些操作的变量即可,如产品应用服务中,我们可以定义CreatePermissionName为 Product/Add 这样的名称,当然也可以自定义。
然后每次在Action中调用相应的检查即可,如下是对创建的判断检查。
或者更新操作的权限检查
如果对于导入、导出等其他权限,我们则可以通过调用
void CheckPermission(string permissionName);
来进行自己自定义权限名称的判断。
在客户端,我们登录成功后,获取用户的权限集合,然后在客户端进行判断即可进行权限的控制管理,可以控制菜单、按钮等界面元素,如下是整合了权限控制的产品信息管理界面。
分页列表展示界面的控制代码如下所示。
1、ABP框架服务端和客户端的处理
本篇随笔介绍使用API调用类的封装类,进行函数的抽象,根据方法名称的推断,构建URL或者WebClient的请求类型,从而实现所有API调用函数的简化处理。
针对Web API接口调用的封装,为了适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。
而ABP的Web API调用类则需要对Web API接口调用进行封装,如下所示。
如对于字典模块的API封装类,它们继承一个相同的基类,然后实现特殊的自定义接口即可,这样可以减少常规的Create、Get、GetAll、Update、Delete等操作的代码,这些全部由调用基类进行处理,而只需要实现自定义的接口调用即可。
2、Web API调用类的简化处理
我们对于常规的Web API调用接口处理,如下代码所示。
public async virtual Task<AuthenticateResult> Authenticate(string username, string password)
{
var url = string.Format("{0}/api/TokenAuth/Authenticate", ServerRootAddress);
var input = new
{
UsernameOrEmailAddress = username,
Password = password
};
var result = await apiClient.PostAsync<AuthenticateResult>(url, input);
return result;
}
这种方法的处理,就需要自己拼接URL地址,以及传递相关的参数,一般情况下,我们的Web API Caller层类的函数和Web API控制器的方法是一一对应的,因此方法名称可以通过对当前接口名称的推断进行获得,如下所示。
public async Task<bool> ChangePassword(ChangePasswordDto input)
{
AddRequestHeaders();//加入认证的token头信息
string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
return await apiClient.PostAsync<bool>(url, input);
}
函数AddRequestHeaders 通过在调用前增加对应的AccessToken信息,然后URL通过当前方法的推断即可构建一个完整的URL,但是这个也仅仅是针对POST的方法,因为ABP框架根据方法的名称前缀的不同,而采用POST、GET、Delete、PUT等不同的HTTP处理操作。
如GET方法,则是需要使用GET请求
public async Task<List<RoleDto>> GetRolesByUser(EntityDto<long> input)
{
AddRequestHeaders();//加入认证的token头信息
string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
url = GetUrlParam(input, url);
var result = await apiClient.GetAsync<List<RoleDto>>(url);
return result;
}
而对于删除方法,则使用下面的DELETE请求,DELETE 和PUT操作,需要把参数串联成GET的URL形式,类似 url += string.Format(“?Id={0}”, id); 这样方式
public virtual async Task Delete(TDeleteInput input)
{
AddRequestHeaders();//加入认证的token头信息
string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
url += GetUrlParam(input, url);
var result = await apiClient.DeleteAsync(url);
return result;
}
对于更新的操作,使用了PUT方法
public async virtual Task<TEntityDto> Update(TUpdateInput input)
{
AddRequestHeaders();//加入认证的token头信息
string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
var result = await apiClient.PutAsync<TEntityDto>(url, input, null);
return result;
}
上面这些方法,我们根据规律,其实可以进一步进行简化,因为这些操作大多数类似的。
首先我们看到变化的地方,就是根据方法的前缀采用GET、POST、DELETE、PUT方法,还有就是URL串联字符串的不同,对于GET、Delete方法,参数使用的是组成URL方式,参数使用的是JSON提交内容方式。
根据这些变化,我们在基类提炼一个统一的处理方法DoActionAsync 来处理这些不同的操作。
/// <summary>
/// 根据方法名称自动执行GET/POST/PUT/DELETE请求方法
/// </summary>
/// <param name="method"></param>
/// <param name="input"></param>
protected virtual async Task DoActionAsync(MethodBase method, object input = null)
{
await DoActionAsync<object>(method, input);
}
/// <summary>
/// 根据方法名称自动执行GET/POST/PUT/DELETE请求方法
/// </summary>
/// <param name="method"></param>
/// <param name="input"></param>
protected virtual async Task<TResult> DoActionAsync<TResult>(MethodBase method, object input = null)
{
AddRequestHeaders();//加入认证的token头信息
string action = GetMethodName(method);
var url = string.Format("{0}/api/services/app/{1}/{2}", ServerRootAddress, DomainName, action);//获取访问API的地址(未包含参数)
var httpVerb = DynamicApiVerbHelper.GetConventionalVerbForMethodName(action);
if(httpVerb == HttpVerb.Get || httpVerb == HttpVerb.Delete)
{
if (input != null)
{
//Get和Delete的操作,需要组装URL参数
url = GetUrlParam(input, url);
}
}
int? timeout = null;
return await apiClient.DoActionAsync<TResult>(url, timeout, httpVerb.ToString().ToLower(), input);
}
这样,有了这两个函数的支持,我们可以简化很多操作代码了。
例如对于Update方法,简化的代码如下所示。
public async virtual Task<TEntityDto> Update(TUpdateInput input)
{
return await DoActionAsync<TEntityDto>(MethodBase.GetCurrentMethod(), input);
}
对于删除操作,简化的代码依旧也是一行代码
public virtual async Task Delete(TDeleteInput input)
{
await DoActionAsync(MethodBase.GetCurrentMethod(), input);
}
GET操作,也是一行代码
public async virtual Task<TEntityDto> Get(TGetInput input)
{
return await DoActionAsync<TEntityDto>(MethodBase.GetCurrentMethod(), input);
}
现在你看到,所有的客户端API封装类调用,都已经非常简化,大同小异了,主要就是交给基类函数进行推断调用处理即可。
如用户操作的APICaller类的代码如下所示。
这样我们再多的接口,都一行代码调用解决问题,非常简单,从此客户端封装类的实现就非常简单了,只需要注意有没有返回值即可,其他的都没有什么不同。
只需要注意的是,我们定义接口的时候,尽可能使用复杂类型对象,这样就可以根据对象属性名称和值进行构建URL或者JSON的了。
2、菜单模块的实现逻辑
为了开发菜单模块,我们需要先定义好菜单的存储数据表,定义菜单表和角色菜单的中间关系表如下所示。
这个菜单模块定位为Web和Winform都通用的,因此菜单表中增加多了一些字段信息。
这个生成的类,默认具有基类的增删改查分页等接口方法,同时我们也会生成对应的Web API Caller层的类代码,代码如下所示。
而界面显示的时候,加载并显示左侧树列表数据如下代码所示。
private async void FrmMenu_Load(object sender, EventArgs e)
{
//列表信息
InitTree();
InitSearchControl();
await BindTree();
}
删除菜单的时候,我们一般想把当前菜单和下面的子菜单一并级联删除,实现这个方法,我们需要在服务端自定义实现,如下是应用服务层的实现方法。
/// <summary>
/// 移除节点和子节点
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[UnitOfWork]
public virtual async Task DeleteWithSubNode(EntityDto<string> input)
{
var children = await _repository.GetAllListAsync(ou => ou.PID == input.Id);
foreach (var child in children)
{
await DeleteWithSubNode(new EntityDto<string>(child.Id));//递归删除
}
await _repository.DeleteAsync(input.Id);
}
我们这里显示声明了UnitOfWork标记,说明这个操作的原子性,全部成功就成功,否则失败的处理。
而客户端的Web API 封装调用类,对这个Web API接口的封装,根据上篇随笔《ABP开发框架前后端开发系列—(10)Web API调用类的简化处理》简化后的处理代码如下所示。
首先我们需要定义一个角色DTO对象中的菜单集合属性,如下所示。
在界面上获取勾选上的权限和菜单ID集合,存储在对应的列表里面。
/// <summary>
/// 编辑或者保存状态下取值函数
/// </summary>
/// <param name="info"></param>
private void SetInfo(RoleDto info)
{
info.DisplayName = txtDisplayName.Text;
info.Name = txtName.Text;
info.Description = txtDescription.Text;
info.Permissions = GetNodeValues(this.tree, "Name");
info.Menus = GetNodeValues(this.treeMenu, "Id");
}
在应用服务层的RoleAppService类里面,我们创建或者更新角色的时候,需要更新它的权限和菜单资源,如下代码是创建角色的函数。
/// <summary>
/// 创建角色对象
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override async Task<RoleDto> Create(CreateRoleDto input)
{
CheckCreatePermission();
var role = ObjectMapper.Map<Role>(input);
role.SetNormalizedName();
CheckErrors(await _roleManager.CreateAsync(role));
await CurrentUnitOfWork.SaveChangesAsync(); //It's done to get Id of the role.
await UpdateGrantedPermissions(role, input.Permissions);
await UpdateGrantedMenus(role, input.Menus);
return MapToEntityDto(role);
}
同理,更新角色一样处理这两个部分的资源
/// <summary>
/// 更新角色对象
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override async Task<RoleDto> Update(RoleDto input)
{
CheckUpdatePermission();
var role = await _roleManager.GetRoleByIdAsync(input.Id);
ObjectMapper.Map(input, role);
CheckErrors(await _roleManager.UpdateAsync(role));
await UpdateGrantedPermissions(role, input.Permissions);
await UpdateGrantedMenus(role, input.Menus);
return MapToEntityDto(role);
}
以上就是菜单的管理,和角色包含菜单的维护操作,整个开发过程主要就是使用代码生成工具来快速生成框架各个层的代码,以及Winform界面的代码,这样在进行一定的函数扩展以及界面布局调整,就可以非常方便、高效的完整一个业务模块的开发工作了。