SSO 单点登录
FreeKit 的 SSO 由独立的 FreeKit.Auth.Host 提供 OAuth 2.0 / OpenID Connect 授权服务。当前最清晰的接入方式是:SPA 使用 Authorization Code + PKCE 登录,业务 API 使用 OpenIddict Validation 校验 Auth 签发的访问令牌。
:::info 三个地址不要混用
- Auth 授权中心:
https://localhost:7005(HTTP:http://localhost:5005) - 主业务 Host:
https://localhost:7000;主 Compose 使用 PathBase/kit_api - 默认 CmsKit 前端:
https://localhost:5173,回调路径/auth/callback
:::
组件边界
FreeKit.Auth.Host 不加载 IdentityModuleStartup。它通过自己的 AuthUser、AuthRole、AuthUserRole 轻量模型读取所配置数据库中的 identity_user、identity_role、identity_user_role 表,并单独管理 OpenIddict 应用、授权、Scope 和 Token 表。因此:
- Auth 与主 Host 可以共享 Identity 数据库,但连接串由 Auth 自己配置;
IdentityUser的注册、资料、租户等业务仍属于 Identity 模块;- SSO 协议端点属于 Auth,不在
/kit_api下。
快速接入:SPA + API
1. 启动 Auth
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
2. 在 Auth 注册 SPA 客户端
以下结构与 auth/src/FreeKit.Auth.Host/appsettings.json 及 OpenIddictBootstrapOptions 对齐。浏览器 PKCE 客户端使用 public 类型,不应下发 ClientSecret。
{
"Host": {
"IdentityApi": "https://localhost:7005",
"CmsKitClient": "https://localhost:5173"
},
"Cors": {
"AllowedOrigins": ["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"]
}
]
}
}
}
}
OpenIddictBootstrapper 会展开 {IdentityApi}、{PathBase}、{ConsoleClient}、{CmsKitClient} 模板,并以增量合并方式初始化客户端。回调 URI 必须与浏览器实际使用的 URI 逐字符一致,包括协议、端口和路径。
3. 配置主业务 API 验证 Token
主 Host 的 UseFreeKit(...) 最终调用 AddDefaultJsonWebToken(...)。把认证模式设为 OpenIddict 后,API 会同时注册 OpenIddict Validation 和本地 JwtBearer,默认使用前者:
{
"Security": {
"AuthMode": "OpenIddict",
"OpenIddict": {
"Validation": {
"Issuer": "https://localhost:7005",
"SigningCertificatePath": "",
"SigningCertificatePassword": "<SIGNING_CERT_PASSWORD>",
"EncryptionCertificatePath": "",
"EncryptionCertificatePassword": "<ENCRYPTION_CERT_PASSWORD>"
}
}
}
}
此 Validation 配置只需要颁发方 Issuer;它不读取旧文档中的 Security:OpenIddict:Authority、ClientId、ClientSecret 或 Scope。签名证书路径留空时,验证器通过 Discovery/JWKS 获取签名材料;若显式配置证书,密码必须由 Secret 管理器或环境变量注入。
4. SPA 使用真实协议端点
| 用途 | 方法与 URL |
|---|---|
| 授权 | GET https://localhost:7005/connect/authorize |
| 换取 / 刷新 Token | POST https://localhost:7005/connect/token |
| 用户信息 | GET https://localhost:7005/connect/userinfo |
| Auth 登录页 | GET https://localhost:7005/connect/login |
| 授权确认页 | GET https://localhost:7005/connect/consent |
| 自定义会话登出 | POST https://localhost:7005/connect/logout |
浏览器客户端至少提交:
GET /connect/authorize?
client_id=freekit.cmskit&
redirect_uri=https%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback&
response_type=code&
scope=openid%20profile%20email%20roles%20cmskit&
code_challenge=<PKCE_CHALLENGE>&
code_challenge_method=S256&
state=<RANDOM_STATE>
回调收到 code 后:
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
client_id=freekit.cmskit&
code=<AUTHORIZATION_CODE>&
redirect_uri=https%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback&
code_verifier=<PKCE_VERIFIER>
公共 SPA 不发送 ClientSecret。若要获取刷新令牌,客户端权限和请求 Scope 还需包含 offline_access。
流程说明
Auth 当前允许授权码流和刷新令牌流,不支持旧文档示例中的隐式流或密码流。
服务端交互式登录(KitSSO)
仓库还提供 AspNet.Security.OAuth.KitSSO 的 OpenID Connect handler。它默认启用 Code + PKCE,回调路径为 /signin-kitsso:
{
"Security": {
"AuthMode": "Jwt",
"KitSSO": {
"Enable": true,
"Authority": "https://localhost:7005",
"ClientId": "freekit.web",
"ClientSecret": "<CLIENT_SECRET>",
"RequireHttpsMetadata": true
}
}
}
:::warning 当前注册分支
AddDefaultJsonWebToken(...) 目前只在 AuthMode=Jwt 分支注册 AddKitSSO(...);AuthMode=OpenIddict 分支负责资源服务器 Token 验证,不会注册交互式 KitSSO handler。两种角色不要用同一段配置混淆。
:::
如果该 handler 运行在主 Host 且启用 /kit_api PathBase,Auth 客户端登记的回调应为:
https://localhost:7000/kit_api/signin-kitsso
若不使用 PathBase,则为 https://localhost:7000/signin-kitsso。这是 OIDC 中间件回调;前端 SPA 的 /auth/callback 是另一种客户端形态,二者不可互换。
与 Identity 第三方登录的关系
OAuth2Controller 仍支持 GitHub、Gitee、QQ、微信、Google、Microsoft 等第三方登录,并把绑定写入 IdentityUserLogin。KitSSO 是例外:它直接使用 Auth Token 中的 GUID sub 查找同 ID 的 IdentityUser,不创建或绑定本地第三方账号;代码也明确拒绝 KitSSO 绑定操作。
因此共享用户表时要保证 Auth 的 AuthUser.Id 与 Identity 的 IdentityUser.Id 一致,否则 KitSSO 回调无法落到业务用户。
生产安全
- 生产环境必须使用 HTTPS,并为 Auth 配置可持久化的签名/加密证书;开发证书不适合多实例或重启后的跨服务验证。
- SPA 使用 public + PKCE,不把客户端密钥写入浏览器包;confidential 客户端的 Secret 使用环境变量或 Secret 管理器注入。
Cors:AllowedOrigins只列明确来源,不能与AllowCredentials()一起放开任意来源。- 回调必须验证
state,PKCE verifier 只保存在发起登录的会话中。 - 访问令牌用于 API,Cookie 用于 Auth 登录会话;
POST /connect/logout是自定义 Cookie 登出接口,不是已配置的 OpenIddict end-session endpoint。
故障排查
| 现象 | 核对项 |
|---|---|
invalid_redirect_uri | Auth Bootstrap 中的 URI 是否与实际 redirect_uri 完全一致 |
| API 返回 401 | Security:AuthMode 是否为 OpenIddict,Issuer 是否为 https://localhost:7005,Discovery 是否可访问 |
| SPA 没有 refresh token | 是否请求 offline_access,客户端是否允许刷新令牌流 |
| 外部提供商列表没有 KitSSO | 当前 Host 是否使用 AuthMode=Jwt 且 Security:KitSSO:Enable=true |
| 角色 Claim 缺失 | 客户端是否允许并请求 roles,数据库声明映射是否启用 |
| Docker 内部使用 HTTP 失败 | 服务端交互 handler 可在可信内网显式设 RequireHttpsMetadata=false;公开入口仍应使用 HTTPS |
源码定位
auth/src/FreeKit.Auth.Host/Program.cs:OpenIddict Server、端点与 Cookie 配置auth/src/FreeKit.Auth.Host/Controllers/ConnectController.cs:真实/connect/*动作auth/src/FreeKit.Auth/OpenIddict/OpenIddictBootstrapper.cs:Scope、客户端和 URI 模板初始化src/BuildingBlocks/IGeekFan.FreeKit.Web/Extensions/ServiceCollectionExtensions.cs:API Validation 与 KitSSO 注册分支src/BuildingBlocks/IGeekFan.FreeKit.Auth.Client/KitSSOAuthOptions.cs:/signin-kitsso、Code + PKCE 默认值src/Services/Identity/FreeKit.Identity.HttpApi/Controllers/OAuth2Controller.cs:Identity 第三方登录及 KitSSO 特例