XiHan.Framework.Security.Abstractions
安全的抽象包:凭据与认证处理器、授权要求与策略目录、RBAC 授予存储与检查器、收窄过滤器与授予来源、Authenticate / Authorize 阶段中间件,以及条件引擎与 IP 归属地契约。能力 security.authentication@1、security.authorization@1、security.ip-region@1 由安全 Provider 提供。
| 项 | 内容 |
|---|---|
| 包 ID | XiHan.Framework.Security.Abstractions |
| 环层 | Ring 2(抽象) |
| 能力 | 定义 security.authentication@1、security.authorization@1、security.ip-region@1(SecurityCapabilities),本包不提供 |
| 特性入口 | — |
| 配置节 | XiHan:Security(SecurityOptions,由 Provider 读取) |
| 依赖 | XiHan.Framework.Kernel、XiHan.Framework.Localization.Abstractions、Microsoft.Extensions.Configuration.Abstractions、Microsoft.Extensions.DependencyInjection.Abstractions |
| AOT | 兼容 |
| 稳定性 | Stable;Conditions 与 Network 命名空间为 Preview |
概述
主体、租户、授权请求、策略接口与权限登记这些原语在内核的 XiHan.Framework.Kernel.Security 命名空间(Principal、SecurityContext、TenantContext、AuthorizationRequest、AuthorizationResult、IPolicy、PermissionRegistry、PermissionDefinition、ResourceName),本包在其上定义认证与授权的组件。
认证:宿主把协议凭据放进调用上下文,AuthenticationMiddleware 交给方案匹配的 IAuthenticationHandler,成功写入主体,失败以结果返回。授权:调用带 AuthorizationRequirement 时,AuthorizationMiddleware 按名在 PolicyCatalog 取策略判定;permission:<code> 在权限码已登记时映射为 PermissionPolicy,由 PermissionChecker 按用户与角色授予之并集判定,再经授予来源补充、经过滤器收窄。认证与授权失败都以 Result 返回,宿主翻译成 401 / 403;处理器内部的基础设施故障抛异常。
条件引擎(Conditions)与 IP 归属地契约(Network)不接入任何管道阶段,应用在自己的策略、过滤器或中间件里使用。
何时使用
- 写自己的认证处理器、授权策略、授予存储、收窄过滤器或授予来源时引用本包。
- 使用条件引擎或 IP 归属地契约时引用本包。
- 只是启用认证与授权时直接装 Security.InMemory、Security.Jwt、Security.OpenIdConnect 或 Security.Database,它们会带上本包。
安装与启用
dotnet add package XiHan.Framework.Security.Abstractions --prerelease当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。本包没有特性入口,Profiles.Standard 与 Profiles.Full 已经引用它。
配置
SecurityOptions.FromConfiguration 读取 XiHan:Security 下的两个键:
| 键 | 类型 | 缺省 | 说明 |
|---|---|---|---|
RequireAuthentication | bool | false | 为真时匿名调用在 Authenticate 阶段被拒绝(SECURITY_DENIED_001) |
RoleClaim | string | role | 承载角色的声明类型,不能为空 |
{
"XiHan": {
"Security": { "RequireAuthentication": false, "RoleClaim": "role" }
}
}多个安全 Provider 同时装配时,SecurityOptions 只注册一次。
主要类型
| 类型 | 说明 |
|---|---|
Authentication.AuthenticationCredential | 凭据:Scheme(不区分大小写)与 Value;TryParse("<scheme> <value>");ToString() 隐藏值 |
Authentication.IAuthenticationHandler | 某一方案的处理器:Scheme、AuthenticateAsync → Result<Principal> |
Authentication.AuthenticationMiddleware | Authenticate 阶段的中间件 |
Authorization.AuthorizationRequirement | 授权要求:Policy、Resource;Authenticated、Permission(code, resource?) |
Authorization.PolicyCatalog | 策略目录:Find(name)、Names;策略名重复在构造时抛出 ArgumentException |
Authorization.AuthenticatedPolicy | 策略 authenticated |
Authorization.PermissionPolicy | 权限策略:Prefix(permission:)、NameFor(code)、CodeOf(name) |
Authorization.AuthorizationMiddleware | Authorize 阶段的中间件;策略判定失败以该失败结束调用(ADR-0071) |
Permissions.GrantSubject | 授予的主体:ForUser(id)、ForRole(name),种类常量 User、Role |
Permissions.PermissionGrant | 一条授予:主体、权限码(All = *)、租户(null 为全局) |
Permissions.IPermissionGrantStore | 授予存储:GetGrantsAsync(subject, tenantId) → Result<IReadOnlyCollection<string>>,全局授予加该租户的授予;存储不可达是 SECURITY_UNAVAILABLE_001,不是空集合 |
Permissions.IPermissionChecker / PermissionChecker | RBAC 检查器:HasPermissionAsync → Result<bool>,存储、来源、过滤器的失败原样返回 |
Permissions.IPermissionFilter | 授予命中后的收窄(ADR-0039):AllowsAsync → Result<bool> |
Permissions.IPermissionGrantSource | 存储之外的额外授予(ADR-0045):GetGrantsAsync → Result<IReadOnlyCollection<string>> |
SecurityInvocationExtensions | SetCredential / GetCredential、RequireAuthorization / GetAuthorizationRequirement |
SecurityOptions | 选项,见上表 |
Conditions.* | 条件引擎(ADR-0046):ConditionSet / ConditionGroup / ConditionClause、ConditionOperator、ConditionValueKind、ConditionAttributeDescriptor、IConditionAttributeRegistry、IConditionCompiler、IConditionEvaluator、IConditionAttributeSource、DictionaryConditionAttributeSource、MemoizingConditionAttributeSource、ConditionOperatorRules、AddConditionAttributes |
Network.IIpRegionResolver / IpRegion | IP 归属地(ADR-0047):Resolve(IPAddress) → Maybe<IpRegion> |
SecurityCapabilities / SecurityErrors / SecurityMessages | 能力 ID、错误码与消息键常量,消息目录 SecurityMessages.Catalog |
使用示例
自定义认证处理器,例如服务间调用用的共享令牌方案:
using System.Security.Cryptography;
using System.Text;
using XiHan.Framework.Kernel.Security;
using XiHan.Framework.Primitives;
using XiHan.Framework.Security.Abstractions;
using XiHan.Framework.Security.Abstractions.Authentication;
public sealed class ServiceTokenHandler : IAuthenticationHandler
{
// 开发值;真实令牌从密钥库读取
private static readonly byte[] _expected = Encoding.UTF8.GetBytes("dev-service-token-0001");
public string Scheme => "Service";
public ValueTask<Result<Principal>> AuthenticateAsync(AuthenticationCredential credential, InvocationContext context, CancellationToken cancellationToken)
{
var presented = Encoding.UTF8.GetBytes(credential.Value);
// 常量时间比较,耗时不随匹配位置泄露信息
var valid = presented.Length == _expected.Length && CryptographicOperations.FixedTimeEquals(presented, _expected);
return ValueTask.FromResult(valid
? Result.Success(new Principal("svc-billing", Scheme) { Claims = [new("role", "service")] })
: Result.Failure<Principal>(Error.Denied(SecurityErrors.InvalidCredential, SecurityMessages.InvalidToken)));
}
}在特性里登记 context.Services.AddSingleton<IAuthenticationHandler, ServiceTokenHandler>()。认证中间件由认证 Provider 加进管道;方案名不与已有处理器重名时,Authorization: Service <token> 就交给它。
在 Authorize 阶段之前的中间件里设置授权要求,例如让没有写授权要求的操作至少要求登录:
using XiHan.Framework.Kernel.Operations;
using XiHan.Framework.Kernel.Pipeline;
using XiHan.Framework.Primitives;
using XiHan.Framework.Security.Abstractions;
using XiHan.Framework.Security.Abstractions.Authorization;
public sealed class AuthenticatedByDefaultMiddleware : IPipelineMiddleware
{
public ValueTask InvokeAsync(InvocationContext context, PipelineStep nextStep)
{
// 端点没挂要求、契约也没写权限或策略时才补;显式要求优先于契约推导
if (!context.GetAuthorizationRequirement().HasValue
&& context.GetOperation().TryGetValue(out var operation)
&& operation.Contract is { Permission: null, Policy: null })
{
context.RequireAuthorization(AuthorizationRequirement.Authenticated);
}
return nextStep(context);
}
}在特性里加到 Authorize 之前的阶段:context.AddMiddleware<AuthenticatedByDefaultMiddleware>(PipelineStage.Tenant)。
收窄过滤器、授予来源、自定义策略与条件引擎的示例见授权。
行为与限制
- 认证:没有凭据保持匿名;方案找不到返回
SECURITY_VALIDATION_001;认证成功保留已有租户;RequireAuthentication在认证之后检查。 - 策略查找:注册的策略按名区分大小写匹配;
permission:<code>只为PermissionRegistry里登记过的权限码创建;找不到返回SECURITY_NOTFOUND_001,宁可拒绝不可放行。操作契约的权限码在装配时由内核核对(KERNEL_VALIDATION_005),契约的Policy在授权能力启动时经PolicyCatalog.EnforceOn核对、端点元数据在UseXiHan()建请求管道时核对(SECURITY_VALIDATION_004,启动失败,ADR-0080),这条路径只剩启动时看不到的要求(RequireAuthorization显式写入的、启动之后才加进来的端点)。 PolicyCatalog.EnforceOn(OperationRegistry):核对已登记的全部操作契约的Policy都在目录里(permission:<code>要求权限码已登记),不在即抛出SECURITY_VALIDATION_004(FrameworkException,一次列出全部);同时经OperationRegistry.Enforce挂到登记表上,此后登记的操作(插件握手之后)引用了不存在的策略,登记时返回同一错误码(消息键security.policy.operation_unregistered,参数id、owner、policy),整组不登记。security.authorization@1的 Provider 被选中时在Configure里经FeatureContext.AddStartupCheck把它登记为启动核对(容器建好之后、任何特性启动之前执行,不论失败策略,抛出即终止启动,ADR-0085);Security.InMemory与Security.Database被选为授权能力时这样登记。- 权限判定:匿名主体不持有任何权限,不访问存储;授予按序数精确匹配或
*;顺序是用户授予 → 角色授予 → 授予来源 → 过滤器,命中即止。存储、来源或过滤器返回的失败原样返回(不当作不持有,ADR-0071),抛出的异常照常传播。替换IPermissionChecker的实现须同样并上全部来源并施加全部过滤器。 PermissionChecker的构造函数接收全部登记的IPermissionFilter与IPermissionGrantSource(可为空集合),集合里出现null抛出ArgumentException。- 失败到 HTTP 的映射(ADR-0042、ADR-0071):
SECURITY_DENIED_001与SECURITY_DENIED_003是 401 并带WWW-Authenticate(Bearer 被拒时为Bearer error="invalid_token"),其余DENIED是 403,SECURITY_UNAVAILABLE_001是 503,任何一环读不到都不能落到 403;认证处理器内部的基础设施失败抛异常。 - 收窄过滤器与授予来源:过滤器收到的是具体权限码,持
*也会被收窄;匿名与未命中授予不调用过滤器;收窄拒绝时权限策略返回SECURITY_DENIED_004。授予来源在用户与角色的存储授予都没命中时按登记顺序逐个查询,任一命中即算授予命中,之后照常经全部过滤器;匿名不查询。两者都按单例登记(如services.AddSingleton<IPermissionFilter, EditionPermissionFilter>()),用于套餐白名单、显式拒绝、委托到期、角色继承、限时委托。 - 条件引擎(ADR-0046):
ConditionSet/ConditionGroup/ConditionClause,组内且、组间或;十五个运算符与七种值种类(整数值是持久化契约),ConditionOperatorRules给出搭配规则;属性按精确名、别名、最长前缀的族解析;编译错误是SECURITY_VALIDATION_002(消息键说明原因,出错子句保留);求值结果为命中、不命中、无法判定或待定,说明模式逐组逐子句;属性来源IConditionAttributeSource由应用提供,自带DictionaryConditionAttributeSource(试算)与MemoizingConditionAttributeSource;AddConditionAttributes(...)登记属性并确保三项单例服务。 IIpRegionResolver.Resolve(IPAddress)对回环、私有网段与查不到返回无值;不装 Provider 时容器里没有本服务,调用方把归属地当作取不到。- 条件引擎的登记表在首次解析时建好,重名与别名冲突即抛出;
MemoizingConditionAttributeSource按单次调用使用,不是线程安全的。
| 错误码 | 含义 |
|---|---|
SECURITY_DENIED_001 | 未认证 |
SECURITY_DENIED_002 | 策略拒绝 |
SECURITY_DENIED_003 | 凭据无效、过期或签名不符 |
SECURITY_DENIED_004 | 缺少权限 |
SECURITY_VALIDATION_001 | 不支持的认证方案 |
SECURITY_VALIDATION_002 | 条件无效(属性未登记、运算符与值种类不搭配、条件值解析不了) |
SECURITY_VALIDATION_003 | 条件属性的值不是登记的值种类 |
SECURITY_VALIDATION_004 | 按名引用的策略不存在:操作契约的 Policy(授权能力的启动核对里,不论失败策略;之后登记的在登记时)、端点元数据(UseXiHan() 建请求管道时)引用了未注册的策略或未登记的权限码;启动失败 |
SECURITY_NOTFOUND_001 | 调用时才看到的要求(显式写入的、启动之后加进来的端点)引用的策略不存在,或其中 permission:<code> 的权限码未登记 |
SECURITY_NOTFOUND_002 | 要撤销的授予不存在 |
SECURITY_NOTFOUND_003 | 取不到条件属性的值 |
SECURITY_UNAVAILABLE_001 | 身份或授予存储不可达:授予存储、授予来源或收窄过滤器读不到数据(HTTP 503,不当作不持有) |
SECURITY_UNAVAILABLE_002 | 身份或授予存储容量已满 |
SECURITY_UNAVAILABLE_003 | 签发者的发现文档或 JWKS 不可达且没有缓存 |
SECURITY_UNAVAILABLE_004 | 条件属性暂不可得 |
消息键(security.*)在本包的 Localization/<区域>.json 里有 10 种区域的文案,Provider 装配时注册 SecurityMessages.Catalog。
相关
- 认证
- 授权
- Security.InMemory
- Security.Database
- Kernel 内核
- Conformance 一致性套件
- 规范:RFC-0008《认证与授权》;ADR-0039、ADR-0042、ADR-0045、ADR-0046、ADR-0047
