總體設計思路
在設計本項目的通用權限前,我參閱過很多設計方案,最終定下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的實現邏輯