基于中间件的授权管理示例

  基于角色访问控制(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     }
Menu
 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     }
Permission
 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     }
Role
 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     }
PermissionCategory
 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     }
MenuInRole
 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     }
PermissionInRole
 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     }
UserInRole

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     }
PermissionContext
 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     }
IUserInRoleDataAccess
 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     }
UserInRoleDataAccess
 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     }
UserInRoleMapping

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     }
IUserInRoleProvider
 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     }
UserInRoleProvider

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 }
PermissionMiddleware
 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     }
PermissionExtensions

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     }
PermissionControllerBase
 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     }
UserInRoleController
  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     }
Startup

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     }
LocalCacheProvider
 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     }
CacheManager
 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     }
UserHttpContext

8.总结

  使用PostgreSQL数据库,Entity Framework Core做为数据提供程序且实现正向工程、逆向工程。在数据层(DataAccess)的上层和服务提供程序(Provider)的下层引入了缓存层(Cache),实现了简单的权限缓存功能。一般情况下服务提供程序职责可能是为服务层(Service)提供支持,但在本示例中省略了服务层,直接使用WebApi调用了服务提供程序(Provider)。最后以中间件的方式实现鉴权功能。此示例使用RPC实现鉴权,在中间件获取权限一处的URL片段controllerdemo/actiondemo其实就是UserInRole/GetUserInRolesByUserId。根据需要也可以再提供一套SDK为不适用RPC的项目使用。

原文地址:https://www.cnblogs.com/xuejietong/p/14349601.html