寫在前面
因為最近在忙別的,好久沒水文了 今天來水一篇;
在學習或者做許可權系統技術選型的過程中,經常有朋友有這樣的疑問 :
“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
如果能有個小星星那就再好不過了(✧◡✧)