跳到主要内容
版本:3.1

基础认证(Aegis.Core.Authentication)

Aegis 的认证体系分两层:本组件提供认证管线和当前用户上下文,具体的登录逻辑(SSO/JWT/ESS)由子组件补齐。只接入 Aegis.Core.Authentication 还不能登录,必须再选一个具体实现组件。

组件概览

字段说明
组件名称基础认证
真实类库Aegis.Core.Authentication
组件定位提供认证管线、Token 处理中间件、CurrentUser 上下文和 IUserManager / IAuthenticationManager 抽象
子级扩展Aegis.Authorization.SSOAegis.Authorization.JwtAegis.Authorization.ESSAegis.Authorization.RedisUserManager;内置内置用户能力(无需额外引入组件)
引入方式Component.deps.json,可叠加手动替换 IUserManager
组件声明Authentication
核心能力Bearer 认证注册、Token 读取、当前用户上下文、默认内存会话管理
是否可扩展
目标框架net8.0
注册入口ServiceCollectionExtensions.csComponentRegister 实现)

如何引入

NuGet 包

角色NuGet 包是否必需说明
基础层Aegis.Core.Authentication提供认证管线和当前用户上下文
具体实现Aegis.Authorization.SSO / Aegis.Authorization.Jwt / Aegis.Authorization.ESS至少选一个提供 IAuthenticationManager 的具体实现

Component.deps.json

Authentication 需要同时出现在 ServicesMiddlewares 中。下面示例使用 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 就会被写入,UserTypeServiceExpireAtDateTime.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 一旦泄露可全权调用系统,必须使用密码学安全的随机源生成,禁止用 test123456、项目名等可猜测值
  • 不同环境(开发/测试/生产)必须使用不同的 Token,禁止复用
  • appsettings.json 中的 Token 属于敏感信息,不应提交到 Git,生产环境建议通过环境变量或密钥管理服务注入
不可撤销,只能改配置重启

默认实现在启动时从配置一次性构建索引,运行时不刷新。撤销某个内置用户需要删除配置条目并重启应用。如果对撤销时效有要求,替换 IBuiltinUserStore 为动态实现(如从数据库读取,支持实时撤销)。替换时必须保持 UserType = ServiceExpireAt = DateTime.MaxValue 两个安全语义。

不配置 Auth:BuiltinUsers 时,内置用户能力完全不生效,对存量项目零影响。

默认会话管理

不替换 IUserManager 时,系统使用内存版 DefaultUserManager:数据保存在内存,适合单实例和本地调试,重启后会话丢失,多实例间不共享。需要跨实例共享会话时,替换为 Redis 实现,见 扩展与自定义

认证链路

认证链路从 HTTP 请求进入开始,到响应返回结束:

CurrentUser 的生命周期

CurrentUser 基于 AsyncLocal<CustomUser>,请求开始时由 TokenHandler 写入,整个请求链路(包括 await 之后)都能读到同一个值,请求结束后由 .NET 运行时自动清理。set 访问器是 internal,业务代码只能读。

后台任务、Task.RunChannel 消费者等脱离请求上下文的场景里,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 覆盖缓存,用于持久化 RefreshTokenExtendInfo 等后续更新
RemoveUserstring identitybool移除会话,返回是否移除成功

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

ITokenManager

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

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

ICustomerUserProvider

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

方法参数返回值
GetUserLoginInfo loginInfoobject

CustomUser

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

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

UserInfoExtendInfo 都是 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 };
}

UserInfoExtendInfo 是 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 或其他持久化实现。