

Asp.net core IdentityServer4与传统基于角色的权限系统的集成
source link: https://www.cnblogs.com/xiaxiaolu/p/15929063.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Asp.net core IdentityServer4与传统基于角色的权限系统的集成
因为最近在忙别的,好久没水文了 今天来水一篇;
在学习或者做权限系统技术选型的过程中,经常有朋友有这样的疑问 :
“IdentityServer4的能不能做到与传统基于角色的权限系统集成呢?”
“我的公司有几百个接口,IdentityServer4能不能做到关联用户,给这些用户授予不同的接口的权限呢?”
我的回答是:是的,可以!
同时,我还想补充下,IdentityServer4是给我们的授权流程/需求提供一个新的 标准化的选择,而不是限制你的需求;它是一个基础的框架,你可以根据你的需求自定义成任意你要的样子。
OK,下面开始说说我的实现思路,不一定最优只为抛砖引玉。
先准备好两个WebApi 项目,分别有两个接口
Hei.UserApi:6001
GetUsername: https://localhost:6001/api/profile/getusername
GetScore: https://localhost:6001/api/Credit/GetScore //用户信用分要求高,期望管理员才可以调用
Hei.OrderApi:6002
GetOrderNo:https://localhost:6002/api/Order/GetOrderNo
GetAddress: https://localhost:6002/api/Delivery/GetAddress //用户地址敏感,期望管理员才可以调用
实现请看源码
准备好两个角色:
R01 管理员
R02 普通用户
准备好两个用户
Bob: subid=1001,普通用户
Alice: subid=1002,管理员
实际用户有多个角色的,本文为了简化问题,一个用户只允许一种角色
角色对应的权限
管理员:可以调用 Hei.UserApi
和Hei.OrderApi
的所有接口;
普通用户:只可以调用 Hei.UserApi
->GetUsername,和Hei.OrderApi
->GetOrderNo;
先来看晓晨大佬画的 access_token 验证交互过程图:
可以看到,Token在首次被服务端验证后,后续的验证都在客户端验证的,本文的重点就在这里,需要判断token有没有权限,重写这部分即可;
1、生成自定义token
1、 IdentityServer4 服务端重写IResourceOwnerPasswordValidator
和 IProfileService
两个接口生成携带有自定义信息的access_token
public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public CustomResourceOwnerPasswordValidator()
{
}
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
if (!string.IsNullOrEmpty(context.UserName) && !string.IsNullOrEmpty(context.Password))
{
var loginUser = UserService.Users.First(c => c.Username == context.UserName && c.Password == context.Password);
if (loginUser != null)
{
context.Result = new GrantValidationResult(loginUser.SubjectId, OidcConstants.AuthenticationMethods.Password, new Claim[]{new Claim("my_phone","10086")}); //这里增加自定义信息
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
StartUp.cs 启用
builder.AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>();
builder.AddProfileService<CustomProfileService>();
2、请求一个token来看看:
可以看到我这里token携带有了自定义信息 my_phone
,同样的,你可以把角色id直接放这里,或者直接跟用户的subid关联(本demo就是);
1、自定义授权标签CustomRBACAuthorize
public class CustomRBACAuthorizeAttribute : AuthorizeAttribute
{
public CustomRBACAuthorizeAttribute(string policyName="")
{
this.PolicyName = policyName;
}
public string PolicyName
{
get
{
return PolicyName;
}
set
{
Policy = $"{Const.PolicyCombineIdentityServer4ExternalRBAC}{value.ToString()}";
}
}
}
后面接口打这个标签就表示使用基于自定义的与权限校验
2、自定义授权 IAuthorizationRequirement
public class CustomRBACRequirement: IAuthorizationRequirement
{
public string PolicyName { get; }
public CustomRBACRequirement(string policyName)
{
this.PolicyName = policyName;
}
}
3、自定义IAuthorizationPolicyProvider
public class CustomRBACPolicyProvider : IAuthorizationPolicyProvider
{
private readonly IConfiguration _configuration;
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public CustomRBACPolicyProvider(IConfiguration configuration, IOptions<AuthorizationOptions> options)
{
_configuration = configuration;
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return FallbackPolicyProvider.GetDefaultPolicyAsync();
}
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
{
return Task.FromResult<AuthorizationPolicy>(null);
}
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(Const.PolicyCombineIdentityServer4ExternalRBAC, StringComparison.OrdinalIgnoreCase))
{
var policys = new AuthorizationPolicyBuilder();
//这里使用自定义Requirement
policys.AddRequirements(new CustomRBACRequirement(policyName.Replace(Const.PolicyCombineIdentityServer4ExternalRBAC,"")));
return Task.FromResult(policys.Build());
}
return Task.FromResult<AuthorizationPolicy>(null);
}
}
4、自定义Requirement的的 AuthorizationHandler
/// <summary>
/// 处理CustomRBACRequirement的逻辑
/// </summary>
/// <param name="context"></param>
/// <param name="requirement"></param>
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRBACRequirement requirement)
{
var subid = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var routeData = _httpContextAccessor.HttpContext?.GetRouteData();
var curentAction = routeData?.Values["action"]?.ToString();
var curentController = routeData?.Values["controller"]?.ToString();
//入口程序集,用来标识某个api
var apiName = Assembly.GetEntryAssembly().GetName().Name;
if (string.IsNullOrWhiteSpace(subid) == false && string.IsNullOrWhiteSpace(curentAction) == false && string.IsNullOrWhiteSpace(curentController) == false)
{
//核心就在这里了,查出用户subid对应的角色权限,然后做处理判断有没有当前接口的权限
//我这里是demo就简单的模拟下,真实的权限数据应该都是写数据库或接口的
var userPermission = PermissionService.GetUserPermissionBySubid(apiName, subid);
if (userPermission != null && userPermission.Authorised.ContainsKey(curentController))
{
var authActions = userPermission.Authorised[curentController];
//这里判断当前用户的角色有当前action/controllers的权限
//(真实的权限划分由你自己定义,比如你划分了只读接口,只写接口、特殊权限接口、内部接口等,在管理后台上分组,打标签/标记然后授予角色就行)
if (authActions?.Any(action => action == curentAction) == true)
{
context.Succeed(requirement);
}
}
}
return Task.CompletedTask;
}
jwt 的token本来是去中心化的,现在这样一来,每次请求进来都去调接口验证可以说是违背了去中心化的思想,所以保证性能问题得自己解决;
public class PermissionService
{
/// <summary>
/// 权限信息(实际上这些应该存在数据库)
/// </summary>
public static List<PermissionEntity> Permissions = new List<PermissionEntity>
{
//RoleId R01 是管理员,有两个Api的多个接口的权限
new PermissionEntity{ PermissionId="0001",RoleId="R01", ApiName="Hei.UserApi",Authorised=new Dictionary<string, List<string>>
{
{ "Profile",new List<string>{ "GetUsername"}},
{ "Credit",new List<string>{ "GetScore"}},
}
},
new PermissionEntity{ PermissionId="0002",RoleId="R01", ApiName="Hei.OrderApi",Authorised=new Dictionary<string, List<string>>
{
{ "Delivery",new List<string>{ "GetAddress"}},
{ "Order",new List<string>{ "GetOrderNo"}},
}
},
//RoleId R02 是普通员工,有两个Api的多个 部分 接口的权限
new PermissionEntity{ PermissionId="0001",RoleId="R02", ApiName="Hei.UserApi",Authorised=new Dictionary<string, List<string>>
{
{ "Profile",new List<string>{ "GetUsername"}},
//{ "Credit",new List<string>{ "GetScore"}}, //用户信用分接口权限就不给普通员工了
}
},
new PermissionEntity{ PermissionId="0002",RoleId="R02", ApiName="Hei.OrderApi",Authorised=new Dictionary<string, List<string>>
{
//{ "Delivery",new List<string>{ "GetAddress"}}, //用户地址信息也是
{ "Order",new List<string>{ "GetOrderNo"}},
}
}
};
当然这些数据一般都是根据你的权限需求存数据库的,与你的权限管理后台相配合;
5、注册自定义授权处理程序
/// <summary>
/// 提交自定义角色的授权策略
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddCustomRBACAuthorizationPolicy(this IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthorizationPolicyProvider, CustomRBACPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, CustomRBACRequirementHandler>();
return services;
}
6、在接口上使用自定义授权标签CustomRBACAuthorize
[Route("api/[controller]/[action]")]
[ApiController]
public class CreditController : ControllerBase
{
/// <summary>
/// 获取信用分
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[CustomRBACAuthorize] //这里就表名
public int GetScore(string id)
{
return 666;
}
}
7、测试结果
管理员1001 角色id R01 Alice
可以看到都是 200
普通用户1002 角色id R02 Bob
可以看到获取用户信用积分、订单投递地址的接口403了,与我们全面的设定相符;
就是一个简单的思路
1、给access_token 带上自定义信息;
2、在客户端重写本地验证/权限校验逻辑即可;
其实token黑白名单,token撤销原理类似 希望能帮上一点小忙;
IdentityServer4就是一个工具,希望大家不要给它设定太多的限制“不能做这个,不能做那个等等”
https://github.com/gebiWangshushu/cnblogs-demos/tree/dev/IdentityServerWithRBAC.Example
如果能有个小星星那就再好不过了(✧◡✧)
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK