介绍
看着网站的登录界面想从新做一个,又看到个人中心内容太少,干脆写一个用户模块,把登录和用户管理的功能优化一下。在写功能的过程使用了Permissions,之前在笔记中有过记录,不过重新整理一下,以前的都是个人笔记,只有自己看得懂,从新整理一下,时间长了也方便查找。
ABP 权限
权限控制(Authorization)是ABP一个核心功能。ABP 提供了一套基于声明式权限的灵活机制,开发者可以通过 PermissionDefinitionProvider 来集中管理权限定义,通过 [Authorize] 特性控制访问。官方文档,看完之后视乎不是我要的。还是自己写一个好。
官方文档:➤ https://abp.io/docs/latest/modules/permission-management
自定义权限
ABP是一个比较成熟的框架,对于权限肯定不需要从头写。在创建项目时,Contracts层会有一个Permissions文件夹,里面已经给出模板。如果没有这个文件夹,自己创建一个就行了。
就以我的 代码为例,需要创建一个AccountPermissionDefinitionProvider 类,名字自定义,只需要集成PermissionDefinitionProvider既可以。每个Group都可以使用AddPermission添加多个权限。
public class AccountPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup(AccountPermissions.GroupName, L("Permission:Account"));
myGroup.AddPermission(AccountPermissions.ManageLogin, L("Permission:ManageLogin"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<AccountResource>(name);
}
}
而AccountPermissions内容和简单,主要就是提供Key,在使用的位置就不需要输入字符串了,这是程序员的自我修养。
ABP 还提供了多语言支持,所以为了显示对应的文字,还要在语言文件中添加对应的文字。
做好上面几步,就可以在后台管理页面得到这样的效果了。
权限的过滤
添加好权限后,最主要的还是如何使用,目前我遇到的方式有三种:
第一种:
直接在类上面添加标签,默认是的类中的所有方法都会判断权限
[Authorize(AccountPermissions.ManageLogin)]
public class ThirdPartyAdminAppService : CrudAppService<ThirdParty, ...>, IThirdPartyAdminAppService
{
public ThirdPartyAdminAppService(IRepository<ThirdParty, Guid> repository) : base(repository)
{
}
}
第二种:
在构造函数中添加
public class ThirdPartyAdminAppService : CrudAppService<ThirdParty, ...>, IThirdPartyAdminAppService
{
public ThirdPartyAdminAppService(IRepository<ThirdParty, Guid> repository) : base(repository)
{
GetListPolicyName = AccountPermissions.ManageLogin;
CreatePolicyName = AccountPermissions.ManageLogin;
UpdatePolicyName = AccountPermissions.ManageLogin;
DeletePolicyName = AccountPermissions.ManageLogin;
}
}
第三种:
直接在类中指定方法上面添加
[Authorize(AccountPermissions.ManageLogin)]
public async Task EnableAsync(Guid id)
{
}
添加了权限之后,再次访问指定的类或者方法,有权限的则可以正常访问没有权限的则会报异常。网站是可以统一跳转错误提示页面的。
踩过的坑
上面的内容都是比较容易得,照着做就行了,但是问题在于,照着做了,却不一定生效。如果你写了一个类ThirdPartyAdminAppService和上面样集成CrudAppService,再加了权限限制,那基本着权限判断会出现有的生效有的不生效,即使没有权限的人也能访问。我第一此遇到的这个问题时,也是一头雾水,问过AI,AI乱回答一些,参照AI的做法基本不解决问题,也许AI辅助编程目前的问题就是在遇到框架问题上,很难有好的回答。
下面就先给出答案:
方法类型 | 是否会被 ABP 权限拦截器拦截 |
---|---|
public virtual |
✅ 会拦截 |
public override |
❌ 不会拦截 |
public 非virtual |
❌ 不会拦截 |
private / static 方法 |
❌ 不会拦截 |
通常我们为了方便会直接继承ABP 服务类CrudAppService
这样可以减少很多重复代码,但这样一来,很多方法就需要override
,有一些方法我们又是我们自己写,这就导致了加了override
的方法不能被拦截,而我们自己写的方法又可以正常拦截。对于不了解的开发人员,这个问题,这是会让人头大。
原理分析
遇到这个问题的原因是,底层框架的限制,即使在override 的方法加 [Authorize] 特性也会失效。这是Castle DynamicProxy(ABP 使用的 AOP 框架)天然的限制。
Castle 动态代理在运行时会生成一个代理类,它只能拦截 virtual 方法。如果你重写了一个基类方法(用 override),它拦截的其实是基类的方法,而不是子类的。Castle 是通过 创建子类并重写虚方法来注入拦截器 的,但你写了 override 后,相当于 ABP 的代理子类没有办法再重写这个方法了(因为你已经占用了这个重写机会)。
为什么 public async Task<LoginBackgroundDto> CreateAsync(string url)
这样写的方法能被拦截,而我没有写 virtual?看起来没有写 virtual,但实际上编译器在生成代码时,会自动把没有标记为 override 的方法视为 virtual。像下面这样调试一下就能看到方法的属性了。
解决方法
在开发过程中,我们不可以避免使用框架提供能的基类,这样可以节省很多重复工作,那要如果避免上面的问题呢?
使用使用第二种写法:
public class ThirdPartyAdminAppService : CrudAppService<ThirdParty, ...>, IThirdPartyAdminAppService
{
public ThirdPartyAdminAppService(IRepository<ThirdParty, Guid> repository) : base(repository)
{
GetListPolicyName = AccountPermissions.ManageLogin;
CreatePolicyName = AccountPermissions.ManageLogin;
UpdatePolicyName = AccountPermissions.ManageLogin;
DeletePolicyName = AccountPermissions.ManageLogin;
}
}
这种写法,会在基类方法执行时判断权限,看一下GetListAsync源码就知道,在执行前会先 CheckGetListPolicyAsync
判断GetListPolicyName
权限,其他方法也是这样。这样权限过滤就会生效了。
public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input)
{
await CheckGetListPolicyAsync();
var query = await CreateFilteredQueryAsync(input);
var totalCount = await AsyncExecuter.CountAsync(query);
var entities = new List<TEntity>();
var entityDtos = new List<TGetListOutputDto>();
if (totalCount > 0)
{
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
entities = await AsyncExecuter.ToListAsync(query);
entityDtos = await MapToGetListOutputDtosAsync(entities);
}
return new PagedResultDto<TGetListOutputDto>(
totalCount,
entityDtos
);
}
protected virtual async Task CheckGetListPolicyAsync()
{
await CheckPolicyAsync(GetListPolicyName);
}