基础认证(Aegis.Core.Authentication)
Aegis 的认证体系分两层:本组件提供认证管线和当前用户上下文,具体的登录逻辑(SSO/JWT/ESS)由子组件补齐。只接入 Aegis.Core.Authentication 还不能登录,必须再选一个具体实现组件。
组件概览
| 字段 | 说明 |
|---|---|
| 组件名称 | 基础认证 |
| 真实类库 | Aegis.Core.Authentication |
| 组件定位 | 提供认证管线、Token 处理中间件、CurrentUser 上下文和 IUserManager / IAuthenticationManager 抽象 |
| 子级扩展 | Aegis.Authorization.SSO、Aegis.Authorization.Jwt、Aegis.Authorization.ESS、Aegis.Authorization.RedisUserManager;内置内置用户能力(无需额外引入组件) |
| 引入方式 | Component.deps.json,可叠加手动替换 IUserManager |
| 组件声明 | Authentication |
| 核心能力 | Bearer 认证注册、Token 读取、当前用户上下文、默认内存会话管理 |
| 是否可扩展 | 是 |
| 目标框架 | net8.0 |
| 注册入口 | ServiceCollectionExtensions.cs(ComponentRegister 实现) |
如何引入
NuGet 包
| 角色 | NuGet 包 | 是否必需 | 说明 |
|---|---|---|---|
| 基础层 | Aegis.Core.Authentication | 是 | 提供认证管线和当前用户上下文 |
| 具体实现 | Aegis.Authorization.SSO / Aegis.Authorization.Jwt / Aegis.Authorization.ESS | 至少选一个 | 提供 IAuthenticationManager 的具体实现 |
Component.deps.json
Authentication 需要同时出现在 Services 和 Middlewares 中。下面示例使用 JWT 作为最小落地方案:
{
"Components": {
"Services": [
"Authentication",
"JwtAuthorize"
],
"Middlewares": [
"Authentication"
]
}
}
Authentication:注册认证服务、默认的IUserManager和 Token 处理中间件。JwtAuthorize:为基础认证补上具体的登录和 Token 校验实现。
只写 Authentication 而不引入任何具体实现,认证链路会建立,但登录和 Token 校验无法工作。
配置说明
| 节点 | 类型 | 是否必填 | 说明 | 常见取值 / 示例 |
|---|---|---|---|---|
Auth:EnableAuthentication | bool | 建议是 | 是否启用认证校验;关闭后 TokenHandler 会直接放行请求 | true |
Auth:BuiltinUsers | 数组 | 否 | 内置用户列表(固定 Token 服务凭证);不配置时零影响,详见 内置用户 | 见下方 |
最小配置示例:
{
"Auth": {
"EnableAuthentication": true
}
}
手动注入与例外情况
基础认证默认会注册内存版 DefaultUserManager。需要分布式会话时,替换 IUserManager 实现:
services.AddRedisSource<AegisRedisSource>(ConfigManager.Get<RedisOptions>("Redis"));
services.AddRedisUserManager<AegisRedisSource>();
会话存储替换的场景对照见 扩展与自定义 章节。
快速接入
以 JWT 为例的三步路径。
第一步:完成依赖和配置
- 安装
Aegis.Core.Authentication - 安装一种具体实现组件,例如
Aegis.Authorization.Jwt - 在
Component.deps.json中加入Authentication - 在配置中启用
Auth:EnableAuthentication
第二步:调用登录接口
基础认证暴露两个默认接口,委托给当前注入的 IAuthenticationManager:
POST /api/Auth/LoginPOST /api/Auth/Logout
登录请求体示例:
{
"userIdentity": "demo-user",
"password": "demo-secret",
"expiresMinutes": 30,
"userModel": {
"name": "Demo"
}
}
返回的用户结构和 Token 规则由具体实现决定。
第三步:在受保护接口里读取当前用户
using Aegis.Core.Authentication;
using Aegis.Core.Infrastructure.Controller;
using Microsoft.AspNetCore.Mvc;
public class ProfileController : ApiControllerBase
{
[HttpGet("current-user")]
public object GetCurrentUser()
{
return CurrentUser.Value;
}
}
接入正常时:登录接口返回 Token;后续请求携带 Authorization: Bearer {token} 后 CurrentUser.Value 可读取到当前用户;未标记 AllowAnonymous 的接口在缺少 Token 时返回 401。
核心能力
当前用户上下文(CurrentUser)
CurrentUser 是认证通过后,业务代码获取当前登录用户的统一入口。TokenHandler 在请求开始时把 CustomUser 写入 CurrentUser.Value,整个请求链路都能读到。
典型用法——在 Controller 或 Service 里读取当前用户:
public class OrderController : ApiControllerBase
{
[HttpGet("my-orders")]
public object GetMyOrders()
{
var user = CurrentUser.Value;
return _orderService.GetOrders(user.UserId);
}
}
CustomUser 的核心字段:
| 字段 | 含义 | 怎么用 |
|---|---|---|
UserIdentity | 登录账号,用户唯一标识 | 审计日志、按用户筛选数据 |
UserId | 业务用户 ID(需 userModel 实现 IUserIdentifiable) | 业务主键查询 |
Token | 当前 Bearer Token | 调用下游服务时透传 |
ExpireAt | 会话过期时间 | 判断是否需要刷新 |
UserInfo | 具体实现写入的用户对象(如 SSO 用户、ESS 用户) | 强转后取业务字段,各实现组件提供扩展方法(如 user.SsoUser()) |
UserType | 用户身份类型:User(真人)或 Service(机器/内置用户) | 区分真人调用和服务调用,做审计分流或权限边界控制 |
完整字段说明见 API 参考 - CustomUser。
内置用户(BuiltinUser)
内置用户是一种前置旁路机制:在主认证源(ESS/JWT/SSO)之前,先检查请求里的 Token 是否匹配一组预先配置好的固定 Token。命中后直接放行,不走主认证流程。
解决什么问题:定时任务、系统间调用、自动化测试这些场景里,调用方不是真人,没有登录态,但需要访问受保护接口。用真人登录流程(签发、刷新、过期)来处理这些机器调用既麻烦又脆弱。
怎么配置:在 appsettings.json 里声明固定 Token:
{
"Auth": {
"EnableAuthentication": true,
"BuiltinUsers": [
{
"ClientId": "scheduler-dolphin",
"Token": "svc_a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5",
"UserId": "svc-scheduler",
"UserName": "调度平台服务账号",
"UserCode": "svc-scheduler"
}
]
}
}
配置项说明:
| 字段 | 必填 | 说明 |
|---|---|---|
ClientId | 是 | 逻辑标识,便于管理和审计。命中后作为 CustomUser.UserIdentity |
Token | 是 | 固定凭证,必须为高熵随机串(推荐 svc_ + 32 字节 base64),且全局唯一 |
UserId | 是 | 业务用户 ID,写入 CustomUser.UserId,业务代码常读取此字段做查询 |
UserName | 是 | 业务用户名,写入 CustomUser.UserName |
UserCode | 是 | 业务用户编码,写入 CustomUser.UserCode |
生成高熵 Token:
echo "svc_$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)"
怎么用:调用方携带配置的 Token 发请求,CurrentUser.Value 就会被写入,UserType 为 Service,ExpireAt 为 DateTime.MaxValue(永不过期):
var user = CurrentUser.Value;
// user.UserIdentity → "scheduler-dolphin"
// user.UserId → "svc-scheduler"
// user.UserName → "调度平台服务账号"
// user.UserType → UserType.Service
// user.ExpireAt → DateTime.MaxValue
业务代码用 UserType 区分真人和服务调用,做审计分流或权限边界控制:
if (user.UserType == UserType.Service)
{
_logger.LogInformation("服务调用: {ClientId}", user.UserIdentity);
}
不配置 Auth:BuiltinUsers 时,内置用户能力完全不生效,对存量项目零影响。
内置用户获得全权访问:命中后直接短路管道,不经过 ApiAuthorize 的细粒度授权。持有内置 Token 的调用方可以访问任何未被显式拒绝的端点。如果需要限制服务账号的访问范围,在 Controller 内部校验:
if (user.UserType == UserType.Service
&& !AllowedServiceClients.Contains(user.UserIdentity))
{
return Forbid();
}
其他安全约束:
- Token 一旦泄露可全权调用系统,必须使用密码学安全的随机源生成,禁止用
test、123456、项目名等可猜测值 - 不同环境(开发/测试/生产)必须使用不同的 Token,禁止复用
appsettings.json中的 Token 属于敏感信息,不应提交到 Git,生产环境建议通过环境变量或密钥管理服务注入
默认实现在启动时从配置一次性构建索引,运行时不刷新。撤销某个内置用户需要删除配置条目并重启应用。如果对撤销时效有要求,替换 IBuiltinUserStore 为动态实现(如从数据库读取,支持实时撤销)。替换时必须保持 UserType = Service 和 ExpireAt = DateTime.MaxValue 两个安全语义。
不配置 Auth:BuiltinUsers 时,内置用户能力完全不生效,对存量项目零影响。
默认会话管理
不替换 IUserManager 时,系统使用内存版 DefaultUserManager:数据保存在内存,适合单实例和本地调试,重启后会话丢失,多实例间不共享。需要跨实例共享会话时,替换为 Redis 实现,见 扩展与自定义。
认证链路
认证链路从 HTTP 请求进入开始,到响应返回结束:
CurrentUser 的生命周期
CurrentUser 基于 AsyncLocal<CustomUser>,请求开始时由 TokenHandler 写入,整个请求链路(包括 await 之后)都能读到同一个值,请求结束后由 .NET 运行时自动清理。set 访问器是 internal,业务代码只能读。
后台任务、Task.Run、Channel 消费者等脱离请求上下文的场景里,CurrentUser.Value 会是 null。
API 参考
IAuthenticationManager
具体登录方案的抽象。基础认证不提供实现,需要由 SSO / JWT / ESS 等组件注册。
| 方法 | 参数 | 返回值 | 语义 |
|---|---|---|---|
Login | LoginInfo loginInfo | Task<CustomUser> | 登录并返回用户上下文,失败抛 AuthenticationException |
Logout | string token | Task | 注销指定 Token 对应的会话 |
CheckToken | string token | Task<CustomUser?> | 校验 Token 并返回用户;Token 无效时返回 null(不抛异常) |
CheckToken 返回 null 不代表「请求被拒绝」,而是「当前请求没有有效登录态」。是否拒绝由 TokenHandler 根据 AllowAnonymous 决定。
IUserManager
会话存储抽象。
| 方法 | 参数 | 返回值 | 语义 |
|---|---|---|---|
GetUser | string identity | CustomUser | 按 identity 取用户;不存在返回 null |
AddUser | string token, string identity, object userModel, DateTime expiredAt | CustomUser | 添加或覆盖会话,返回构造好的 CustomUser |
UpdateUser | CustomUser user | void | 用完整 CustomUser 覆盖缓存,用于持久化 RefreshToken、ExtendInfo 等后续更新 |
RemoveUser | string identity | bool | 移除会话,返回是否移除成功 |
AddUser 接收原始参数由实现构造 CustomUser,UpdateUser 接收已经构造好的完整对象。
ITokenManager
Token 生成与查询的抽象,主要用于业务层通过用户标识取回 Token(例如主动通知场景)。
| 方法 | 参数 | 返回值 |
|---|---|---|
GetToken | string identity | string |
GetTokenAsync | string identity | Task<string> |
ICustomerUserProvider
登录前的用户信息补全扩展点。注册后,登录链路可以在生成 Token 前从业务系统补充用户数据。基础认证不强制要求注册。
| 方法 | 参数 | 返回值 |
|---|---|---|
GetUser | LoginInfo loginInfo | object |
CustomUser
写入 CurrentUser.Value 的用户上下文对象。
| 字段 | 类型 | 由谁填充 | 说明 |
|---|---|---|---|
UserIdentity | string | IUserManager.AddUser 的 identity 参数 | 用户唯一标识,通常是登录账号 |
Token | string | IAuthenticationManager.Login | 当前 Bearer Token |
RefreshToken | string | 具体实现(如 ESS) | 刷新 Token,可选 |
ExpireAt | DateTime | IUserManager.AddUser 的 expiredAt 参数 | 会话过期时间 |
UserId | string | DefaultUserManager 从 IUserIdentifiable 提取 | 业务用户 ID |
UserCode | string | DefaultUserManager 从 IUserIdentifiable 提取 | 业务用户编码 |
UserName | string | DefaultUserManager 从 IUserIdentifiable 提取 | 业务用户姓名 |
UserInfo | object | IUserManager.AddUser 的 userModel 参数 | 具体实现定义的用户模型,业务侧需要自己强转 |
ExtendInfo | object | DefaultUserManager 从 userModel.ExtendInfo 反射提取 | 扩展返回信息 |
UserType | UserType(enum) | 默认 User;内置用户为 Service | 用户身份类型,区分真人/机器调用,详见 内置用户 |
UserInfo 和 ExtendInfo 都是 object 类型,框架不做类型约束。各实现组件会提供扩展方法做强转,例如 SSO 提供 user.SsoUser()。
LoginInfo
登录请求参数。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
UserIdentity | string | - | 登录账号 |
Password | string | - | 密码,JWT 实现中会参与 Token 生成 |
ExtendInfo | string | - | 扩展信息,如 ESS 的回调地址 |
UserModel | object | - | 业务自定义的用户对象,可实现 IUserIdentifiable |
CodeVerifier | string | - | PKCE 校验码,ESS 场景使用 |
ExpiresMinutes | int | 30 | 会话有效期(分钟) |
IUserIdentifiable
可选实现的用户标识接口。userModel 实现此接口后,DefaultUserManager 会自动提取三个字段写入 CustomUser。
| 属性 | 类型 |
|---|---|
UserId | string |
UserCode | string |
UserName | string |
默认接口
AuthController 路由前缀 api/Auth。
| 路由 | 方法 | 请求体 | 响应体 | 是否 AllowAnonymous |
|---|---|---|---|---|
POST /api/Auth/Login | Login | LoginInfo | EntityResponse<CustomUser> | 是 |
POST /api/Auth/Logout | Logout | 无 | 无(Task) | 否 |
Logout 没有标记 AllowAnonymous,调用方必须先通过认证才能登出。
扩展与自定义
| 扩展点 | 接口 | 默认实现 | 改变的行为 |
|---|---|---|---|
| 认证逻辑 | IAuthenticationManager | 由具体实现组件提供(SSO/JWT/ESS) | 登录、登出、Token 校验的整体逻辑 |
| 会话存储 | IUserManager | DefaultUserManager(内存) | 会话保存位置、字段填充策略 |
| 内置用户识别 | IBuiltinUserStore | BuiltinUserStore(配置驱动) | 主认证源之前的固定 Token 旁路,详见 内置用户 |
| Token 查询 | ITokenManager | 由具体实现提供 | Token 的生成与按 identity 反查 |
| 登录前用户补全 | ICustomerUserProvider | 无默认实现 | 在生成 Token 前从业务系统加载用户数据 |
替换方式是在 Startup 中用 DI 覆盖默认实现,例如:
services.AddSingleton<IUserManager, 你的实现>();
会话存储替换场景对照:
| 场景 | 推荐实现 |
|---|---|
| 单机调试 | DefaultUserManager(默认,内存存储,重启会丢) |
| 多实例部署 | RedisUserManager<T>(跨实例共享会话) |
| ESS 场景 | EssRedisUserManager<T>(键策略贴近 ESS 流程) |
| 自定义键策略 | 实现 IUserManager(按设备、业务维度等) |
完整的自定义实现步骤和代码模板见 自定义认证鉴权实现指南。
边界与陷阱
CurrentUser.Value 在匿名端点上可能为 null
AllowAnonymous 端点不强制要求 Token,校验失败时 CurrentUser.Value 会被设为 null。如果在匿名端点里需要访问当前用户,必须做 null 检查:
[AllowAnonymous]
[HttpGet("optional-user")]
public object GetOptionalUser()
{
var user = CurrentUser.Value;
return user == null ? new { logged = false } : new { logged = true, user.UserIdentity };
}
UserInfo 和 ExtendInfo 是 object 类型
框架不约束 UserInfo 的具体类型,业务侧拿到 CurrentUser.Value.UserInfo 后需要自己强转。推荐使用各实现组件提供的扩展方法(如 user.SsoUser()),避免在业务代码里到处写强转。
AuthController.Logout 需要先登录才能调用
Logout 接口没有 AllowAnonymous,Token 已过期时调用它会先返回 401,无法完成登出。如果业务上需要「过期也能登出」:
- 自己实现
AuthController并在Logout上加AllowAnonymous - 在业务层直接调用
IAuthenticationManager.Logout
DEBUG 模式自动补 Bearer 前缀
TokenHandler 在 #if DEBUG 编译条件下会自动给缺少前缀的 Token 补 Bearer 。本地调试时 Postman 不带前缀也能通过认证,但生产环境会返回 401。排查时优先确认请求头格式是否为 Authorization: Bearer {token}。
单实例重启会话全丢
DefaultUserManager 是内存实现,应用重启后会话全部丢失。多实例部署时每个实例的会话也不共享,需要切换到 RedisUserManager<T> 或其他持久化实现。
常见问题
为什么只接了 Authentication,登录接口还是不能用?
基础认证只提供认证管线,不提供具体登录逻辑。要让 /api/Auth/Login 工作,还必须注册一个具体实现(SSO/JWT/ESS)。
为什么接口已经加了 Token,但 CurrentUser.Value 还是空?
检查这几项:
Auth:EnableAuthentication是否为trueAuthentication是否已经加入Middlewares- 是否已经注册了
IAuthenticationManager - 请求头是否使用了
Authorization: Bearer {token}格式
为什么应用一重启,用户就全部掉线了?
默认的 DefaultUserManager 是内存实现。需要跨实例、跨重启保留会话时,改用 Redis 或其他持久化实现。