Auth 认证中心模块
Auth 是仓库 auth/ 下可独立运行的 OAuth2/OpenID Connect 授权中心,目标框架为 net10.0,当前引用 OpenIddict.AspNetCore 7.5.0。它支持授权码流、PKCE 和刷新令牌,不属于主 Host 的 /kit_api 路由空间。
本地 launch profile 固定监听:
- HTTPS:
https://localhost:7005 - HTTP:
http://localhost:5005
功能概览
Auth 模块基于 OpenIddict 实现 OAuth2/OpenID Connect 认证服务,提供:
| 功能域 | 能力 |
|---|---|
| 协议端点 | /connect/authorize、/connect/token、/connect/userinfo |
| 交互页面 | /connect/login、/connect/consent |
| 会话登出 | POST /connect/logout(自定义 Cookie 登出,不是已配置的 end-session endpoint) |
| 认证流程 | 授权码流、PKCE、刷新令牌 |
| 应用管理 | OAuth2 客户端应用 CRUD、Scope 管理、Token 管理 |
| 声明映射 | 动态声明映射(数据库配置)、默认 Scope(openid/profile/email/roles) |
| 安全 | 密码策略(5 次锁定)、证书签名、Token 撤销与清理 |
| 启动引导 | 自动创建 Scope 和应用、非破坏性合并 |
快速开始
1. 配置数据库
{
"ConnectionStrings": {
"DefaultDB": "Sqlite",
"MySql": "Server=localhost;Port=3306;Database=freekit;User ID=<DB_USER>;Password=<DB_PASSWORD>;",
"Sqlite": "Data Source=freekit.auth.db"
}
}
AddAuthPersistence(...) 把 DefaultDB 解析为 FreeSql DataType,再读取同名连接串。建议使用 Sqlite / MySql 这类枚举名称,避免数字值难以审查。
2. 配置证书
{
"Security": {
"OpenIddict": {
"Certificates": {
"SigningCertificatePath": "certs/signing.pfx",
"SigningCertificatePassword": "<SIGNING_CERT_PASSWORD>",
"EncryptionCertificatePath": "certs/encryption.pfx",
"EncryptionCertificatePassword": "<ENCRYPTION_CERT_PASSWORD>"
}
}
}
}
开发环境在证书文件不存在时回退到开发证书。生产部署必须挂载持久化证书;否则重启或多实例之间无法稳定验证已签发内容。
3. 启动服务
dotnet run --project auth/src/FreeKit.Auth.Host --launch-profile https-dev
启动后访问 https://localhost:7005/connect/login;Discovery 位于 https://localhost:7005/.well-known/openid-configuration,Swagger 位于 https://localhost:7005/swagger。
架构概览
OAuth2 端点
/connect/authorize
OAuth2 授权端点,支持授权码流。
| 参数 | 说明 |
|---|---|
client_id | 客户端 ID |
redirect_uri | 回调地址 |
response_type | 响应类型(code) |
scope | 请求的 Scope |
code_challenge | PKCE 挑战 |
code_challenge_method | 挑战方法(S256) |
/connect/token
令牌端点,支持授权码和刷新令牌。
| 参数 | 说明 |
|---|---|
grant_type | 授权类型 |
code | 授权码 |
redirect_uri | 回调地址 |
code_verifier | PKCE 验证器 |
refresh_token | 刷新令牌 |
/connect/userinfo
返回用户信息,基于授权的 Scope。
| Scope | 返回的 Claims |
|---|---|
openid | sub |
profile | name |
email | email |
roles | role |
/connect/logout
这是 ConnectController 提供的自定义 POST 接口,要求 Auth Cookie。它清理登录 Cookie,并在当前主体带有 sub 时尝试撤销该主体的有效 Token。Program.cs 没有调用 SetEndSessionEndpointUris(...),因此不要把它当作标准 OIDC end-session 地址。
登录与同意页面
| 方法 | 路由 | 说明 |
|---|---|---|
| GET / POST | /connect/login | Razor Page,用户名或邮箱 + 密码登录,成功后写入 freekit.auth Cookie |
| GET / POST | /connect/consent | ConsentType=explicit 且没有覆盖本次 Scope 的有效授权时显示 |
| GET | /connect/test-callback | 仓库内置联调页入口,不是生产客户端回调约定 |
核心功能
用户认证
支持用户名或邮箱登录,大小写不敏感。
认证流程
- 查找用户(用户名/邮箱)
- 检查账户锁定状态
- 验证密码哈希
- 记录失败尝试
- 5 次失败后锁定 30 分钟
密码策略
| 配置 | 说明 |
|---|---|
| 最大失败次数 | 5 次 |
| 锁定时间 | 30 分钟 |
| 密码哈希 | ASP.NET Identity PasswordHasher |
| 自动重哈希 | 支持 |
动态声明映射
支持从数据库配置的动态声明映射。
映射实体
| 字段 | 类型 | 说明 |
|---|---|---|
ClaimType | string | 声明类型 |
UserPropertyName | string | 用户属性名 |
SourceType | ClaimSourceType | 来源类型 |
IsEnabled | bool | 是否启用 |
Scopes | string? | 关联的 Scope |
默认映射
| 声明类型 | 来源 | 需要的 Scope |
|---|---|---|
sub | AuthUser.Id | openid |
name | AuthUser.UserName | profile |
email | AuthUser.Email | email |
role | 用户角色 | roles |
OAuth2 应用管理
管理 OAuth2 客户端应用。
应用属性
| 属性 | 说明 |
|---|---|
ClientId | 客户端 ID |
ClientSecret | 客户端密钥 |
DisplayName | 显示名称 |
ApplicationType | 应用类型 |
ConsentType | 同意类型 |
RedirectUris | 回调地址 |
PostLogoutRedirectUris | 登出回调 |
Scopes | 允许的 Scope |
Permissions | 权限 |
Scope 管理
管理 OAuth2 Scope。
内置 Scope
| Scope | 显示名称 | 资源 |
|---|---|---|
openid | OpenID | identity |
profile | 用户资料 | identity |
email | 邮箱信息 | identity |
roles | 角色信息 | identity |
console | Console API | console |
cmskit | CmsKit API | cmskit |
im | IM API | im |
Token 管理
管理访问令牌和刷新令牌。
| 功能 | 说明 |
|---|---|
| 列出 Token | 分页查询 |
| 撤销 Token | 单个/批量撤销 |
| 清理 Token | 清理已兑换/已撤销的旧 Token |
启动引导
OpenIddictBootstrapper 在启动时自动配置:
| 功能 | 说明 |
|---|---|
| 确保 Scope | 自动创建内置 Scope |
| 确保应用 | 自动创建配置的应用 |
| 非破坏性合并 | 不覆盖已有记录 |
API 控制器
ConnectController
| 方法 | 路由 | 认证 | 说明 |
|---|---|---|---|
| GET | /connect/authorize | Anonymous | 授权端点 |
| POST | /connect/token | Anonymous | 令牌端点 |
| GET | /connect/userinfo | OpenIddict | 用户信息 |
| POST | /connect/logout | Cookie | 自定义 Cookie 登出,并按可用 sub 撤销 Token |
| GET | /connect/test-callback | Anonymous | 本地协议联调页 |
OpenIddictAdminController
| 方法 | 路由 | 认证 | 说明 |
|---|---|---|---|
| GET | /api/identity/openiddict/applications | Admin | 列出应用 |
| GET | /api/identity/openiddict/applications/{id} | Admin | 获取应用 |
| POST | /api/identity/openiddict/applications | Admin | 创建应用 |
| PUT | /api/identity/openiddict/applications/{id} | Admin | 更新应用 |
| DELETE | /api/identity/openiddict/applications/{id} | Admin | 删除应用 |
| GET | /api/identity/openiddict/scopes | Admin | 列出 Scope |
| POST | /api/identity/openiddict/scopes | Admin | 创建 Scope |
| DELETE | /api/identity/openiddict/scopes/{id} | Admin | 删除 Scope |
| GET | /api/identity/openiddict/tokens | Admin | 列出 Token |
| POST | /api/identity/openiddict/tokens/{id}/revoke | Admin | 撤销 Token |
| POST | /api/identity/openiddict/tokens/revoke-by-subject | Admin | 撤销用户 Token |
| POST | /api/identity/openiddict/tokens/revoke-by-application/{appId} | Admin | 撤销应用 Token |
| POST | /api/identity/openiddict/tokens/prune | Admin | 清理旧 Token |
| GET | /api/identity/openiddict/authorizations | Admin | 列出授权 |
配置选项
数据库配置
{
"ConnectionStrings": {
"DefaultDB": "MySql",
"MySql": "Server=localhost;Port=3306;Database=freekit_auth;User ID=<DB_USER>;Password=<DB_PASSWORD>;",
"Sqlite": "Data Source=freekit.auth.db"
}
}
证书配置
{
"Security": {
"OpenIddict": {
"Certificates": {
"SigningCertificatePath": "certs/signing.pfx",
"SigningCertificatePassword": "<SIGNING_CERT_PASSWORD>",
"EncryptionCertificatePath": "certs/encryption.pfx",
"EncryptionCertificatePassword": "<ENCRYPTION_CERT_PASSWORD>"
}
}
}
}
应用配置
{
"Host": {
"IdentityApi": "https://localhost:7005",
"CmsKitClient": "https://localhost:5173"
},
"Security": {
"OpenIddict": {
"Bootstrap": {
"Enabled": true,
"Applications": [
{
"ClientId": "freekit.cmskit",
"DisplayName": "FreeKit CmsKit SPA",
"Type": "public",
"ConsentType": "explicit",
"AllowAuthorizationCodeFlow": true,
"AllowRefreshTokenFlow": true,
"RequirePkce": true,
"RedirectUris": ["{CmsKitClient}/auth/callback"],
"PostLogoutRedirectUris": ["{CmsKitClient}/"],
"Scopes": ["openid", "profile", "email", "roles", "cmskit"]
}
]
}
}
}
}
上例是 public SPA,因此没有 ClientSecret。confidential 服务端客户端才配置 "ClientSecret": "<CLIENT_SECRET>",并从 Secret 管理器注入真实值。
环境变量
| 变量 | 说明 |
|---|---|
ASPNETCORE_ENVIRONMENT | 运行环境 |
Host__IdentityApi | Auth 对外基地址,例如 https://localhost:7005 |
Host__ConsoleClient | Console 前端基地址,用于 Bootstrap URI 模板 |
Host__CmsKitClient | CmsKit 前端基地址,用于 Bootstrap URI 模板 |
Security__OpenIddict__Certificates__SigningCertificatePassword | 签名证书密码;仅通过 Secret 注入 |
数据库表
| 表名 | 说明 |
|---|---|
identity_user | 用户表 |
identity_role | 角色表 |
identity_user_role | 用户角色关联 |
identity_connect_claim_mapping | 声明映射表 |
open_iddict_application | OAuth2 应用表 |
open_iddict_authorization | 授权记录表 |
open_iddict_scope | Scope 表 |
open_iddict_token | Token 表 |
与前端集成
1. 配置回调地址
const authServer = 'https://localhost:7005';
const clientId = 'freekit.cmskit';
// 必须与 Auth Bootstrap 的 RedirectUris 完全一致;它属于前端,不属于 Auth Server。
const callbackUrl = 'https://localhost:5173/auth/callback';
2. 获取授权码
const authorize = new URL('/connect/authorize', authServer);
authorize.search = new URLSearchParams({
client_id: clientId,
redirect_uri: callbackUrl,
response_type: 'code',
scope: 'openid profile email roles cmskit',
code_challenge: challenge,
code_challenge_method: 'S256',
state
}).toString();
window.location.assign(authorize);
challenge / verifier 必须是一对 PKCE 值,state 必须是每次登录新生成并在回调时校验的随机值。
3. 交换令牌
const response = await fetch(`${authServer}/connect/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: callbackUrl,
code_verifier: verifier,
client_id: clientId
})
});
浏览器 public client 不发送 client_secret。服务端 confidential client 如需 Secret,示例值使用 <CLIENT_SECRET>,真实值只能从 Secret 管理器读取。
4. 刷新令牌
const response = await fetch(`${authServer}/connect/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId
})
});
客户端要获得刷新令牌,还必须在 Bootstrap 权限与授权请求中包含 offline_access。
源码定位
auth/src/FreeKit.Auth.Host/Program.cs:7.5.0 Server 注册、证书、真实端点和中间件auth/src/FreeKit.Auth.Host/Properties/launchSettings.json:7005 / 5005auth/src/FreeKit.Auth.Host/Controllers/ConnectController.cs:授权、Token、userinfo 与自定义 logoutauth/src/FreeKit.Auth.Host/Pages/Login.cshtml、Pages/Consent.cshtml:交互页面路由auth/src/FreeKit.Auth/OpenIddict/OpenIddictBootstrapper.cs:内置 Scope、客户端权限和 URI 模板auth/src/FreeKit.Auth.Host/AuthPersistenceExtensions.cs:独立 FreeSql 持久化选择