3.通用許可權設計——SnailAspNetCoreFramework快速開發框架之後端設計
阿新 • • 發佈:2020-08-07
總體設計思路
在設計本專案的通用許可權前,我參閱過很多設計方案,最終定下RBAC(基於角色的許可權控制)。微軟本身是有一套預設的許可權控制的(asp.net core identity),但有如下幾個缺點
1、表結構固定,不好擴充套件。
2、不能動態的對介面進行角色的授權,只能寫在程式碼裡。所以本框架的設計會考慮如下幾點
- 不定義表結構,各許可權表的結構完全可由使用者自己定義,只需按規範實現介面即可
- 能動態分配介面的角色
具體設計摘要
- 許可權包含人員、角色、人員角色關係、資源、角色資源這5個表,各表不自定具體的結構,只通過介面進行約定
- 許可權核心邏輯由IPermission和IPermissionStore來定義。
- IPermission定義了使用者的登入、鑑權、初始化資源、從請求上下文識別出資源id、獲取所有資源及對應角色、登入密碼加密演算法等方法,預設實現為DefaultPermission->BasePermission->IPermission
- IPermissionStore定義各許可權相關資料的獲取、更新、快取重新整理方法。所有的許可權相關資料都在快取裡,當資料有變化時,要呼叫IPermissionStore的快取重新整理方法來進行。預設實現為DefaultPermissionStore->BasePermissionStore->IPermissionStore
- 用“基於策略”的方式進行鑑權。策略的實現邏輯為PermissionRequirementHandler,依賴於IPermission,通過IPermission.HasPermission方法來判斷是否有許可權。
- 支援cookies和jwt兩種方式。在登入時,由
- 通過在Action上加ResourceAttribute來定義哪些介面是需要進行鑑權的,並自動加入到Resource資料表裡
各表結構的介面約定
- 人員、角色、人員角色關係、資源、角色資源關係的介面如下
public interface IHasKeyAndName { /// <summary> /// 一般為id,主鍵 /// </summary> /// <returns></returns> string GetKey(); /// <summary> /// 一般為描述 /// </summary> /// <returns></returns> string GetName(); }
人員介面
public interface IUser:IHasKeyAndName
{
string GetAccount();
string GetPassword();
}
角色介面
public interface IRole:IHasKeyAndName
{
}
人員角色關係介面
public interface IUserRole
{
string GetUserKey();
string GetRoleKey();
}
資源介面
/// <summary>
/// 資源(指所有要許可權控制的資源,如介面,選單)
/// </summary>
public interface IResource:IHasKeyAndName
{
/// <summary>
/// 用於繫結到前端,前端在做許可權和介面元素的繫結時,一般不會用id(id可讀性差)和name(name可能會改變),一般以code做約定
/// </summary>
/// <returns></returns>
string GetResourceCode();
}
角色資源關係介面
public interface IRoleResource
{
string GetRoleKey();
string GetResourceKey();
}
核心許可權介面定義
IPermission介面定義
/// <summary>
/// 許可權介面,這此介面是對外的,非對外的方法,不要寫在接口裡。
/// </summary>
public interface IPermission
{
#region 用於判斷使用者是否有資源許可權的必要方法
/// <summary>
/// 通過訪問的資源,獲取資源的key。如obj可能為action,url
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
string GetRequestResourceKey(object obj);
/// <summary>
/// 通過物件獲取資源code
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
string GetRequestResourceCode(object obj);
/// <summary>
/// 使用者是否有資源的許可權
/// </summary>
/// <param name="resourceKey">資源key</param>
/// <param name="userKey">使用者key</param>
/// <returns></returns>
bool HasPermission(string resourceKey, string userKey);
/// <summary>
/// 從ClaimsPrincipal獲取使用者資訊
/// </summary>
/// <param name="claimsPrincipal">ClaimsPrincipal</param>
/// <returns></returns>
UserInfo GetUserInfo(ClaimsPrincipal claimsPrincipal);
#endregion
#region 登入、前端介面許可權控制必要方法
/// <summary>
/// 登入
/// </summary>
/// <param name="loginDto">登入dto</param>
/// <returns>如果登入成功,返回的結果;如果登入不成功,會丟擲異常</returns>
/// <remarks>
/// 配置GetAllResourceRoles方法,可實現前端的許可權控制
/// </remarks>
LoginResult Login(LoginDto loginDto);
/// <summary>
/// 獲取所有的資源以及資源角色的對應關係資訊
/// </summary>
/// <returns></returns>
/// <remarks>
/// 前端呼叫此介面,獲取所有的資源及資源的角色,用於渲染介面許可權控制
/// </remarks>
List<ResourceRoleInfo> GetAllResourceRoles();
/// <summary>
/// 通過userInfo生成Claims,Claims會用於生成token
/// </summary>
/// <param name="userInfo"></param>
/// <returns></returns>
List<Claim> GetClaims(IUserInfo userInfo);
///// <summary>
///// 獲取登入token
///// </summary>
///// <param name="account"></param>
///// <param name="pwd"></param>
///// <returns></returns>
//string GetLoginToken(string account, string pwd);
///// <summary>
///// 獲取使用者資訊,用於給前端使用者展示
///// </summary>
///// <param name="token"></param>
//IUserInfo GetUserInfo(string token);
#endregion
#region 其它
/// <summary>
/// 獲取password的hash,可能加salt或是不加,hash的演算法也可以由使用者自己配置。
/// 如果使用者密碼在儲存時不做hash處理,則此方法返回pwd的明文即可
/// 此方法用於兩處
/// 1、登入驗證
/// 2、修改、增加密碼時
/// </summary>
/// <param name="pwd">使用者輸入的密碼明文</param>
/// <returns>密碼明文的hash</returns>
string HashPwd(string pwd);
/// <summary>
/// 初始化許可權資源
/// </summary>
void InitResource();
#endregion
}
IPermissionStore介面定義
/// <summary>
/// 許可權儲存相關的介面約定
/// </summary>
public interface IPermissionStore
{
#region 查詢許可權資料
/// <summary>
/// 獲取所有的使用者
/// </summary>
/// <returns></returns>
List<IUser> GetAllUser();
/// <summary>
/// 獲取所有的角色
/// </summary>
/// <returns></returns>
List<IRole> GetAllRole();
/// <summary>
/// 獲取所有角色和使用者的關係
/// </summary>
/// <returns></returns>
List<IUserRole> GetAllUserRole();
/// <summary>
/// 獲取所有的資源
/// </summary>
/// <returns></returns>
List<IResource> GetAllResource();
/// <summary>
/// 獲取所有角色和資源的關係
/// </summary>
/// <returns></returns>
List<IRoleResource> GetAllRoleResource();
#endregion
#region 管理許可權資料
/// <summary>
/// 儲存使用者
/// </summary>
/// <param name="user"></param>
void SaveUser(IUser user);
/// <summary>
/// 刪除使用者
/// </summary>
/// <param name="userKey"></param>
void RemoveUser(string userKey);
/// <summary>
/// 儲存角色
/// </summary>
/// <param name="role"></param>
void SaveRole(IRole role);
/// <summary>
/// 刪除角色
/// </summary>
/// <param name="roleKey"></param>
void RemoveRole(string roleKey);
/// <summary>
/// 儲存資源
/// </summary>
/// <param name="resource"></param>
void SaveResource(IResource resource);
/// <summary>
/// 刪除資源
/// </summary>
/// <param name="resourceKey"></param>
void RemoveResource(string resourceKey);
/// <summary>
/// 裝置使用者的角色
/// </summary>
/// <param name="userKey"></param>
/// <param name="roleKeys"></param>
void SetUserRoles(string userKey, List<string> roleKeys);
/// <summary>
/// 設定角色的資源
/// </summary>
/// <param name="roleKey">角色key</param>
/// <param name="resourceKeys">資源keys</param>
void SetRoleResources(string roleKey, List<string> resourceKeys);
/// <summary>
/// IPermissionStore的實現裡如果用了快取,此方法用於重新整理快取為最新資料。
/// 如果使用者是通過非IPermissionStore介面方法操作許可權資料,則要呼叫此方法進行資料重新整理
/// </summary>
void ReloadPemissionDatas();
#endregion
}
怎麼用?
下面按將許可權控制接入到專案的開發步驟進行示例和解讀
1、定義許可權表實體
- 包含人員、角色、人員角色關係、資源、角色資源這5個表
- 分別定義User,Role,UserRole,Resource,RoleResource5個實體,分別繼承IUser,IRole,IUserRole,IResource,IRoleResource介面
- 由於程式碼比較簡單,就不附原始碼了,詳細可以檢視ApplicationCore的Entities資料夾裡的實現定義
2、建立IPermissionStore介面的實現類
- 資料庫框架用的是entityframework core,將已經定義好的實體加到DbContext裡(參考Infrastracture專案裡的AppDbContext)
- 為方便擴充套件,我建立了基類BasePermissionStore,並實現IPermissionStore,在專案接入時,可繼承BasePermissionStore類,並實現部分虛方法即可。如DefaultPermissionStore。(由於只是簡單的資料庫CRUD操作,詳細程式碼請檢視Web專案裡Permission裡的程式碼)
- 預設的實現裡,我加了快取,避免每次許可權驗證時去查庫,並在許可權相關資料改變時清空對應的快取
3、建立IPermission介面的實現類
- 為方便是快速接入,可繼承BasePermission。或自己實現IPermission介面
- 本框架預設的實現為DefaultPermission
- IPermission的大致思路為,用IPermissionStore裡提供的許可權相關資料,判斷使用者的角色,進而知道使用者有哪些授權資源。
- 附BasePermission和DefaultPermission的原始碼
BasePermission
/// <summary>
/// 許可權控制抽象基類,外部在實現許可權控制時,如果繼承此類,會簡化實現的過程,也可以繼承IPermission介面,自己實現
/// </summary>
/// <remarks>
/// todo 由於鑑權是頻繁的操作,後期計劃將鑑權方法裡linq相關的操作用hash和快取技術實現,進一步提高效能
/// </remarks>
public abstract class BasePermission : IPermission
{
protected IPermissionStore _permissionStore;
protected abstract PermissionOptions PermissionOptions {set;get;}
public BasePermission(IPermissionStore permissionStore)
{
_permissionStore = permissionStore;
}
#region 用於判斷使用者是否有資源許可權的必要方法
public virtual string GetRequestResourceKey(object obj)
{
var resourceKey = string.Empty;
var resourceCode = GetRequestResourceCode(obj);
if (!string.IsNullOrEmpty(resourceCode))
{
resourceKey = _permissionStore.GetAllResource().FirstOrDefault(a => a.GetResourceCode() == resourceCode)?.GetKey();
}
return resourceKey;
}
public abstract string GetRequestResourceCode(object obj);
public virtual bool HasPermission(string resourceKey, string userKey)
{
var userRoleKeys = _permissionStore.GetAllUserRole().Where(a => a.GetUserKey() == userKey).Select(a => a.GetRoleKey());
var resource = _permissionStore.GetAllResource().FirstOrDefault(a => a.GetKey() == resourceKey);
//未納入到資源表裡的資源,如果進入到鑑權過程時,不允許訪問。請將不需要做許可權控制的資源設定成允許匿名訪問,避免進入到鑑權流程
if (resource==null)
{
return false;
}
var resourceRoleKeys = _permissionStore.GetAllRoleResource().Where(a => a.GetResourceKey() == resource.GetKey()).Select(a => a.GetRoleKey());
return userRoleKeys.Intersect(resourceRoleKeys).Any();
}
public virtual UserInfo GetUserInfo(ClaimsPrincipal claimsPrincipal)
{
return new UserInfo
{
Account = claimsPrincipal.FindFirst(PermissionConstant.accountClaim)?.Value,
RoleKeys = (claimsPrincipal.FindFirst(PermissionConstant.roleIdsClaim)?.Value ?? "").Split(',').ToList(),
RoleNames = (claimsPrincipal.FindFirst(PermissionConstant.rolesNamesClaim)?.Value ?? "").Split(',').ToList(),
UserKey = claimsPrincipal.FindFirst(PermissionConstant.userIdClaim)?.Value,
UserName = claimsPrincipal.FindFirst(PermissionConstant.userNameClaim)?.Value,
};
}
#endregion
#region 登入、前端介面許可權控制必要方法
/// <summary>
/// 登入,返回使用者的基本資訊和token
/// </summary>
/// <param name="loginDto">登入dto</param>
/// <returns>使用者的基本資訊和token物件</returns>
public virtual LoginResult Login(LoginDto loginDto)
{
var user = _permissionStore.GetAllUser().FirstOrDefault(a => a.GetAccount().Equals(loginDto.Account,StringComparison.OrdinalIgnoreCase));
if (user != null && HashPwd(loginDto.Pwd).Equals(user.GetPassword(),StringComparison.OrdinalIgnoreCase))
{
var roleKeys = _permissionStore.GetAllUserRole().Where(a => a.GetUserKey() == user.GetKey()).Select(a => a.GetRoleKey()) ?? new List<string>();
var roleNames = _permissionStore.GetAllRole().Where(a => roleKeys.Contains(a.GetKey())).Select(a => a.GetName()) ?? new List<string>();
var userInfo = new UserInfo
{
Account = user.GetAccount(),
RoleKeys = roleKeys.ToList(),
RoleNames = roleNames.ToList(),
UserKey = user.GetKey(),
UserName = user.GetName()
};
var claims = GetClaims(userInfo);
var tokenStr= GenerateTokenStr(claims);
return new LoginResult
{
Token = tokenStr,
UserInfo = userInfo
};
}
else
{
throw new BusinessException($"使用者名稱或密碼錯誤");
}
}
public virtual List<ResourceRoleInfo> GetAllResourceRoles()
{
var result = new List<ResourceRoleInfo>();
var allResource = _permissionStore.GetAllResource();
var allRole = _permissionStore.GetAllRole();
var allRoleResource = _permissionStore.GetAllRoleResource();
allResource.ForEach(resource =>
{
var resourceRoleKeys = allRoleResource.Where(a => a.GetResourceKey() == resource.GetKey()).Select(a => a.GetRoleKey()).Distinct().ToList();
result.Add(new ResourceRoleInfo
{
ResourceCode=resource.GetResourceCode(),
ResourceKey=resource.GetKey(),
ResourceName=resource.GetName(),
RoleKeys= resourceRoleKeys
});
});
return result;
}
public virtual List<Claim> GetClaims(IUserInfo userInfo)
{
return new List<Claim>
{
new Claim(PermissionConstant.userIdClaim,userInfo.UserKey),
new Claim(PermissionConstant.userNameClaim,userInfo.UserName),
new Claim(PermissionConstant.accountClaim,userInfo.Account),
new Claim(PermissionConstant.roleIdsClaim,string.Join(",",userInfo.RoleKeys??new List<string>()) ),
new Claim(PermissionConstant.rolesNamesClaim,string.Join(",",userInfo.RoleNames??new List<string>()) ),
};
}
#endregion
/// <summary>
/// 預設的密碼hash演算法
/// </summary>
/// <param name="pwd">密碼明文</param>
/// <returns></returns>
public virtual string HashPwd(string pwd)
{
return BitConverter.ToString(HashAlgorithm.Create(HashAlgorithmName.MD5.Name).ComputeHash(Encoding.UTF8.GetBytes(pwd))).Replace("-", "");
}
public abstract string GenerateTokenStr(List<Claim> claims);
public abstract void InitResource();
}
DefaultPermission
/// <summary>
/// 許可權的預設實現類
/// </summary>
public class DefaultPermission : BasePermission
{
public static readonly string superAdminRoleName = "SuperAdmin";
protected override PermissionOptions PermissionOptions { get; set; }
public DefaultPermission(IPermissionStore permissionStore, IOptionsMonitor<PermissionOptions> permissionOptions) : base(permissionStore)
{
PermissionOptions = permissionOptions.CurrentValue ?? new PermissionOptions();
}
public override bool HasPermission(string resourceKey, string userKey)
{
if (IsSuperAdmin(userKey))
{
return true;
}
return base.HasPermission(resourceKey, userKey);
}
public override string GenerateTokenStr(List<Claim> claims)
{
var expireTimeSpan = (PermissionOptions.ExpireTimeSpan == null || PermissionOptions.ExpireTimeSpan == TimeSpan.Zero) ? new TimeSpan(6, 0, 0) : PermissionOptions.ExpireTimeSpan;
SigningCredentials creds;
if (PermissionOptions.IsAsymmetric)
{
var key = new RsaSecurityKey(RSAHelper.GetRSAParametersFromFromPrivatePem(PermissionOptions.RsaPrivateKey));
creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
}
else
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(PermissionOptions.SymmetricSecurityKey));
creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
}
var token = new JwtSecurityToken(PermissionOptions.Issuer, PermissionOptions.Audience, claims, DateTime.Now, DateTime.Now.Add(expireTimeSpan), creds);
var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
return tokenStr;
}
public override string HashPwd(string pwd)
{
return HashHelper.Md5($"{pwd}{PermissionOptions.PasswordSalt}");
}
/// <summary>
/// 獲取資源物件的code,已經適配如下型別:AuthorizationFilterContext,ControllerActionDescriptor,methodInfo
/// 預設為className_methodName,或是resourceAttribute裡設定的code
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override string GetRequestResourceCode(object obj)
{
if (obj is MethodInfo)
{
return GetResourceCode((MethodInfo)obj);
}
MethodInfo methodInfo;
if (obj is AuthorizationFilterContext authorizationFilterContext)
{
if (authorizationFilterContext.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
methodInfo = controllerActionDescriptor.MethodInfo;
return GetResourceCode(methodInfo);
//resourceCode = GetResourceCode(controllerActionDescriptor.ControllerName, controllerActionDescriptor.ActionName);
}
}
if (obj is ControllerActionDescriptor controllerActionDescriptor1)
{
methodInfo = controllerActionDescriptor1.MethodInfo;
return GetResourceCode(methodInfo);
//resourceCode = GetResourceCode(controllerActionDescriptor1.ControllerName, controllerActionDescriptor1.ActionName);
}
if (obj is RouteEndpoint endpoint)
{
//.net core 3.1後,AuthorizationHandlerContext.Resource為endpoint
methodInfo = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>()?.MethodInfo;
return GetResourceCode(methodInfo);
}
return string.Empty;
}
/// <summary>
/// 初始化所有的許可權資源。
/// 所有有定義ResourceAttribute的方法都為許可權資源,否則不是。要使方法受許可權控制,必須做到如下兩點:1、在方法上加ResourceAttribute,2、在controller或是action上加Authorize
/// </summary>
public override void InitResource()
{
var resources = new List<Resource>();
if (PermissionOptions.ResourceAssemblies == null)
{
PermissionOptions.ResourceAssemblies = new List<Assembly>();
}
var existResources = _permissionStore.GetAllResource();
PermissionOptions.ResourceAssemblies.Add(this.GetType().Assembly);
PermissionOptions.ResourceAssemblies?.Distinct().ToList().ForEach(assembly =>
{
//對所有的controller類進行掃描
assembly.GetTypes().Where(type => typeof(ControllerBase).IsAssignableFrom(type)).ToList().ForEach(controller =>
{
var controllerIsAdded = false;//父是否增加
var parentId = IdGenerator.Generate<string>();
var parentResource = controller.GetCustomAttribute<ResourceAttribute>();
controller.GetMethods().ToList().ForEach(method =>
{
if (method.IsDefined(typeof(ResourceAttribute), true))
{
var methodResource = method.GetCustomAttribute<ResourceAttribute>();
if (!controllerIsAdded)
{
// 增加父
resources.Add(new Resource
{
Id = parentId,
Code = parentResource?.ResourceCode??controller.Name,
CreateTime = DateTime.Now,
IsDeleted = false,
Name = parentResource?.Description??controller.Name
});
controllerIsAdded = true;
}
// 增加子
resources.Add(new Resource
{
Id = IdGenerator.Generate<string>(),
Code = GetResourceCode(method),
CreateTime = DateTime.Now,
IsDeleted = false,
ParentId = parentId,
Name = methodResource?.Description??method.Name
});
}
});
});
});
resources.ForEach(item =>
{
var temp = new Resource
{
Id = item.Id,
Code = item.Code,
CreateTime = DateTime.Now,
IsDeleted = false,
Name = item.Name,
ParentId = item.ParentId,
UpdateTime = DateTime.Now
};
// 設定資源的id
var matchRs = existResources.FirstOrDefault(i => i.GetResourceCode() == temp.Code);
if (matchRs!=null)
{
temp.Id = matchRs.GetKey();
}
// 設定資源的父id
if (!string.IsNullOrEmpty(temp.ParentId))
{
var pa = resources.FirstOrDefault(a => a.Id == temp.ParentId);
var matchPa = existResources.FirstOrDefault(i => i.GetResourceCode() == pa?.Code);
if (matchPa!=null)
{
item.ParentId = matchPa.GetKey();
}
}
_permissionStore.SaveResource(item);
});
}
private bool IsSuperAdmin(string userKey)
{
var superRole = _permissionStore.GetAllRole().FirstOrDefault(a => a.GetName().Equals(DefaultPermission.superAdminRoleName,StringComparison.OrdinalIgnoreCase));
return _permissionStore.GetAllUserRole().Any(a => a.GetUserKey() == userKey && a.GetRoleKey() == superRole.GetKey());
}
/// <summary>
/// 通過類名和方法名,獲取
/// </summary>
/// <param name="className"></param>
/// <param name="methodName"></param>
/// <returns></returns>
private string GetResourceCode(MethodInfo methodInfo)
{
if (Attribute.IsDefined(methodInfo, typeof(ResourceAttribute)))
{
var attr = methodInfo.GetCustomAttribute<ResourceAttribute>();
if (attr != null && !string.IsNullOrEmpty(attr.ResourceCode))
{
return attr.ResourceCode;
}
}
return $"{methodInfo.DeclaringType.Name.Replace("Controller", "")}_{methodInfo.Name}";
}
}
4、編寫鑑權處理類PermissionRequirementHandler
- 鑑權的原理請參考微軟的官方文件https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/introduction?view=aspnetcore-3.1
- 原理概要解說
我用的是基於策略的鑑權方式,一個專案裡可以有多種方法(策略)來判斷一個資源是否有訪問許可權。策略的實現裡是以“是否獲得某個Requirement”來判斷是否有許可權,獲得Requirement即有授權,反之則無。而判斷是否有某某Requirement是由此Requirement的AuthorizationHandler來處理
配置策略
services.AddAuthorization(options =>
{
// 增加鑑權策略,並告知這個策略要判斷使用者是否獲得了PermissionRequirement這個Requirement
options.AddPolicy(PermissionConstant.PermissionAuthorizePolicy, policyBuilder =>
{
policyBuilder.AddRequirements(new PermissionRequirement());
});
});
PermissionRequirementHandler原始碼如下
public class PermissionRequirementHandler : AuthorizationHandler<PermissionRequirement>
{
private IPermission _permission;
public PermissionRequirementHandler(IPermission permission)
{
_permission = permission;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var resourceKey=_permission.GetRequestResourceKey(context.Resource);// 獲取資源的key
var userKey = _permission.GetUserInfo(context.User).UserKey; // 根據使用者的claims獲取使用者的key
if (_permission.HasPermission(resourceKey,userKey)) // 判斷使用者是否有許可權
{
context.Succeed(requirement); // 如果有許可權,則獲得此Requirement
}
return Task.CompletedTask;
}
}
5、配置身份驗證和許可權驗證
- 在Startup.cs裡注入許可權元件services.AddPermission,並在asp.net core管理裡配置好身份驗證和鑑權,即增加 app.UseAuthentication()和app.UseAuthorization();
- 預設的身份驗證現實支援cookies和token,當請求過來時,如果不包含token則走cookies方式,否則走token。
AddPermission原始碼如下
/// <summary>
/// 許可權控制核心,即必須的配置
/// </summary>
/// <param name="services"></param>
/// <param name="action"></param>
public static void AddPermission(this IServiceCollection services, Action<PermissionOptions> action)
{
services.TryAddScoped<IPermission, DefaultPermission>();
services.TryAddScoped<IPermissionStore, DefaultPermissionStore>();
#region 身份驗證
var permissionOption = new PermissionOptions();
action(permissionOption);
//addAuthentication不放到AddPermissionCore方法裡,是為了外部可自己配置
// 當未通過authenticate時(如無token或是token出錯時),會返回401,當通過了authenticate但沒通過authorize時,會返回403。
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(
CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
//下面的委託方法只會在第一次cookie驗證時呼叫,呼叫時會用到上面的permissionOption變數,但其實permissionOption變數是在以前已經初始化的,所以在此方法呼叫之前,permissionOption變數不會被釋放
options.Cookie.Name = "auth";
options.AccessDeniedPath = permissionOption.AccessDeniedPath;
options.LoginPath = permissionOption.LoginPath;
options.ExpireTimeSpan = permissionOption.ExpireTimeSpan != default ? permissionOption.ExpireTimeSpan : new TimeSpan(12, 0, 0);
options.ForwardDefaultSelector = context =>
{
string authorization = context.Request.Headers["Authorization"];
//身份驗證的順序為jwt、cookie
if (authorization != null && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return JwtBearerDefaults.AuthenticationScheme;
}
else
{
return CookieAuthenticationDefaults.AuthenticationScheme;
}
};
var cookieAuthenticationEvents = new CookieAuthenticationEvents
{
OnSignedIn = context =>
{
return Task.CompletedTask;
},
OnSigningOut = context =>
{
return Task.CompletedTask;
}
};
options.Events = cookieAuthenticationEvents;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
// jwt可用對稱和非對稱演算法進行驗籤
SecurityKey key;
if (permissionOption.IsAsymmetric)
{
key = new RsaSecurityKey(RSAHelper.GetRSAParametersFromFromPublicPem(permissionOption.RsaPublicKey));
}
else
{
key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(permissionOption.SymmetricSecurityKey));
}
options.TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = PermissionConstant.userIdClaim,
RoleClaimType = PermissionConstant.roleIdsClaim,
ValidIssuer = permissionOption.Issuer,
ValidAudience = permissionOption.Audience,
IssuerSigningKey = key,
ValidateIssuer = false,
ValidateAudience = false
};
var jwtBearerEvents = new JwtBearerEvents
{
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
}
};
options.Events = jwtBearerEvents;
});
#endregion
#region 授權
//許可權控制只要在配置IServiceCollection,不需要額外配置app管道
//許可權控制參考:https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.2
//handler和requirement有幾種關係:1 handler對多requirement(此時handler實現IAuthorizationHandler);1對1(實現AuthorizationHandler<PermissionRequirement>),和多對1
//所有的handler都要注入到services,用services.AddSingleton<IAuthorizationHandler, xxxHandler>(),而哪個requirement用哪個handler,低層會自動匹配。最後將requirement對到policy裡即可
services.AddAuthorization(options =>
{
options.AddPolicy(PermissionConstant.PermissionAuthorizePolicy, policyBuilder =>
{
policyBuilder.AddRequirements(new PermissionRequirement());
});
});
services.AddScoped<IAuthorizationHandler, PermissionRequirementHandler>();
services.AddMemoryCache();
services.TryAddScoped<IApplicationContext, ApplicationContext>();
services.AddHttpContextAccessor();
services.Configure(action);
#endregion
}
6、在需要進行許可權控制的action或是Controller上加Authorize特性
[Authorize(Policy = PermissionConstant.PermissionAuthorizePolicy)]
其它要點
如何根據程式碼的介面自動生成許可權資源
- IPermission接口裡定義了InitResource方法,此方法即是自動生成許可權資源的入口。
- 預設將所有的Controller裡的Action設定為許可權資源,如果不需要,則加上AllowAnonymous特性可即
- 資源的code為ControllerName_ActionName,描述資訊可以用Resource特性來定義
- 參考DefaultPermission.InitResource的實現邏輯