XiHan.Framework.Security.Database
授权的数据库 Provider:权限授予是数据库里的 PermissionGrantRecord 行,经 persistence.relational@1 的仓储读写,授予存储带有界的 TTL 缓存。它只提供 security.authorization@1,适用于授予由管理后台维护、多实例共享的生产应用。
| 项 | 内容 |
|---|---|
| 包 ID | XiHan.Framework.Security.Database |
| 环层 | Ring 3(实现) |
| 能力 | 提供 security.authorization@1;依赖 kernel.hosting@1、persistence.relational@1 |
| 特性入口 | DatabaseSecurityFeature(清单 xihan.security.database,role: provider) |
| 配置节 | XiHan:Security(含 Database 子节) |
| 依赖 | XiHan.Framework.Security.Abstractions、XiHan.Framework.Persistence.Abstractions;不引用数据库驱动 |
| AOT | 兼容 |
| 稳定性 | Preview |
概述
本包只换授予存储(RFC-0008,ADR-0027):检查器、策略目录、authenticated 策略与 Authorize 阶段中间件与内存实现是同一套,IPermissionFilter 与 IPermissionGrantSource 照常生效。读写分成两个类型:
DatabasePermissionGrantStore是单例IPermissionGrantStore,Authorize 阶段经检查器查它。它按(主体,租户)一次查询全局行加该租户的行,在根提供者的新作用域里直接开只读工作单元读仓储(配了从库也读主库,ADR-0122),不经App.InvokeAsync:授予在 Authorize 阶段被查询,嵌套调用会再过一遍 Tenant 与 Authorize 阶段,而且要求应用处于服务状态。结果进有界的 TTL 缓存,返回冻结集合,调用方改不了缓存。PermissionGrantRecordStore是作用域服务,在调用的 Transaction 阶段内用调用方的工作单元批量授予、撤销,逐项返回结果,写入后失效涉及主体的缓存。
授予是平台级数据:PermissionGrantRecord 不实现 IMultiTenant,不受当前调用租户过滤,不软删除,租户在授予表自己的列里。全局授予以空字符串(PermissionGrantRecord.GlobalTenantId)落库,因为唯一索引对 NULL 的语义各数据库不一致;契约边界仍以 PermissionGrant.TenantId == null 表示全局。
本包不提供认证。与 Security.InMemory 同时装配时顶替它的授权部分,内存实现只剩 ApiKey 认证;认证也可以换成 Security.Jwt 或 Security.OpenIdConnect。被别的显式授权 Provider 顶替时,本包只保留自己的类型,不注册检查器与中间件,也不核对策略引用。
何时使用
- 授予在运行时由管理后台增删,或多实例部署要共享授予:用本包。
- 开发与测试、授予随配置提交:用 Security.InMemory。
安装与启用
dotnet add package XiHan.Framework.Security.Database --prerelease当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。本包必须和一个持久化 Provider 一起装配:
using SqlSugar;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Persistence.SqlSugar;
using XiHan.Framework.Profiles.Standard;
using XiHan.Framework.Security.Database;
using XiHan.Framework.Security.Jwt;
var builder = WebApplication.CreateBuilder(args);
// JWT 认证 + 数据库授予;Standard 组合包里的内存持久化与内存安全都被顶替
builder.AddXiHan(xihan => xihan
.UseStandardProfile()
.UseFeature(new SqlSugarPersistenceFeature(static (binding, config) =>
{
config.DbType = DbType.PostgreSQL;
config.ConnectionString = binding.ValueOrThrow().Value;
}))
.UseFeature<JwtSecurityFeature>()
.UseFeature<DatabaseSecurityFeature>());
var app = builder.Build();
app.UseXiHan();
app.Run();Persistence.EntityFrameworkCore 的应用在自己的 DbContext 里登记实体并应用协议约定,四列的联合唯一索引由实体上的 [EntityIndex] 建出:
using Microsoft.EntityFrameworkCore;
using XiHan.Framework.Security.Database.Grants;
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
// 全局授予的 TenantId 是空字符串而不是 NULL,唯一索引在各数据库上语义一致
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PermissionGrantRecord>();
modelBuilder.ApplyPersistenceConventions(Database);
}
}配置
| 键 | 类型 | 缺省 | 说明 |
|---|---|---|---|
XiHan:Security:Database:CacheSize | int | 10000 | 缓存条目上限,每个(主体,租户)组合一条;满时整体清空 |
XiHan:Security:Database:CacheTimeToLiveSeconds | double | 30 | 每条缓存的存活时间;0 表示不过期,只靠失效与容量清空 |
XiHan:Security:RoleClaim 决定检查器从哪类声明取角色,见 Security.Abstractions。
{
"XiHan": {
"Bindings": { "db": "Host=localhost;Database=app" },
"Security": {
"RoleClaim": "role",
"Database": { "CacheSize": 10000, "CacheTimeToLiveSeconds": 30 }
}
}
}主要类型
| 类型 | 说明 |
|---|---|
DatabaseSecurityFeature | 特性入口:登记授予实体的仓储、授予存储、PermissionChecker、authenticated 策略、PolicyCatalog 与 AuthorizationMiddleware;不声明健康项 |
DatabaseSecurityOptions | CacheSize、CacheTimeToLive(TimeSpan?,null 不过期) |
Grants.PermissionGrantRecord | 授予行:Id、SubjectKind、SubjectId、Permission、TenantId、CreatedAt;ToGrant() 换算成 PermissionGrant;TenantColumnOf(tenantId) 给出租户列的值 |
Grants.DatabasePermissionGrantStore | 单例 IPermissionGrantStore:GetGrantsAsync(Result,数据库故障为 SECURITY_UNAVAILABLE_001)、Invalidate(subject?)、CachedEntryCount |
Grants.PermissionGrantRecordStore | 作用域服务:GrantAsync、RevokeAsync 批量逐项结果,ListAsync(subject?, tenantId?, skip, take) 分页 |
Grants.PermissionGrantResult | 一项结果:Grant、Outcome |
使用示例
管理操作在处理器里经 PermissionGrantRecordStore 写入,Transaction 阶段已经开了工作单元:
using XiHan.Framework.Security.Abstractions.Permissions;
using XiHan.Framework.Security.Database.Grants;
// 作用域服务,在操作的处理器里使用
public sealed class RoleAdministration(PermissionGrantRecordStore grants)
{
// 一次授予多项:已存在的返回成功且不重复写
public ValueTask<IReadOnlyList<PermissionGrantResult>> SetUpEditorAsync(string tenantId, CancellationToken cancellationToken)
=> grants.GrantAsync(
[
new PermissionGrant(GrantSubject.ForRole("editor"), "orders.read"),
new PermissionGrant(GrantSubject.ForRole("editor"), "orders.write", tenantId),
], cancellationToken);
// 不存在的授予逐项返回 SECURITY_NOTFOUND_002
public ValueTask<IReadOnlyList<PermissionGrantResult>> RevokeWriteAsync(string tenantId, CancellationToken cancellationToken)
=> grants.RevokeAsync([new PermissionGrant(GrantSubject.ForRole("editor"), "orders.write", tenantId)], cancellationToken);
public Task<IReadOnlyList<PermissionGrantRecord>> ListAsync(string userId, CancellationToken cancellationToken)
=> grants.ListAsync(GrantSubject.ForUser(userId), tenantId: null, skip: 0, take: 100, cancellationToken);
}行为与限制
- 被选为授权能力时,特性在
Configure里登记启动核对PolicyCatalog.EnforceOn,核对操作契约按名引用的策略:内核在容器建好之后、任何特性启动之前执行它,不存在(含permission:<code>的权限码未登记)即抛出SECURITY_VALIDATION_004、宿主启动失败,不论失败策略(IsolateFeature也一样,不包成KERNEL_HOSTING_002);之后登记的操作(插件握手之后)在登记时核对(ADR-0080、ADR-0085)。特性本身不实现IHostedFeature。 - 授予:一次查询取回已有行(主体种类、主体 ID、权限码、租户各取集合做
IN,再在内存里精确匹配),已存在的返回成功且不重复写,同一批里重复的项同样成功,没有的用一次AddRangeAsync写入。空集合直接返回空结果。 - 租户:全局授予的
TenantId用null;GrantAsync与RevokeAsync遇到空白租户视为编程错误,在访问数据库前抛出ArgumentException。 - 撤销:一次
RemoveRangeAsync;不存在的授予与同一批里第二次出现的同一授予返回SECURITY_NOTFOUND_002(消息键security.grant.not_found,参数subject/permission/tenant,全局时tenant为*)。 - 列表:按主体种类、主体 ID、权限码、租户排序;
subject为null不按主体过滤,tenantId为null不按租户过滤,否则只列该租户专属的行;每页最多 1000 条。 - 缓存:条目按
CacheTimeToLive过期,条目数达到CacheSize时整体清空,不逐条淘汰。PermissionGrantRecordStore写入后对涉及的每个主体调用一次Invalidate;写入在调用方事务提交后才对其他调用可见,失效与提交之间的窗口由存活时间收敛。多实例部署时其他实例的缓存同样靠存活时间收敛,需要更快生效时调小CacheTimeToLiveSeconds。 - 故障:开启、读取、提交只读工作单元时的数据库故障(连接、命令超时、事务失败)返回
SECURITY_UNAVAILABLE_001(消息键security.grant_store.unavailable,不带参数),检查器原样返回,调用以 503 结束——不会落到 403。底层原因只记警告日志,不进响应。失败不进缓存,数据库恢复后下一次判定立即重新查库。调用方取消照常抛出;容器里缺工作单元或仓储是装配错误,照常抛出(ADR-0071)。 - 已有的库按唯一索引的名字补建索引;表里有重复授予时先去重。
- 表结构由应用管理,
InitTables只用于开发与测试。唯一索引UX_PermissionGrantRecord_SuKi_SuId_Pe_TeId((SubjectKind, SubjectId, Permission, TenantId),实体上的[EntityIndex],SqlSugar 的InitTables与 EF Core 的ApplyPersistenceConventions(Database)都按它建,ADR-0072),按主体的查询走它的(SubjectKind, SubjectId)前缀;两个调用同时授予同一项时后插入的那一批返回PERSISTENCE_CONFLICT_003、调用回滚,重试即成功(已存在);列长度:SubjectKind32、SubjectId256、Permission256、TenantId128。 - 通过
Security.Conformance的PermissionGrantStoreConformance(含存储不可达用例:连接指向不存在的 SQLite 文件),并在 SQLite(Persistence.SqlSugar)上验证了实体映射。
相关
- 授权
- Security.Abstractions
- Security.InMemory
- Persistence.SqlSugar
- Persistence.EntityFrameworkCore
- 规范:RFC-0008《认证与授权》;ADR-0027
