基础授权(Aegis.Core.Authorization)
Aegis 的授权体系分两层:本组件提供 ApiAuthorize 拦截器和权限缓存,具体的权限来源(如 ESS)由子组件补齐。基础授权依赖基础认证先建立当前用户上下文,因此不能单独使用。
组件概览
| 字段 | 说明 |
|---|---|
| 组件名称 | 基础授权 |
| 真实类库 | Aegis.Core.Authorization |
| 父级组件 | Aegis.Core.Authentication |
| 子级扩展 | Aegis.Authorization.ESS 或自定义 IAuthorizationManager 实现 |
| 组件定位 | 提供授权过滤器、资源权限缓存、ApiAuthorize 特性和授权上下文抽象 |
| 引入方式 | Component.deps.json |
| 组件声明 | Authorization |
| 核心能力 | ApiAuthorize、权限缓存、资源权限抽象、当前用户扩展字典 |
| 是否可扩展 | 是 |
| 目标框架 | net8.0 |
| 注册入口 | ServiceCollectionExtensions.cs(ComponentRegister 实现) |
基础授权负责「怎么拦、拦什么、权限怎么缓存」,但不内置权限来源。要真正按资源列表做授权判断,还需要配一个 IAuthorizationManager 的具体实现(如 ESS)。
如何引入
NuGet 包
| 角色 | NuGet 包 | 是否必需 | 说明 |
|---|---|---|---|
| 认证基础层 | Aegis.Core.Authentication | 是 | 基础授权依赖当前用户上下文 |
| 授权基础层 | Aegis.Core.Authorization | 是 | 提供授权过滤和资源权限缓存 |
| 具体实现 | Aegis.Authorization.ESS 或自定义实现 | 按需 | 提供真正的权限来源 |
Component.deps.json
基础授权既有服务注册,也有中间件加载,且显式依赖 Authentication:
{
"Components": {
"Services": [
"Authentication",
"Authorization",
"EssAuthorize"
],
"Middlewares": [
"Authentication",
"Authorization",
"EssAuthorize"
]
}
}
Authentication:建立当前用户和认证链路Authorization:加载授权服务和UseAuthorization()EssAuthorize:提供真实的权限来源与客凭校验能力
只接入 Authorization 而没有 Authentication,组件顺序校验会直接失败。
配置说明
| 节点 | 类型 | 是否必填 | 说明 | 常见取值 / 示例 |
|---|---|---|---|---|
Auth:EnableAuthorization | bool | 建议是 | 是否启用授权校验;关闭后 ApiAuthorize 会直接放行 | true |
最小配置示例:
{
"Auth": {
"EnableAuthorization": true
}
}
快速接入
第一步:按顺序接入认证和授权
- 安装
Aegis.Core.Authentication - 安装
Aegis.Core.Authorization - 在
Component.deps.json中先写Authentication,再写Authorization
第二步:在接口上使用 ApiAuthorize
using Aegis.Core.Authentication;
using Aegis.Core.Authorization.Filters;
using Aegis.Core.Infrastructure.Controller;
using Microsoft.AspNetCore.Authorization;
[ApiAuthorize]
public class OrdersController : ApiControllerBase
{
[HttpGet("me")]
public object Me()
{
return CurrentUser.Value;
}
[AllowAnonymous]
[HttpGet("ping")]
public string Ping()
{
return "ok";
}
}
第三步:确认授权链路生效
- 未带 Token 访问受保护接口时返回
401 - 标记了
AllowAnonymous的接口不会被ApiAuthorize拦截 - 注册了具体授权实现后,没有权限访问时返回
403
核心能力
用 ApiAuthorize 保护接口
ApiAuthorize 是 Aegis 的授权拦截器,标记在 Controller 或 Action 上。请求到达时,它会检查当前用户是否有权访问该接口,没有权限返回 403。
[ApiAuthorize] // 整个 Controller 都需要授权
public class OrdersController : ApiControllerBase
{
[HttpGet("me")]
public object Me() => CurrentUser.Value;
[AllowAnonymous] // 单独放行匿名访问
[HttpGet("ping")]
public string Ping() => "ok";
}
不标记 ApiAuthorize 的接口不受授权管控,只要认证通过(或匿名)就能访问。
ApiAuthorize 的判断顺序固定为 5 步:
- 检查
Auth:EnableAuthorization,关闭则直接放行 - 检查是否存在
ClientAuthorization请求头,标记当前请求是否为「客凭访问」 - 检查
AllowAnonymous(Filter、EndpointMetadata、Action/Controller 特性三处),标记了则放行 - 非客凭请求确认
CurrentUser.Value是否存在、是否过期 - 取权限列表,查
Method|Path是否允许,无权限返回 403
没有具体授权实现时,基础授权只能完成「受保护接口拦截」,不能完成真正的资源权限判定。
资源权限缓存
注册了具体授权实现(如 ESS)后,ApiAuthorize 每次拦截都需要查权限列表。为了避免每个请求都打远端权限服务,基础授权默认把权限缓存到内存,20 分钟过期。同一个 Token 在缓存有效期内复用已取回的权限。
需要跨实例共享权限缓存或自定义缓存时长时,替换 IResourcePermissionManager,见 扩展与自定义。
当前用户扩展字典
扩展字典是挂在 CustomUser 上的临时存储,用来在一次请求周期内暂存业务数据。典型场景:在过滤器或中间件里提前解析好组织、数据权限范围等信息,避免 Controller 里重复查询。
using Aegis.Core.Authorization.Extensions;
// 写入(通常在过滤器或中间件里)
CurrentUser.Value.Set("org", new OrgInfo { Id = 1, Name = "技术部" });
CurrentUser.Value.Set("dataScope", "self");
// 读取(在 Controller 或 Service 里)
var org = CurrentUser.Value.Get<OrgInfo>("org");
var dataScope = CurrentUser.Value.Get<string>("dataScope");
客凭访问
当本系统需要以「客户端身份」调用其他受保护服务时,使用 ClientAuthorization 请求头携带客凭 Token。客凭请求会跳过当前用户检查(CurrentUser.Value 可能为 null),但仍走完整的权限比对流程。客凭 Token 的有效期由 IAuthorizationManager 自己管理。
客凭能力需要配合 ESS 组件使用,详见 ESS 认证鉴权。
授权拦截链路
API 参考
IAuthorizationManager
权限来源抽象。基础授权不提供默认实现,需要由 ESS 或业务自定义实现注册。
| 方法 | 参数 | 返回值 | 语义 |
|---|---|---|---|
GetResourcePermissions | string token | Task<IEnumerable<ResourcePermission>> | 按 Token 返回该 Token 能访问的资源权限列表 |
这个方法只在缓存未命中时被调用。
IResourcePermissionManager
权限缓存抽象。控制权限「怎么缓存、缓存多久、按什么 key 隔离」。
| 方法 | 参数 | 返回值 | 语义 |
|---|---|---|---|
GetResourcePermission | string resourceId, string key | ResourcePermission | 按 key 和 resourceId 取单条权限 |
HasResourcePermission | string resourceId | bool | 当前用户的权限集合中是否包含某 resourceId |
SetResourcePermissions | IEnumerable<ResourcePermission>, string key | void | 批量写入权限(覆盖式,20 分钟过期) |
GetResourcePermissions | string key | IEnumerable<ResourcePermission> | 按 key 取整组权限;缓存未命中返回 null,空集合返回空列表 |
SetResourcePermission | string resourceId, ResourcePermission | ResourcePermission | 在当前用户权限集合中追加或覆盖单条 |
RemoveResourcePermission | string resourceId | bool | 在当前用户权限集合中移除单条 |
GetResourcePermissions 返回 null 表示缓存未命中(应去 IAuthorizationManager 取权限并回填);返回空集合表示缓存已命中但该 Token 确实没有任何权限(防穿透)。
ResourcePermission
权限比对的核心数据结构。
| 字段 | 类型 | 是否参与比对 | 说明 |
|---|---|---|---|
Uri | string | 是 | 资源路径,与请求 Path 拼接后比对 |
RequestMethod | string | 是 | HTTP 方法,与请求 Method 拼接后比对 |
RequestPath | string | 否 | 死字段,当前实现不读取 |
CanAccess | bool | 是 | 是否允许访问;false 或找不到都会触发 403 |
RequestPath 是历史遗留字段,ApiAuthorize 的比对逻辑只读 Uri 和 RequestMethod。如果业务侧在 IAuthorizationManager 里只设置了 RequestPath,权限比对会失败。
扩展与自定义
| 扩展点 | 接口 | 默认实现 | 改变的行为 |
|---|---|---|---|
| 权限来源 | IAuthorizationManager | 由具体实现提供(如 ESS) | 资源权限列表的获取方式 |
| 权限缓存 | IResourcePermissionManager | DefaultResourcePermissionManager(内存,20 分钟) | 缓存位置、缓存策略、缓存 key 规则 |
| 资源管理 | IResourceManager | DefaultResourceManager(当前所有方法均抛 NotImplementedException,不可直接使用) | 资源元数据的维护 |
替换方式是在 Startup 中用 DI 覆盖默认实现,例如:
services.AddSingleton<IAuthorizationManager, 你的实现>();
IResourceManager虽然是公开扩展点,但默认实现DefaultResourceManager当前未完成(所有方法抛NotImplementedException)。ApiAuthorize的运行时不依赖IResourceManager,不实现它不影响权限拦截链路。
完整的自定义实现步骤和代码模板见 自定义认证鉴权实现指南。
边界与陷阱
过期返回 200 不是 401
user.ExpireAt 已过期时,ApiAuthorize 返回 HTTP 200 + Code = TokenExpired,而不是 401。前端拦截响应时需要同时检查 HTTP 状态码和业务 Code:
- HTTP 401 +
Code = UnAuthenticate:完全未登录 - HTTP 200 +
Code = TokenExpired:登录已过期,应触发刷新或重新登录 - HTTP 403 +
Code = NotAllowed:已登录但无权限
只按 HTTP 状态码判断会把「过期」误判为「成功」。
客凭请求跳过用户和过期检查
带 ClientAuthorization 头的请求不会检查 CurrentUser.Value 和 ExpireAt。这意味着客凭请求里 CurrentUser.Value 可能是 null,业务代码不能假设所有经过 ApiAuthorize 的请求都有当前用户。客凭 Token 的有效期由 IAuthorizationManager 自己管理。
权限缓存命中后 20 分钟内不刷新
DefaultResourcePermissionManager 的缓存时长固定 20 分钟。缓存命中期间,即使权限中心已经更新了该用户的权限,应用层也不会感知。需要立即生效的场景可以替换 IResourcePermissionManager 缩短缓存时长,或在权限变更后主动调用 RemoveResourcePermission 清除缓存。
ResourcePermission.RequestPath 是死字段
ApiAuthorize 比对时只用 Uri 和 RequestMethod。业务侧构造 ResourcePermission 时必须设置 Uri,否则权限比对会失败。
权限缓存按 token 而非用户隔离
缓存 key 是 token,同一个用户用不同 Token 登录会产生多个缓存项。短期内同一用户的权限变更需要清所有相关 Token 的缓存。
常见问题
为什么加了 Authorization 以后,接口还是没有真正按权限拦截?
基础授权只负责授权框架,不负责权限来源。要实现真正的资源权限校验,还需要接入 Aegis.Authorization.ESS 或自己的 IAuthorizationManager。
为什么 ApiAuthorize 没有拦住某个接口?
检查这几项:
Auth:EnableAuthorization是否为true- 当前接口或控制器是否标记了
AllowAnonymous Authorization是否已经加入Middlewares- 是否已经注册了具体授权实现
为什么 Authentication 和 Authorization 的顺序不能反过来?
授权判断依赖 CurrentUser。只有认证先把用户写入上下文,授权层才能继续判断是否允许访问。