基于中间件的授权管理示例
基于角色访问控制(RBAC)的主要思路是以角色为“桥梁”连接用户与资源,而角色对资源的访问控制又是通过授权许可来完成。菜单是一种特殊的资源,不过为了从技术上和业务上方便操作,可以将其单独抽出来,做为一种独立的“资源”处理。开始之前需要理解授权与认证,此处没有对认证的处理,默认流程走到授权时都是认证通过的。
1.新建/生成实体
依次为菜单表(Menu)描述系统菜单项,以URI做为基本控制单元;授权许可表(Permission)描述系统可授予的许可,同样以URI做为基本控制单元;角色表(Role)描述系统可以授予用户的角色;权限类别表(PermissionCategory)描述权限的类别,主要作用是为权限归类,该类别可以根据实际业务自定义。剩下的实体表示数据之间的关联关系,如MenuInRole表示菜单与角色的对应关系,PermissionInRole表示许可与角色的对应关系,UserInRole表示用户与角色的对应关系。因之前已经创建了授权表结构,所以这里的实体是通过EFCore的逆向工程自动生成的。示例如下:
1 /// <summary> 2 /// Menu 3 /// </summary> 4 public class Menu 5 { 6 /// <summary> 7 /// Id 8 /// </summary> 9 public string Id { get; set; } 10 /// <summary> 11 /// Name 12 /// </summary> 13 public string Name { get; set; } 14 /// <summary> 15 /// Label 16 /// </summary> 17 public string Label { get; set; } 18 /// <summary> 19 /// Icon 20 /// </summary> 21 public string Icon { get; set; } 22 /// <summary> 23 /// Uri 24 /// </summary> 25 public string Uri { get; set; } 26 /// <summary> 27 /// Parent Id 28 /// </summary> 29 public string ParentId { get; set; } 30 /// <summary> 31 /// Sort 32 /// </summary> 33 public int? Sort { get; set; } 34 /// <summary> 35 /// Is Deleted 36 /// </summary> 37 public bool IsDeleted { get; set; } 38 /// <summary> 39 /// Created By 40 /// </summary> 41 public string CreatedBy { get; set; } 42 /// <summary> 43 /// Created Time 44 /// </summary> 45 [Column(TypeName = "timestamp")] 46 public DateTime CreatedTime { get; set; } 47 /// <summary> 48 /// Last Updated By 49 /// </summary> 50 public string LastUpdatedBy { get; set; } 51 /// <summary> 52 /// Last Updated Time 53 /// </summary> 54 [Column(TypeName = "timestamp")] 55 public DateTime LastUpdatedTime { get; set; } 56 57 /// <summary> 58 /// Menu In Roles 59 /// </summary> 60 public virtual ICollection<MenuInRole> MenuInRoles { get; set; } 61 }
1 /// <summary> 2 /// Permission 3 /// </summary> 4 public class Permission 5 { 6 /// <summary> 7 /// Id 8 /// </summary> 9 public string Id { get; set; } 10 /// <summary> 11 /// Name 12 /// </summary> 13 public string Name { get; set; } 14 /// <summary> 15 /// Label 16 /// </summary> 17 public string Label { get; set; } 18 /// <summary> 19 /// Uri 20 /// </summary> 21 public string Uri { get; set; } 22 /// <summary> 23 /// CategoryId 24 /// </summary> 25 public string PermissionCategoryId { get; set; } 26 /// <summary> 27 /// Description 28 /// </summary> 29 public string Description { get; set; } 30 /// <summary> 31 /// Is Deleted 32 /// </summary> 33 public bool IsDeleted { get; set; } 34 /// <summary> 35 /// Created By 36 /// </summary> 37 public string CreatedBy { get; set; } 38 /// <summary> 39 /// Created Time 40 /// </summary> 41 [Column(TypeName = "timestamp")] 42 public DateTime CreatedTime { get; set; } 43 /// <summary> 44 /// Last Updated By 45 /// </summary> 46 public string LastUpdatedBy { get; set; } 47 /// <summary> 48 /// Last Updated Time 49 /// </summary> 50 [Column(TypeName = "timestamp")] 51 public DateTime LastUpdatedTime { get; set; } 52 53 /// <summary> 54 /// CategoryId Navigation 55 /// </summary> 56 public virtual PermissionCategory PermissionCategory { get; set; } 57 /// <summary> 58 /// Permission In Roles 59 /// </summary> 60 public virtual ICollection<PermissionInRole> PermissionInRoles { get; set; } 61 }
1 /// <summary> 2 /// Role 3 /// </summary> 4 public class Role 5 { 6 /// <summary> 7 /// Id 8 /// </summary> 9 public string Id { get; set; } 10 /// <summary> 11 /// Tenant Id 12 /// </summary> 13 public string TenantId { get; set; } 14 /// <summary> 15 /// Name 16 /// </summary> 17 public string Name { get; set; } 18 /// <summary> 19 /// Label 20 /// </summary> 21 public string Label { get; set; } 22 /// <summary> 23 /// Description 24 /// </summary> 25 public string Description { get; set; } 26 /// <summary> 27 /// Is Deleted 28 /// </summary> 29 public bool IsDeleted { get; set; } 30 /// <summary> 31 /// Created By 32 /// </summary> 33 public string CreatedBy { get; set; } 34 /// <summary> 35 /// Created Time 36 /// </summary> 37 [Column(TypeName = "timestamp")] 38 public DateTime CreatedTime { get; set; } 39 /// <summary> 40 /// Last Updated By 41 /// </summary> 42 public string LastUpdatedBy { get; set; } 43 /// <summary> 44 /// Last Updated Time 45 /// </summary> 46 [Column(TypeName = "timestamp")] 47 public DateTime LastUpdatedTime { get; set; } 48 49 /// <summary> 50 /// Menu In Roles 51 /// </summary> 52 public virtual ICollection<MenuInRole> MenuInRoles { get; set; } 53 /// <summary> 54 /// Permission In Roles 55 /// </summary> 56 public virtual ICollection<PermissionInRole> PermissionInRoles { get; set; } 57 /// <summary> 58 /// User In Roles 59 /// </summary> 60 public virtual ICollection<UserInRole> UserInRoles { get; set; } 61 }
1 /// <summary> 2 /// Permission CategoryId 3 /// </summary> 4 public class PermissionCategory 5 { 6 /// <summary> 7 /// Id 8 /// </summary> 9 public string Id { get; set; } 10 /// <summary> 11 /// Name 12 /// </summary> 13 public string Name { get; set; } 14 /// <summary> 15 /// Label 16 /// </summary> 17 public string Label { get; set; } 18 /// <summary> 19 /// Is Deleted 20 /// </summary> 21 public bool IsDeleted { get; set; } 22 /// <summary> 23 /// Created By 24 /// </summary> 25 public string CreatedBy { get; set; } 26 /// <summary> 27 /// Created Time 28 /// </summary> 29 [Column(TypeName = "timestamp")] 30 public DateTime CreatedTime { get; set; } 31 /// <summary> 32 /// Last Updated By 33 /// </summary> 34 public string LastUpdatedBy { get; set; } 35 /// <summary> 36 /// Last Updated Time 37 /// </summary> 38 [Column(TypeName = "timestamp")] 39 public DateTime LastUpdatedTime { get; set; } 40 41 /// <summary> 42 /// Permissions 43 /// </summary> 44 public virtual ICollection<Permission> Permissions { get; set; } 45 }
1 /// <summary> 2 /// Menu In Role 3 /// </summary> 4 public class MenuInRole 5 { 6 /// <summary> 7 /// Id 8 /// </summary> 9 public string Id { get; set; } 10 /// <summary> 11 /// Role Id 12 /// </summary> 13 public string RoleId { get; set; } 14 /// <summary> 15 /// Menu Id 16 /// </summary> 17 public string MenuId { get; set; } 18 /// <summary> 19 /// Label 20 /// </summary> 21 public string Label { get; set; } 22 /// <summary> 23 /// Icon 24 /// </summary> 25 public string Icon { get; set; } 26 /// <summary> 27 /// Sort 28 /// </summary> 29 public int? Sort { get; set; } 30 /// <summary> 31 /// Is Deleted 32 /// </summary> 33 public bool IsDeleted { get; set; } 34 /// <summary> 35 /// Created By 36 /// </summary> 37 public string CreatedBy { get; set; } 38 /// <summary> 39 /// Created Time 40 /// </summary> 41 [Column(TypeName = "timestamp")] 42 public DateTime CreatedTime { get; set; } 43 /// <summary> 44 /// Last Updated By 45 /// </summary> 46 public string LastUpdatedBy { get; set; } 47 /// <summary> 48 /// Last Updated Time 49 /// </summary> 50 [Column(TypeName = "timestamp")] 51 public DateTime LastUpdatedTime { get; set; } 52 53 /// <summary> 54 /// Menu 55 /// </summary> 56 public virtual Menu Menu { get; set; } 57 /// <summary> 58 /// Role 59 /// </summary> 60 public virtual Role Role { get; set; } 61 }
1 /// <summary> 2 /// Permission In Role 3 /// </summary> 4 public class PermissionInRole 5 { 6 /// <summary> 7 /// Id 8 /// </summary> 9 public string Id { get; set; } 10 /// <summary> 11 /// Role Id 12 /// </summary> 13 public string RoleId { get; set; } 14 /// <summary> 15 /// Permission Id 16 /// </summary> 17 public string PermissionId { get; set; } 18 /// <summary> 19 /// Not Before (nbf) 20 /// </summary> 21 [Column(TypeName = "timestamp")] 22 public DateTime NotBefore { get; set; } 23 /// <summary> 24 /// Expiration (exp) 25 /// </summary> 26 [Column(TypeName = "timestamp")] 27 public DateTime Expiration { get; set; } 28 /// <summary> 29 /// Country Region 30 /// </summary> 31 public string CountryRegion { get; set; } 32 /// <summary> 33 /// Is Deleted 34 /// </summary> 35 public bool IsDeleted { get; set; } 36 /// <summary> 37 /// Created By 38 /// </summary> 39 public string CreatedBy { get; set; } 40 /// <summary> 41 /// Created Time 42 /// </summary> 43 [Column(TypeName = "timestamp")] 44 public DateTime CreatedTime { get; set; } 45 /// <summary> 46 /// Last Updated By 47 /// </summary> 48 public string LastUpdatedBy { get; set; } 49 /// <summary> 50 /// Last Updated Time 51 /// </summary> 52 [Column(TypeName = "timestamp")] 53 public DateTime LastUpdatedTime { get; set; } 54 55 /// <summary> 56 /// Permission 57 /// </summary> 58 public virtual Permission Permission { get; set; } 59 /// <summary> 60 /// Role 61 /// </summary> 62 public virtual Role Role { get; set; } 63 }
1 /// <summary> 2 /// User In Role 3 /// </summary> 4 public class UserInRole 5 { 6 /// <summary> 7 /// Id 8 /// </summary> 9 public string Id { get; set; } 10 /// <summary> 11 /// User Id 12 /// </summary> 13 public string UserId { get; set; } 14 /// <summary> 15 /// Role Id 16 /// </summary> 17 public string RoleId { get; set; } 18 /// <summary> 19 /// Is Deleted 20 /// </summary> 21 public bool IsDeleted { get; set; } 22 /// <summary> 23 /// Created By 24 /// </summary> 25 public string CreatedBy { get; set; } 26 /// <summary> 27 /// Created Time 28 /// </summary> 29 [Column(TypeName = "timestamp")] 30 public DateTime CreatedTime { get; set; } 31 /// <summary> 32 /// Last Updated By 33 /// </summary> 34 public string LastUpdatedBy { get; set; } 35 /// <summary> 36 /// Last Updated Time 37 /// </summary> 38 [Column(TypeName = "timestamp")] 39 public DateTime LastUpdatedTime { get; set; } 40 41 /// <summary> 42 /// Role 43 /// </summary> 44 public virtual Role Role { get; set; } 45 }
2.创建数据访问层
使用EFCore的逆向工程生成实体和上下文,为每一个实体建立DAO接口和实现。为方便后续迁移和执行正向工程,可以为每一个实体做数据结构和关系映射。示例如下:(上下文类、一个接口示例类、一个实现示例类、一个映射类)
1 public class PermissionContext : DbContext 2 { 3 private static readonly string _connectionString = AppSettings.Configuration.GetConnectionString("NpgConnectionString"); 4 5 public PermissionContext() 6 { 7 Database.EnsureCreated(); 8 } 9 10 public PermissionContext(DbContextOptions<PermissionContext> options) : base(options) 11 { 12 Database.EnsureCreated(); 13 } 14 15 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 16 { 17 if (!optionsBuilder.IsConfigured) 18 { 19 optionsBuilder.UseNpgsql(_connectionString); 20 } 21 } 22 23 protected override void OnModelCreating(ModelBuilder modelBuilder) 24 { 25 modelBuilder.ApplyConfiguration(new MenuInRoleMapping()); 26 modelBuilder.ApplyConfiguration(new MenuMapping()); 27 modelBuilder.ApplyConfiguration(new PermissionCategoryMapping()); 28 modelBuilder.ApplyConfiguration(new PermissionInRoleMapping()); 29 modelBuilder.ApplyConfiguration(new PermissionMapping()); 30 modelBuilder.ApplyConfiguration(new RoleMapping()); 31 modelBuilder.ApplyConfiguration(new UserInRoleMapping()); 32 } 33 34 public virtual DbSet<Menu> Menus { get; set; } 35 public virtual DbSet<MenuInRole> MenuInRoles { get; set; } 36 public virtual DbSet<Entities.Permission> Permissions { get; set; } 37 public virtual DbSet<PermissionCategory> PermissionCategories { get; set; } 38 public virtual DbSet<PermissionInRole> PermissionInRoles { get; set; } 39 public virtual DbSet<Role> Roles { get; set; } 40 public virtual DbSet<UserInRole> UserInRoles { get; set; } 41 }
1 public interface IUserInRoleDataAccess 2 { 3 int AddUserInRole(string tenantId, UserInRole userInRole); 4 5 int EditUserInRole(string tenantId, UserInRole userInRole); 6 7 int RemoveUserInRole(string tenantId, string id); 8 9 int Remove(string tenantId, string id); 10 11 UserInRole GetUserInRole(string tenantId, string id); 12 13 List<UserInRole> GetUserInRolesByUserId(string tenantId, string userId); 14 15 List<UserInRole> GetUserInRoles(string tenantId); 16 }
1 public class UserInRoleDataAccess : IUserInRoleDataAccess 2 { 3 public int AddUserInRole(string tenantId, UserInRole userInRole) 4 { 5 using PermissionContext context = new PermissionContext(); 6 7 context.UserInRoles.Add(userInRole); 8 int result = context.SaveChanges(); 9 10 return result; 11 } 12 13 public int EditUserInRole(string tenantId, UserInRole userInRole) 14 { 15 using PermissionContext context = new PermissionContext(); 16 17 context.Attach(userInRole); 18 context.Entry(userInRole).Property(p => p.LastUpdatedBy).IsModified = true; 19 context.Entry(userInRole).Property(p => p.LastUpdatedTime).IsModified = true; 20 context.Entry(userInRole).Property(p => p.RoleId).IsModified = true; 21 context.Entry(userInRole).Property(p => p.UserId).IsModified = true; 22 int result = context.SaveChanges(); 23 24 return result; 25 } 26 27 public int RemoveUserInRole(string tenantId, string id) 28 { 29 using PermissionContext context = new PermissionContext(); 30 31 int result = context.Database.ExecuteSqlInterpolated($"UPDATE \"user_in_role\" SET \"is_deleted\" = true WHERE \"id\"={id}"); 32 33 return result; 34 } 35 36 public int Remove(string tenantId, string id) 37 { 38 using PermissionContext context = new PermissionContext(); 39 40 int result = context.Database.ExecuteSqlInterpolated($"DELETE FROM \"user_in_role\" WHERE \"id\"={id}"); 41 42 return result; 43 } 44 45 public UserInRole GetUserInRole(string tenantId, string id) 46 { 47 using PermissionContext context = new PermissionContext(); 48 49 UserInRole userInRole = context.UserInRoles.Include(p => p.Role).Where(p => p.Id == id && !p.IsDeleted).AsNoTracking().FirstOrDefault(); 50 51 return userInRole; 52 } 53 54 public List<UserInRole> GetUserInRolesByUserId(string tenantId, string userId) 55 { 56 using PermissionContext context = new PermissionContext(); 57 58 List<UserInRole> userInRole = context.UserInRoles 59 .Include(p => p.Role) 60 .ThenInclude(p => p.PermissionInRoles) 61 .ThenInclude(p => p.Permission) 62 .Where(p => p.UserId == userId && !p.IsDeleted) 63 .AsNoTracking() 64 .ToList(); 65 66 return userInRole; 67 } 68 69 public List<UserInRole> GetUserInRoles(string tenantId) 70 { 71 using PermissionContext context = new PermissionContext(); 72 73 List<UserInRole> userInRoles = context.UserInRoles.Where(p => !p.IsDeleted).AsNoTracking().ToList(); 74 75 return userInRoles; 76 } 77 }
1 public class UserInRoleMapping : IEntityTypeConfiguration<UserInRole> 2 { 3 public void Configure(EntityTypeBuilder<UserInRole> builder) 4 { 5 builder.HasKey(p => p.Id).HasName("user_in_role_id"); ; 6 7 builder.Property(p => p.Id).IsRequired().HasColumnType("varchar(36)").HasColumnName("id"); 8 builder.Property(p => p.CreatedBy).IsRequired().HasColumnType("varchar(36)").HasColumnName("created_by"); 9 builder.Property(p => p.CreatedTime).IsRequired().HasColumnType("timestamp(6) without time zone").HasColumnName("created_time"); 10 builder.Property(p => p.IsDeleted).HasColumnType("boolean").HasDefaultValue(false).HasColumnName("is_deleted"); 11 builder.Property(p => p.LastUpdatedBy).HasColumnType("varchar(36)").HasColumnName("last_updated_by"); 12 builder.Property(p => p.LastUpdatedTime).HasColumnType("timestamp(6) without time zone").HasColumnName("last_updated_time"); 13 builder.Property(p => p.RoleId).IsRequired().HasColumnType("varchar(36)").HasColumnName("role_id"); 14 builder.Property(p => p.UserId).IsRequired().HasColumnType("varchar(36)").HasColumnName("user_id"); 15 16 builder.ToTable("user_in_role"); 17 } 18 }
3.创建服务提供程序
一般来说有了实体和数据访问层就可以做CRUD操作了,但为了屏蔽数据访问层差异、为接入其它层做准备、方便集成工作单元、事务等,需要包装各种业务服务使其易用易维护。示例如下:(一个接口示例类、一个实现示例类)
1 public interface IUserInRoleProvider 2 { 3 UserInRole AddUserInRole(string tenantId, UserInRole userInRole); 4 5 UserInRole EditUserInRole(string tenantId, UserInRole userInRole); 6 7 bool RemoveUserInRole(string tenantId, string userId, string id); 8 9 bool Remove(string tenantId, string userId, string id); 10 11 UserInRole GetUserInRole(string tenantId, string id); 12 13 List<UserInRole> GetUserInRolesByUserId(string tenantId, string userId); 14 15 List<UserInRole> GetUserInRoles(string tenantId); 16 }
1 public class UserInRoleProvider : IUserInRoleProvider 2 { 3 private static readonly string cacheKeyPrefix = nameof(UserInRole); 4 private static readonly IUserInRoleDataAccess _userInRoleDataAccess = new UserInRoleDataAccess(); 5 6 public UserInRole AddUserInRole(string tenantId, UserInRole userInRole) 7 { 8 userInRole.Id = Guid.NewGuid().ToString(); 9 10 _userInRoleDataAccess.AddUserInRole(tenantId, userInRole); 11 12 string key = $"{cacheKeyPrefix}_{userInRole.UserId}"; 13 CacheManager.Remove(key); 14 15 return userInRole; 16 } 17 18 public UserInRole EditUserInRole(string tenantId, UserInRole userInRole) 19 { 20 _userInRoleDataAccess.EditUserInRole(tenantId, userInRole); 21 22 string key = $"{cacheKeyPrefix}_{userInRole.UserId}"; 23 CacheManager.Remove(key); 24 25 return _userInRoleDataAccess.GetUserInRole(tenantId, userInRole.Id); 26 } 27 28 public bool RemoveUserInRole(string tenantId, string userId, string id) 29 { 30 bool result = _userInRoleDataAccess.RemoveUserInRole(tenantId, id) >= 1; 31 if (result) 32 { 33 string key = $"{cacheKeyPrefix}_{userId}"; 34 CacheManager.Remove(key); 35 } 36 37 return result; 38 } 39 40 public bool Remove(string tenantId, string userId, string id) 41 { 42 bool result = _userInRoleDataAccess.Remove(tenantId, id) >= 1; 43 if (result) 44 { 45 string key = $"{cacheKeyPrefix}_{userId}"; 46 CacheManager.Remove(key); 47 } 48 49 return result; 50 } 51 52 public UserInRole GetUserInRole(string tenantId, string id) 53 { 54 return _userInRoleDataAccess.GetUserInRole(tenantId, id); 55 } 56 57 public List<UserInRole> GetUserInRolesByUserId(string tenantId, string userId) 58 { 59 string key = $"{cacheKeyPrefix}_{userId}"; 60 61 return CacheManager.GetOrCreate(key, () => 62 { 63 return _userInRoleDataAccess.GetUserInRolesByUserId(tenantId, userId); 64 }); 65 } 66 67 public List<UserInRole> GetUserInRoles(string tenantId) 68 { 69 return _userInRoleDataAccess.GetUserInRoles(tenantId); 70 } 71 }
4.以中间件的方式授权
如果我们的服务端(服务端与客户端的名称是相对的,比如在这里提到的服务端在IDS中其实是客户端)需要接入授权功能,有较多的方式可以选择使用。因为中间件使用简单,且授权流程与中间件设计思想有异曲同工之妙,所以此处选择中间件来完成授权。使用中间件实现授权功能的方式主要有两种,一种是直接引入中间件项目,包括服务提供程序,另一种是仅引入中间件项目,使用RPC的方式调用授权服务。两种方式的优缺点都比较明显,第一种会将中间件项目的整个项目依赖全部引入,包括数据访问层等,另一种仅需要在引入中间件的同时引入实体项目即可。第一种方式的代码依赖变复杂了,但是因为没有RPC,性能会比第二种方式好一些,第二种方式代码简洁易维护易升级,但是性能就比第一种差了些。可根据实际情况选择,示例选用的是第二种方案,中间件项目以HTTP请求的方式完成授权功能,示例如下:(中间件示例,扩展示例)
1 public class PermissionMiddleware 2 { 3 private static readonly string _timeFormatter = "yyyy-MM-dd HH:mm:ss fff"; 4 private static readonly string _logPrefix = "PermissionMiddleware"; 5 6 private readonly string _identityApiHost; 7 private readonly string _permissionHost; 8 private readonly RequestDelegate _next; 9 private readonly ILogger _logger; 10 11 public PermissionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) 12 { 13 _next = next; 14 _logger = loggerFactory.CreateLogger<PermissionMiddleware>(); 15 _identityApiHost = AppSettings.Configuration.GetSection("IdentityApiHost").Value; 16 _permissionHost = AppSettings.Configuration.GetSection("PermissionHost").Value; 17 18 _logger.LogInformation($"{_logPrefix}|{DateTime.Now.ToString(_timeFormatter)}|Middleware Registered Successfully."); 19 } 20 21 public async Task Invoke(HttpContext context) 22 { 23 Stopwatch stopwatch = new Stopwatch(); 24 25 try 26 { 27 DebugEnter(stopwatch, nameof(Invoke)); 28 29 bool isPassed = false; 30 31 UserIdentityInfo userIdentityInfo = TryGetUserIdentityInfo(context); 32 string tenantId = userIdentityInfo.TenantId; 33 string userId = userIdentityInfo.UserId; 34 string token = userIdentityInfo.AccessToken; 35 36 if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(token)) 37 { 38 Unauthorized(context, "必要信息为空"); 39 40 return; 41 } 42 43 if (CheckAllowAnonymous(context)) 44 { 45 isPassed = true; 46 } 47 else 48 { 49 bool result = await CheckPermission(context, tenantId, userId); 50 if (result) 51 { 52 isPassed = true; 53 } 54 } 55 56 if (isPassed) 57 { 58 DebugExit(stopwatch, nameof(Invoke)); 59 60 await _next.Invoke(context); 61 } 62 else 63 { 64 Unauthorized(context, "未通过权限校验"); 65 } 66 } 67 catch (Exception ex) 68 { 69 await ExceptionHandle(context, ex); 70 } 71 } 72 73 private UserIdentityInfo TryGetUserIdentityInfo(HttpContext context) 74 { 75 IHeaderDictionary headers = context.Request.Headers; 76 IQueryCollection query = context.Request.Query; 77 UserIdentityInfo userIdentityInfo = new UserIdentityInfo(); 78 79 if (headers != null) 80 { 81 userIdentityInfo.TenantId = headers.ContainsKey(Constant.TenantId) ? headers[Constant.TenantId].ToString() : null; 82 userIdentityInfo.UserId = headers.ContainsKey(Constant.UserId) ? headers[Constant.UserId].ToString() : null; 83 userIdentityInfo.AccessToken = headers.ContainsKey(Constant.Authorization) ? headers[Constant.Authorization].ToString() : null; 84 } 85 86 if (query != null) 87 { 88 userIdentityInfo.TenantId = string.IsNullOrWhiteSpace(userIdentityInfo.TenantId) ? query.ContainsKey(Constant.TenantId) ? query[Constant.TenantId].ToString() : null : userIdentityInfo.TenantId; 89 userIdentityInfo.UserId = string.IsNullOrWhiteSpace(userIdentityInfo.UserId) ? query.ContainsKey(Constant.UserId) ? query[Constant.UserId].ToString() : null : userIdentityInfo.UserId; 90 userIdentityInfo.AccessToken = string.IsNullOrWhiteSpace(userIdentityInfo.AccessToken) ? query.ContainsKey(Constant.Authorization) ? query[Constant.Authorization].ToString() : null : userIdentityInfo.AccessToken; 91 } 92 93 return userIdentityInfo; 94 } 95 96 private bool CheckAllowAnonymous(HttpContext context) 97 { 98 Endpoint endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint; 99 if (endpoint != null) 100 { 101 return endpoint.Metadata.GetMetadata<IAllowAnonymous>() != null; 102 } 103 104 return false; 105 } 106 107 private async Task<bool> CheckPermission(HttpContext context, string tenantId, string userId) 108 { 109 string requestPath = context.Request.Path.Value.ToLower(); 110 111 List<Entities.Permission> permissions = await GetUserPermissions(tenantId, userId); 112 Entities.Permission permission = permissions?.Find(p => requestPath.StartsWith(p.Uri.ToLower()) && (requestPath.Length == p.Uri.Length || requestPath[p.Uri.Length] == '/')); 113 114 if (permission != null) 115 { 116 return true; 117 } 118 119 return false; 120 } 121 122 private async Task<List<Entities.Permission>> GetUserPermissions(string tenantId, string userId) 123 { 124 Stopwatch stopwatch = new Stopwatch(); 125 DebugEnter(stopwatch, nameof(GetUserPermissions)); 126 127 List<Entities.Permission> permissions = new List<Entities.Permission>(); 128 129 string url = $"{_permissionHost}api/controllerdemo/actiondemo?tenantId={tenantId}&userId={userId}"; 130 string result = await HttpHelper.RequestAsync(url); 131 if (string.IsNullOrWhiteSpace(result)) 132 { 133 return null; 134 } 135 136 List<UserInRole> userInRoles = JsonSerializer.Deserialize<List<UserInRole>>(result); 137 138 if (userInRoles != null) 139 { 140 foreach (var userInRole in userInRoles) 141 { 142 List<PermissionInRole> permissionInRoles = userInRole.Role?.PermissionInRoles?.ToList(); 143 if (permissionInRoles != null) 144 { 145 foreach (var permissionInRole in permissionInRoles) 146 { 147 permissions.Add(permissionInRole.Permission); 148 } 149 } 150 } 151 } 152 153 DebugExit(stopwatch, nameof(GetUserPermissions)); 154 155 return permissions; 156 } 157 158 private async Task ExceptionHandle(HttpContext context, Exception ex) 159 { 160 _logger.LogError(ex, ex.Message); 161 162 context.Response.StatusCode = StatusCodes.Status401Unauthorized; 163 string message = "Unauthorized"; 164 165 await context.Response.WriteAsync($"{message}"); 166 } 167 168 private void Unauthorized(HttpContext context, string message) 169 { 170 string logSegment = $"{_logPrefix}|{DateTime.Now.ToString(_timeFormatter)}"; 171 172 _logger.LogWarning($"{logSegment}|Unauthorized|{message}"); 173 174 context.Response.StatusCode = StatusCodes.Status401Unauthorized; 175 } 176 177 private void DebugEnter(Stopwatch stopwatch, string method) 178 { 179 string logSegment = $"{_logPrefix}|{DateTime.Now.ToString(_timeFormatter)}"; 180 181 stopwatch.Start(); 182 183 _logger.LogDebug($"{logSegment}|Enter {method}."); 184 } 185 186 private void DebugExit(Stopwatch stopwatch, string method) 187 { 188 string logSegment = $"{_logPrefix}|{DateTime.Now.ToString(_timeFormatter)}"; 189 190 _logger.LogDebug($"{logSegment}|Exit {method}. Cost:{stopwatch.ElapsedMilliseconds}"); 191 192 stopwatch.Stop(); 193 } 194 }
1 public static class PermissionExtensions 2 { 3 public static IApplicationBuilder UsePermission(this IApplicationBuilder app) 4 { 5 if (app == null) 6 { 7 throw new ArgumentNullException(nameof(app)); 8 } 9 10 return app.UseMiddleware<PermissionMiddleware>(); 11 } 12 }
5.为授权提供API服务
新增AspNetCore项目,该项目引用服务提供程序,主要完成两个功能,一个是对资源、许可、角色的管理,一个是为授权中间件提供远程服务。示例如下:(一个Controller基类、一个Controller示例、Startup)
1 /// <summary> 2 /// Permission ControllerBase Class 3 /// </summary> 4 [Route("api/[controller]/[action]")] 5 [ApiController] 6 public class PermissionControllerBase : ControllerBase 7 { 8 /// <summary> 9 /// User HttpContext 10 /// </summary> 11 protected UserHttpContext UserHttpContext { get; } 12 13 /// <summary> 14 /// TenantId 15 /// </summary> 16 protected string TenantId { get; } 17 18 /// <summary> 19 /// UserId 20 /// </summary> 21 protected string UserId { get; } 22 23 /// <summary> 24 /// Permission ControllerBase Constructor 25 /// </summary> 26 /// <param name="userHttpContext"></param> 27 public PermissionControllerBase(UserHttpContext userHttpContext) 28 { 29 UserHttpContext = userHttpContext; 30 31 if (UserHttpContext != null) 32 { 33 TenantId = UserHttpContext.TenantId; 34 UserId = UserHttpContext.UserId; 35 } 36 } 37 }
1 /// <summary> 2 /// UserInRole Controller Class 3 /// </summary> 4 public class UserInRoleController : PermissionControllerBase 5 { 6 private readonly IUserInRoleProvider _userInRoleProvider; 7 8 /// <summary> 9 /// UserInRole Controller Constructor 10 /// </summary> 11 /// <param name="userInRoleProvider"></param> 12 /// <param name="userHttpContext"></param> 13 public UserInRoleController(IUserInRoleProvider userInRoleProvider, UserHttpContext userHttpContext) : base(userHttpContext) 14 { 15 _userInRoleProvider = userInRoleProvider; 16 } 17 18 /// <summary> 19 /// Get UserInRole 20 /// </summary> 21 /// <param name="id"></param> 22 /// <returns></returns> 23 [HttpGet] 24 public UserInRole GetUserInRole(string id) 25 { 26 return _userInRoleProvider.GetUserInRole(TenantId, id); 27 } 28 29 /// <summary> 30 /// Get UserInRoles By UserId 31 /// </summary> 32 /// <returns></returns> 33 [HttpGet] 34 public List<UserInRole> GetUserInRolesByUserId() 35 { 36 return _userInRoleProvider.GetUserInRolesByUserId(TenantId, UserId); 37 } 38 39 /// <summary> 40 /// Get UserInRoles 41 /// </summary> 42 /// <returns></returns> 43 [HttpGet] 44 public List<UserInRole> GetUserInRoles() 45 { 46 return _userInRoleProvider.GetUserInRoles(TenantId); 47 } 48 49 /// <summary> 50 /// Add UserInRole 51 /// </summary> 52 /// <param name="userInRole"></param> 53 /// <returns></returns> 54 [HttpPost] 55 public UserInRole AddUserInRole(UserInRole userInRole) 56 { 57 return _userInRoleProvider.AddUserInRole(TenantId, userInRole); 58 } 59 60 /// <summary> 61 /// Edit UserInRole 62 /// </summary> 63 /// <param name="userInRole"></param> 64 /// <returns></returns> 65 [HttpPost] 66 public UserInRole EditUserInRole(UserInRole userInRole) 67 { 68 return _userInRoleProvider.EditUserInRole(TenantId, userInRole); 69 } 70 71 /// <summary> 72 /// Remove UserInRole 73 /// </summary> 74 /// <param name="id"></param> 75 /// <returns></returns> 76 [HttpPost] 77 public bool RemoveUserInRole(string id) 78 { 79 return _userInRoleProvider.RemoveUserInRole(TenantId, UserId, id); 80 } 81 }
1 /// <summary> 2 /// Startup Class 3 /// </summary> 4 public class Startup 5 { 6 /// <summary> 7 /// Startup Constructor 8 /// </summary> 9 /// <param name="configuration"></param> 10 public Startup(IConfiguration configuration) 11 { 12 Configuration = configuration; 13 } 14 15 /// <summary> 16 /// Represents a set of key/value application configuration properties. 17 /// </summary> 18 public IConfiguration Configuration { get; } 19 20 /// <summary> 21 /// This method gets called by the runtime. Use this method to add services to the container. 22 /// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 23 /// </summary> 24 /// <param name="services"></param> 25 public void ConfigureServices(IServiceCollection services) 26 { 27 services.AddDbContext<PermissionContext>(options => options.UseNpgsql(Configuration.GetConnectionString("NpgConnectionString"))); 28 29 services.AddControllers().AddNewtonsoftJson(p => 30 { 31 p.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; 32 p.SerializerSettings.ContractResolver = new DefaultContractResolver(); 33 }); 34 35 SwaggerGenServiceCollection(services); 36 37 services.AddHttpContextAccessor(); 38 39 services.AddScoped<UserHttpContext>(); 40 services.AddScoped<IUserInRoleProvider, UserInRoleProvider>(); 41 } 42 43 /// <summary> 44 /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 45 /// </summary> 46 /// <param name="app"></param> 47 /// <param name="env"></param> 48 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 49 { 50 if (env.IsDevelopment()) 51 { 52 app.UseDeveloperExceptionPage(); 53 54 // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 55 // specifying the Swagger JSON endpoint. 56 app.UseSwaggerUI(c => 57 { 58 c.SwaggerEndpoint("/swagger/v1/swagger.json", "Sample Swagger V1"); 59 }); 60 } 61 62 app.UseStaticFiles(); 63 64 app.UseSwagger(); 65 66 app.UseRouting(); 67 68 app.UseEndpoints(endpoints => 69 { 70 endpoints.MapControllers(); 71 }); 72 } 73 74 private void SwaggerGenServiceCollection(IServiceCollection services) 75 { 76 //Swagger 77 services.AddSwaggerGen(c => 78 { 79 c.SwaggerDoc("v1", new OpenApiInfo 80 { 81 Version = "v1", 82 Title = "Permission", 83 Description = "Authentication Authorization", 84 TermsOfService = new Uri("https://TODO") 85 Contact = new OpenApiContact 86 { 87 Name = "#TODO#", 88 Email = "#TODO#", 89 Url = new Uri("https://TODO"), 90 }, 91 License = new OpenApiLicense 92 { 93 Name = "#TODO#", 94 Url = new Uri("https://TODO"), 95 } 96 }); 97 98 c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "Demo.Permission.Entities.xml")); 99 c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "Demo.Permission.Restful.xml")); 100 }); 101 } 102 }
6.单元测试
中间件的单元测试参见:为AspNetCore中间件编写单元测试,地址:https://www.cnblogs.com/xuejietong/p/14336602.html
7.其它技术
示例中使用了缓存(仅做示例使用,重点在于简单授权的思路,缓存未做高可用支持,比如逻辑过期时间、物理过期时间、同步等均未考虑)、依赖注入(当前用户上下文)等技术,核心代码如下所示:
1 public sealed class LocalCacheProvider 2 { 3 private static readonly IMemoryCache _cache = new MemoryCache(Options.Create(new MemoryCacheOptions() { })); 4 5 public static LocalCacheProvider Instance { get; } = new LocalCacheProvider(); 6 7 private LocalCacheProvider() { } 8 9 public bool? Get<T>(string key, out T t) 10 { 11 return _cache.TryGetValue(key, out t); 12 } 13 14 public bool? Set<T>(string key, T t) 15 { 16 _cache.Set(key, t, UseDefaultExpiration()); 17 return true; 18 } 19 20 public bool? Remove(string key) 21 { 22 _cache.Remove(key); 23 return true; 24 } 25 26 private static MemoryCacheEntryOptions UseDefaultExpiration() 27 { 28 return new MemoryCacheEntryOptions().SetAbsoluteExpiration(new DateTimeOffset(DateTime.Now.Date.AddDays(1).AddHours(4))); 29 } 30 }
1 public static class CacheManager 2 { 3 private static readonly object obj = new object(); 4 5 public static T GetOrCreate<T>(string key, Func<T> func) 6 { 7 T t = GetCache<T>(key); 8 9 if (null == t) 10 { 11 lock (obj) 12 { 13 t = GetCache<T>(key); 14 15 if (null == t) 16 { 17 t = func(); 18 19 SetCache(key, t); 20 } 21 } 22 } 23 24 return t; 25 } 26 27 public static void SetCache<T>(string key, T t) 28 { 29 LocalCacheProvider.Instance.Set(key, t); 30 } 31 32 public static void Remove(string key) 33 { 34 LocalCacheProvider.Instance.Remove(key); 35 } 36 37 private static T GetCache<T>(string key) 38 { 39 LocalCacheProvider.Instance.Get(key, out T t); 40 41 return t; 42 } 43 }
1 public sealed class UserHttpContext 2 { 3 private string _tenantId; 4 private string _userId; 5 6 public string TenantId { get { return _tenantId; } } 7 8 public string UserId { get { return _userId; } } 9 10 public UserHttpContext(IHttpContextAccessor httpContextAccessor) 11 { 12 HttpContext context = httpContextAccessor?.HttpContext; 13 14 if (context != null) 15 { 16 TrySetUserHttpContext(context); 17 18 if (_tenantId == null) 19 { 20 throw new System.ArgumentNullException(Constant.TenantId); 21 } 22 if (_userId == null) 23 { 24 throw new System.ArgumentNullException(Constant.UserId); 25 } 26 } 27 } 28 29 private void TrySetUserHttpContext(HttpContext context) 30 { 31 IHeaderDictionary headers = context.Request.Headers; 32 IQueryCollection query = context.Request.Query; 33 34 if (headers != null) 35 { 36 _tenantId = headers.ContainsKey(Constant.TenantId) ? headers[Constant.TenantId].ToString() : null; 37 _userId = headers.ContainsKey(Constant.UserId) ? headers[Constant.UserId].ToString() : null; 38 } 39 40 if (query != null) 41 { 42 _tenantId = string.IsNullOrWhiteSpace(_tenantId) ? query.ContainsKey(Constant.TenantId) ? query[Constant.TenantId].ToString() : null : _tenantId; 43 _userId = string.IsNullOrWhiteSpace(_userId) ? query.ContainsKey(Constant.UserId) ? query[Constant.UserId].ToString() : null : _userId; 44 } 45 } 46 }
8.总结
使用PostgreSQL数据库,Entity Framework Core做为数据提供程序且实现正向工程、逆向工程。在数据层(DataAccess)的上层和服务提供程序(Provider)的下层引入了缓存层(Cache),实现了简单的权限缓存功能。一般情况下服务提供程序职责可能是为服务层(Service)提供支持,但在本示例中省略了服务层,直接使用WebApi调用了服务提供程序(Provider)。最后以中间件的方式实现鉴权功能。此示例使用RPC实现鉴权,在中间件获取权限一处的URL片段controllerdemo/actiondemo其实就是UserInRole/GetUserInRolesByUserId。根据需要也可以再提供一套SDK为不适用RPC的项目使用。