跳到主要内容
版本:3.1

基础认证(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:EnableAuthenticationbool建议是是否启用认证校验;关闭后 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/Login
  • POST /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 等组件注册。

方法参数返回值语义
LoginLoginInfo loginInfoTask<CustomUser>登录并返回用户上下文,失败抛 AuthenticationException
Logoutstring tokenTask注销指定 Token 对应的会话
CheckTokenstring tokenTask<CustomUser?>校验 Token 并返回用户;Token 无效时返回 null(不抛异常)

CheckToken 返回 null 不代表「请求被拒绝」,而是「当前请求没有有效登录态」。是否拒绝由 TokenHandler 根据 AllowAnonymous 决定。

IUserManager​

会话存储抽象。

方法参数返回值语义
GetUserstring identityCustomUser按 identity 取用户;不存在返回 null
AddUserstring token, string identity, object userModel, DateTime expiredAtCustomUser添加或覆盖会话,返回构造好的 CustomUser
UpdateUserCustomUser uservoid用完整 CustomUser 覆盖缓存,用于持久化 RefreshToken、ExtendInfo 等后续更新
RemoveUserstring identitybool移除会话,返回是否移除成功

AddUser 接收原始参数由实现构造 CustomUser,UpdateUser 接收已经构造好的完整对象。

ITokenManager​

Token 生成与查询的抽象,主要用于业务层通过用户标识取回 Token(例如主动通知场景)。

方法参数返回值
GetTokenstring identitystring
GetTokenAsyncstring identityTask<string>

ICustomerUserProvider​

登录前的用户信息补全扩展点。注册后,登录链路可以在生成 Token 前从业务系统补充用户数据。基础认证不强制要求注册。

方法参数返回值
GetUserLoginInfo loginInfoobject

CustomUser​

写入 CurrentUser.Value 的用户上下文对象。

字段类型由谁填充说明
UserIdentitystringIUserManager.AddUser 的 identity 参数用户唯一标识,通常是登录账号
TokenstringIAuthenticationManager.Login当前 Bearer Token
RefreshTokenstring具体实现(如 ESS)刷新 Token,可选
ExpireAtDateTimeIUserManager.AddUser 的 expiredAt 参数会话过期时间
UserIdstringDefaultUserManager 从 IUserIdentifiable 提取业务用户 ID
UserCodestringDefaultUserManager 从 IUserIdentifiable 提取业务用户编码
UserNamestringDefaultUserManager 从 IUserIdentifiable 提取业务用户姓名
UserInfoobjectIUserManager.AddUser 的 userModel 参数具体实现定义的用户模型,业务侧需要自己强转
ExtendInfoobjectDefaultUserManager 从 userModel.ExtendInfo 反射提取扩展返回信息
UserTypeUserType(enum)默认 User;内置用户为 Service用户身份类型,区分真人/机器调用,详见 内置用户

UserInfo 和 ExtendInfo 都是 object 类型,框架不做类型约束。各实现组件会提供扩展方法做强转,例如 SSO 提供 user.SsoUser()。

LoginInfo​

登录请求参数。

字段类型默认值说明
UserIdentitystring-登录账号
Passwordstring-密码,JWT 实现中会参与 Token 生成
ExtendInfostring-扩展信息,如 ESS 的回调地址
UserModelobject-业务自定义的用户对象,可实现 IUserIdentifiable
CodeVerifierstring-PKCE 校验码,ESS 场景使用
ExpiresMinutesint30会话有效期(分钟)

IUserIdentifiable​

可选实现的用户标识接口。userModel 实现此接口后,DefaultUserManager 会自动提取三个字段写入 CustomUser。

属性类型
UserIdstring
UserCodestring
UserNamestring

默认接口​

AuthController 路由前缀 api/Auth。

路由方法请求体响应体是否 AllowAnonymous
POST /api/Auth/LoginLoginLoginInfoEntityResponse<CustomUser>是
POST /api/Auth/LogoutLogout无无(Task)否

Logout 没有标记 AllowAnonymous,调用方必须先通过认证才能登出。

扩展与自定义​

扩展点接口默认实现改变的行为
认证逻辑IAuthenticationManager由具体实现组件提供(SSO/JWT/ESS)登录、登出、Token 校验的整体逻辑
会话存储IUserManagerDefaultUserManager(内存)会话保存位置、字段填充策略
内置用户识别IBuiltinUserStoreBuiltinUserStore(配置驱动)主认证源之前的固定 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 是否为 true
  • Authentication 是否已经加入 Middlewares
  • 是否已经注册了 IAuthenticationManager
  • 请求头是否使用了 Authorization: Bearer {token} 格式

为什么应用一重启,用户就全部掉线了?​

默认的 DefaultUserManager 是内存实现。需要跨实例、跨重启保留会话时,改用 Redis 或其他持久化实现。