跳到主要内容

KitSSO — FreeKit OAuth2 认证服务

FreeKit.Auth.Host(代号 KitSSO)是 FreeKit Pro Modules 的独立 OAuth2 / OpenID Connect 认证服务,基于 OpenIddict 7.5 + FreeSql 自定义存储实现,对外提供标准的授权码流程和 Token 签发能力。

定位

KitSSO 是一个独立的认证中心,不依赖主宿主 FreeKit.Host

  • 主宿主(FreeKit.Host)和各模块客户端通过 OAuth2 Authorization Code Flow + PKCE 接入 KitSSO
  • 用户身份管理(登录、注册、密码)由 KitSSO 独立负责
  • CmsKit、Console 等客户端通过 appsettings.json 中的 Security:KitSSO 配置段接入

技术架构

┌─────────────────────────────────────────────────────────────┐
│ FreeKit.Auth.Host │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Connect │ │ OpenIddict │ │ Login/Consent│ │
│ │ Controller │ │ Admin API │ │ Razor Pages │ │
│ │ │ │ │ │ │ │
│ │ /authorize │ │ /api/identity│ │ /connect/ │ │
│ │ /token │ │ /openiddict/*│ │ login │ │
│ │ /userinfo │ │ │ │ consent │ │
│ │ /logout │ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
│ │ │ │
│ ┌──────┴──────────────────┴───────────────────────────┐ │
│ │ 服务层 (Services) │ │
│ │ AuthService ConnectService OAuth2AdminService │ │
│ │ (身份验证) (Claim构建) (管理CRUD) │ │
│ └──────┬──────────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴──────────────────────────────────────────────┐ │
│ │ IGeekFan.OpenIddict.FreeSql │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Application │ │ Token │ │ │
│ │ │ Store │ │ Store │ │ │
│ │ ├──────────────┤ ├──────────────┤ │ │
│ │ │ Authorization│ │ Scope │ │ │
│ │ │ Store │ │ Store │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ FreeSql ORM │
│ MySQL / SQLite │
└─────────────────────────────────────────────────────────────┘

核心组件

1. OpenIddict 集成

Program.cs 中配置了完整的 OpenIddict Server + Validation:

services.AddOpenIddict()
.AddCore(options => options.AddFreeSqlStores()) // 自定义 FreeSql 存储
.AddServer(options => { /* 授权/令牌端点 */ })
.AddValidation(options => { /* 本地验证 */ });

支持的授权类型:

  • Authorization Code Flow(授权码流程)
  • Refresh Token Flow(刷新令牌)

端点:

端点路径说明
Authorization/connect/authorize用户授权入口,支持 Consent 确认
Token/connect/token换取 access_token / refresh_token
UserInfo/connect/userinfo获取当前用户信息(需 Bearer Token)
Logout/connect/logout登出并吊销所有令牌

内部使用 ASP.NET Core Cookie 认证管理登录会话:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/connect/login";
options.LogoutPath = "/connect/logout";
options.Cookie.Name = "freekit.auth";
});

3. 身份验证(AuthService)

  • 用户名/邮箱登录:支持用户名或邮箱 + 密码登录
  • 登录锁定:连续失败 5 次后锁定 30 分钟
  • 密码哈希:使用 IPasswordHasher<T>(ASP.NET Core Identity 标准算法)
  • 自动升级哈希:旧格式密码验证后自动升级为最新格式
// 登录流程
AuthLoginResult result = await authService.ValidateCredentialsAsync(username, password);
if (result.Succeeded) { /* 签发令牌 */ }

4. Claim 映射系统(ConnectClaimMapping)

KitSSO 的核心创新之一是数据库驱动的 Claim 映射,将用户实体属性动态映射为 JWT/OIDC Claims:

映射 IDClaim Type用户属性来源所属 Scope
...0001subAuthUser.IdUserPropertyopenid
...0002nameAuthUser.UserNameUserPropertyprofile
...0003emailAuthUser.EmailUserPropertyemail
...0004nameAuthUser.NameUserPropertyprofile
...0005role(角色表查询)UserRoleroles

