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 | 登出并吊销所有令牌 |
2. Cookie 认证
内部使用 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:
| 映射 ID | Claim Type | 用户属性 | 来源 | 所属 Scope |
|---|---|---|---|---|
...0001 | sub | AuthUser.Id | UserProperty | openid |
...0002 | name | AuthUser.UserName | UserProperty | profile |
...0003 | email | AuthUser.Email | UserProperty | email |
...0004 | name | AuthUser.Name | UserProperty | profile |
...0005 | role | (角色表查询) | UserRole | roles |
设计优势:
- 无需硬编码 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));
}
}
5. Consent 授权确认机制
KitSSO 实现了完整的 OAuth2 Consent 流程:
- 用户登录后首次访问
/connect/authorize - 检查应用的
ConsentType是否为Explicit - 若用户从未授权此应用,或请求了新的 scope → 跳转
/connect/consent页面 - 用户确认后,创建
OpenIddictAuthorization记录 - 后续同 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 | 实体 | 数据库表 | 功能 |
|---|---|---|---|
FreeSqlOpenIddictApplicationStore | OpenIddictApplication | open_iddict_application | 客户端应用 CRUD |
FreeSqlOpenIddictAuthorizationStore | OpenIddictAuthorization | open_iddict_authorization | 授权记录管理 |
FreeSqlOpenIddictScopeStore | OpenIddictScope | open_iddict_scope | Scope / Resource 管理 |
FreeSqlOpenIddictTokenStore | OpenIddictToken | open_iddict_token | Token 生命周期管理 |
注册方式
// 一行注册所有 Store
builder.Services.AddOpenIddict()
.AddCore(options => options.AddFreeSqlStores());
AddFreeSqlStores() 内部执行:
SetDefaultXxxEntity<T>()— 设置 OpenIddict 使用 FreeSql 实体ReplaceXxxStore<T>()— 替换默认存储为 FreeSql 实现- 所有 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(始终创建):
| Scope | Display Name | Resource | 说明 |
|---|---|---|---|
openid | OpenID | identity | OIDC 基础 |
profile | 用户资料 | identity | 用户名、头像等 |
email | 邮箱信息 | identity | 邮箱 |
roles | 角色信息 | identity | 角色列表 |
console | Console API | console | FreeKit Console |
cmskit | CmsKit API | cmskit | 内容社区 |
应用配置示例:
{
"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)
| 字段 | 类型 | 说明 |
|---|---|---|
Id | Guid | 主键 |
UserName | string | 用户名 |
NormalizedUserName | string | 标准化用户名(大写) |
Email | string | 邮箱 |
NormalizedEmail | string | 标准化邮箱(大写) |
PasswordHash | string | 密码哈希 |
Name | string | 真实姓名 |
NickName | string | 昵称 |
PhoneNumber | string | 手机号 |
LockoutEnabled | bool | 是否启用锁定 |
LockoutEnd | DateTime? | 锁定截止时间 |
AccessFailedCount | int | 连续失败次数 |
Status | int | 用户状态 |
IsDeleted | bool | 软删除标记 |
identity_role(AuthRole)
| 字段 | 类型 | 说明 |
|---|---|---|
Id | Guid | 主键 |
Name | string | 角色名称 |
identity_user_role(AuthUserRole)
| 字段 | 类型 | 说明 |
|---|---|---|
UserId | Guid | 用户 ID |
RoleId | Guid | 角色 ID |
OpenIddict 表
| 表名 | 实体 | 说明 |
|---|---|---|
open_iddict_application | OpenIddictApplication | OAuth2 客户端应用 |
open_iddict_authorization | OpenIddictAuthorization | 用户授权记录 |
open_iddict_scope | OpenIddictScope | Scope / Resource 定义 |
open_iddict_token | OpenIddictToken | 访问令牌 / 刷新令牌 |
Claim 映射表
identity_connect_claim_mapping(ConnectClaimMapping)
| 字段 | 类型 | 说明 |
|---|---|---|
Id | Guid | 主键 |
ClaimType | string | OIDC Claim 类型(sub/name/email/role) |
UserPropertyName | string | 用户实体的属性名称 |
SourceType | enum | 来源类型:UserProperty / UserRole |
IsEnabled | bool | 是否启用 |
Scopes | string | JSON 数组,关联的 scope |
SortCode | int | 排序 |
Description | string | 描述 |
数据库兼容性处理
// 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.Host 在 appsettings.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 双引擎)。