跳到主要内容

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。它通过自己的 AuthUserAuthRoleAuthUserRole 轻量模型读取所配置数据库中的 identity_useridentity_roleidentity_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.jsonOpenIddictBootstrapOptions 对齐。浏览器 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:AuthorityClientIdClientSecretScope。签名证书路径留空时,验证器通过 Discovery/JWKS 获取签名材料;若显式配置证书,密码必须由 Secret 管理器或环境变量注入。

4. SPA 使用真实协议端点

用途方法与 URL
授权GET https://localhost:7005/connect/authorize
换取 / 刷新 TokenPOST 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_uriAuth Bootstrap 中的 URI 是否与实际 redirect_uri 完全一致
API 返回 401Security:AuthMode 是否为 OpenIddict,Issuer 是否为 https://localhost:7005,Discovery 是否可访问
SPA 没有 refresh token是否请求 offline_access,客户端是否允许刷新令牌流
外部提供商列表没有 KitSSO当前 Host 是否使用 AuthMode=JwtSecurity: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 特例

相关文档