设计优势:

  • 无需硬编码 Claim 构建逻辑,映射关系存储在 identity_connect_claim_mapping 表中
  • 通过 Scopes 字段实现「按 Scope 过滤 Claim」— 客户端请求哪些 scope,就返回对应 Claim
  • 支持 UserProperty(直接取值)和 UserRole(查角色表)两种来源
  • 新增用户属性和 Claim 只需添加映射记录,无需改代码
// ConnectService.BuildClaimsPrincipalAsync 核心逻辑
foreach (ConnectClaimMapping mapping in FilterMappingsByScopes(mappings, grantedScopes))
{
if (mapping.SourceType == ClaimSourceType.UserRole)
{
// 查询用户角色,为每个角色添加 role claim
foreach (string role in roles)
identity.AddClaim(new Claim(mapping.ClaimType, role));
}
else
{
// 从 AuthUser 属性反射取值
string? value = GetPropertyValue(user, mapping.UserPropertyName);
identity.AddClaim(new Claim(mapping.ClaimType, value));
}
}

KitSSO 实现了完整的 OAuth2 Consent 流程:

  1. 用户登录后首次访问 /connect/authorize
  2. 检查应用的 ConsentType 是否为 Explicit
  3. 若用户从未授权此应用,或请求了新的 scope → 跳转 /connect/consent 页面
  4. 用户确认后,创建 OpenIddictAuthorization 记录
  5. 后续同 scope 请求不再弹 Consent,直接通过

6. Logout 吊销令牌

登出时会主动吊销用户所有有效 Token:

await tokenRepository.UpdateDiy
.Where(x => x.Subject == subject && x.Status == "valid")
.Set(x => x.Status, "revoked")
.ExecuteAffrowsAsync();

OpenIddict FreeSql 扩展实现

NuGet 包:IGeekFan.OpenIddict.FreeSql

这是 FreeKit 生态中的关键基础库,位于 src/FreeKit/src/IGeekFan.OpenIddict.FreeSql/

依赖:

<PackageReference Include="OpenIddict.Core" Version="7.5.0" />
<PackageReference Include="FreeSql" />
<PackageReference Include="FreeSql.Repository" />

4 个 FreeSql Store 实现

Store实体数据库表功能
FreeSqlOpenIddictApplicationStoreOpenIddictApplicationopen_iddict_application客户端应用 CRUD
FreeSqlOpenIddictAuthorizationStoreOpenIddictAuthorizationopen_iddict_authorization授权记录管理
FreeSqlOpenIddictScopeStoreOpenIddictScopeopen_iddict_scopeScope / Resource 管理
FreeSqlOpenIddictTokenStoreOpenIddictTokenopen_iddict_tokenToken 生命周期管理

注册方式

// 一行注册所有 Store
builder.Services.AddOpenIddict()
.AddCore(options => options.AddFreeSqlStores());

AddFreeSqlStores() 内部执行:

  1. SetDefaultXxxEntity<T>() — 设置 OpenIddict 使用 FreeSql 实体
  2. ReplaceXxxStore<T>() — 替换默认存储为 FreeSql 实现
  3. 所有 Store 使用 IBaseRepository<T, Guid> 操作数据库

CodeFirst 自动建表

// Program.cs 启动时同步表结构
freeSql.SyncOpenIddictTables();

SyncOpenIddictTables() 扩展方法同步 4 张 OpenIddict 表,无需手动 DDL。

配置参考

appsettings.json 结构

{
"ConnectionStrings": {
"DefaultDB": "0",
"MySql": "Data Source=localhost;Port=3306;User ID=root;...;Initial Catalog=freekit;",
"Sqlite": "Data Source=DB/freekit.db"
},
"Host": {
"IdentityApi": "https://localhost:7005",
"CmsKitClient": "https://localhost:5173"
},
"Security": {
"OpenIddict": {
"Certificates": {
"SigningCertificatePath": "Certificates/auth-signing.pfx",
"SigningCertificatePassword": "",
"EncryptionCertificatePath": "Certificates/auth-encryption.pfx",
"EncryptionCertificatePassword": ""
},
"Bootstrap": {
"Enabled": true,
"Scopes": [],
"Applications": [ /* ... */ ]
}
}
}
}

证书配置(跨服务 Token 验证)

KitSSO 使用 X.509 证书签名和加密 JWT:

  • 开发环境:自动生成临时开发证书(AddDevelopmentSigningCertificate()
  • 生产环境:通过 SigningCertificatePath + EncryptionCertificatePath 指定持久化证书
  • 其他服务(如 FreeKit.Host)需要配置相同的公钥来验证 Token 签名

Bootstrap 配置

OpenIddictBootstrapOptions 用于启动时自动创建 Scope 和应用:

内置 Scope(始终创建):

ScopeDisplay NameResource说明
openidOpenIDidentityOIDC 基础
profile用户资料identity用户名、头像等
email邮箱信息identity邮箱
roles角色信息identity角色列表
consoleConsole APIconsoleFreeKit Console
cmskitCmsKit APIcmskit内容社区

应用配置示例:

{
"ClientId": "freekit.cmskit",
"DisplayName": "FreeKit CmsKit",
"ApplicationType": "web",
"Type": "public",
"RequirePkce": true,
"RedirectUris": ["{CmsKitClient}/auth/callback"],
"PostLogoutRedirectUris": ["{CmsKitClient}/"],
"Scopes": ["openid", "profile", "email", "roles", "cmskit"]
}

模板变量:

  • {IdentityApi} — 替换为 Host:IdentityApi 配置值
  • {CmsKitClient} — 替换为 Host:CmsKitClient 配置值
  • {PathBase} — 替换为 ASPNETCORE_PATHBASE 环境变量

认证流程详解

Authorization Code Flow + PKCE

┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ CmsKit │ │ KitSSO │ │ Database│ │ CmsKit │
│ Client │ │ Auth │ │ (FreeSql)│ │ API │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ 1. Redirect to │ │ │
│ /connect/authorize │ │
│────────────────>│ │ │
│ │ │ │
│ │ 2. Check Cookie (登录态) │
│ │──────────────>│ │
│ │ │ │
│ │ 3. Check App Consent │
│ │──────────────>│ │
│ │ │ │
│ 4. Login/Consent Page │ │
│<────────────────│ │ │
│ │ │ │
│ 5. User Auth │ │ │
│────────────────>│ │ │
│ │ │ │
│ │ 6. Build ClaimsPrincipal │
│ │──────────────>│ │
│ │ │ │
│ 7. Redirect with code │ │
│<────────────────│ │ │
│ │ │ │
│ 8. POST /connect/token (code + code_verifier) │
│────────────────>│ │ │
│ │ │ │
│ 9. access_token + refresh_token│ │
│<────────────────│ │ │
│ │ │ │
│ 10. API Request (Bearer access_token) │
│──────────────────────────────────────────────────>│
│ │ │ │
│ 11. API Response │ │
│<──────────────────────────────────────────────────│

Token 验证方式

其他服务验证 KitSSO 签发的 Token 有两种方式:

方式一:本地验证(推荐,Docker Compose 中使用)

// FreeKit.Host 配置
builder.Services.AddOpenIddict()
.AddValidation(options =>
{
options.SetIssuer("http://auth:8080");
options.UseSystemNetHttp(); // 通过 OIDC Discovery 获取公钥
options.UseAspNetCore();
});

KitSSO 签发 Token 时使用自己的签名证书,资源服务器通过 OIDC Discovery 端点获取公钥来验证签名。

方式二:Introspection

options.UseIntrospection()
.SetClientId("resource_server")
.SetClientSecret("...");

Admin API

OpenIddictAdminController 提供管理 API(需要 Admin 角色),路由前缀 /api/identity/openiddict

方法路径说明
GET/applications获取所有应用列表
GET/applications/{id}获取应用详情(含密钥)
POST/applications创建应用
PUT/applications/{id}更新应用
DELETE/applications/{id}删除应用
GET/scopes获取所有 Scope
POST/scopes创建 Scope
DELETE/scopes/{id}删除 Scope
GET/tokens分页查询 Token
POST/tokens/{id}/revoke吊销指定 Token
POST/tokens/revoke-by-subject吊销用户所有 Token
POST/tokens/revoke-by-application/{appId}吊销应用所有 Token
POST/tokens/prune清理过期 Token
GET/authorizations分页查询授权记录

数据模型

用户身份表

identity_user(AuthUser)

字段类型说明
IdGuid主键
UserNamestring用户名
NormalizedUserNamestring标准化用户名(大写)
Emailstring邮箱
NormalizedEmailstring标准化邮箱(大写)
PasswordHashstring密码哈希
Namestring真实姓名
NickNamestring昵称
PhoneNumberstring手机号
LockoutEnabledbool是否启用锁定
LockoutEndDateTime?锁定截止时间
AccessFailedCountint连续失败次数
Statusint用户状态
IsDeletedbool软删除标记

identity_role(AuthRole)

字段类型说明
IdGuid主键
Namestring角色名称

identity_user_role(AuthUserRole)

字段类型说明
UserIdGuid用户 ID
RoleIdGuid角色 ID

OpenIddict 表

表名实体说明
open_iddict_applicationOpenIddictApplicationOAuth2 客户端应用
open_iddict_authorizationOpenIddictAuthorization用户授权记录
open_iddict_scopeOpenIddictScopeScope / Resource 定义
open_iddict_tokenOpenIddictToken访问令牌 / 刷新令牌

Claim 映射表

identity_connect_claim_mapping(ConnectClaimMapping)

字段类型说明
IdGuid主键
ClaimTypestringOIDC Claim 类型(sub/name/email/role)
UserPropertyNamestring用户实体的属性名称
SourceTypeenum来源类型:UserProperty / UserRole
IsEnabledbool是否启用
ScopesstringJSON 数组,关联的 scope
SortCodeint排序
Descriptionstring描述

数据库兼容性处理

// MySQL 环境下修复 OpenIddict Token 的 type 列长度
static async Task EnsureOpenIddictCompatibilityAsync(IFreeSql freeSql)
{
if (freeSql.Ado.DataType != DataType.MySql) return;
await freeSql.Ado.ExecuteNonQueryAsync(
"ALTER TABLE `open_iddict_token` MODIFY COLUMN `type` varchar(200) NULL;");
}

OpenIddict 的 Token type 字段在 MySQL 中默认长度不足,KitSSO 启动时自动修复此问题。

客户端接入指南

CmsKit 前端接入

// CmsKit 客户端 OAuth2 配置
const config = {
authority: 'https://localhost:7005', // KitSSO 地址
client_id: 'freekit.cmskit', // 预注册的应用 ID
redirect_uri: window.location.origin + '/auth/callback',
response_type: 'code',
scope: 'openid profile email roles cmskit',
};

主宿主接入

FreeKit.Hostappsettings.json 中配置:

{
"Security": {
"KitSSO": {
"Enable": true,
"Authority": "http://auth:8080",
"ClientId": "freekit.cmskit",
"ClientSecret": "",
"RequireHttpsMetadata": false
}
}
}

部署

Docker Compose

auth:
build:
context: ../../../
dockerfile: src/Services/Auth/FreeKit.Auth.Host/Dockerfile
image: freekit/auth:local
container_name: freekit-auth
environment:
ASPNETCORE_ENVIRONMENT: LocalProduction
ASPNETCORE_URLS: http://+:8080
ports:
- "18083:8080"

本地开发

cd src/Services/Auth/FreeKit.Auth.Host
dotnet run --launch-profile https-dev

访问 https://localhost:7005/swagger 查看 API 文档(Swagger + RapiDoc 双引擎)。