feat(identity): 接入认证中心多租户登录

支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
This commit is contained in:
2026-07-28 17:28:35 +08:00
parent 0b02e62c72
commit 5c679ff13f
45 changed files with 2986 additions and 139 deletions
+46 -3
View File
@@ -13,6 +13,7 @@ import (
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/oidcsession"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
"github.com/google/uuid"
)
const (
@@ -49,7 +50,12 @@ func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) {
if returnTo == "" {
returnTo = "/"
}
transaction, err := oidcsession.NewLoginTransaction(returnTo, time.Now())
tenantHint := strings.TrimSpace(r.URL.Query().Get("tenantHint"))
if tenantHint != "" && (runtime.Revision.TenantMode != "multi_tenant" || uuid.Validate(tenantHint) != nil) {
writeError(w, http.StatusBadRequest, "租户提示无效", errorCodeOIDCLoginInvalid)
return
}
transaction, err := oidcsession.NewLoginTransactionWithTenantHint(returnTo, tenantHint, time.Now())
if err != nil {
writeError(w, http.StatusBadRequest, "登录后返回地址无效", errorCodeOIDCLoginInvalid)
return
@@ -60,7 +66,9 @@ func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusServiceUnavailable, "登录会话初始化失败,请稍后重试", errorCodeOIDCSessionStoreUnavailable)
return
}
authorizationURL, err := runtime.PublicClient.AuthorizationURL(r.Context(), transaction.State, transaction.Nonce, transaction.PKCEVerifier)
authorizationURL, err := runtime.PublicClient.AuthorizationURL(
r.Context(), transaction.State, transaction.Nonce, transaction.PKCEVerifier, transaction.TenantHint,
)
if err != nil {
s.logger.ErrorContext(r.Context(), "load OIDC authorization endpoint failed", "error", err)
writeError(w, http.StatusServiceUnavailable, "认证中心暂时不可用,请稍后重试", "OIDC_AUTHORIZATION_UNAVAILABLE")
@@ -124,7 +132,7 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
s.writeOIDCTokenFailure(w, r, "ID_TOKEN_INVALID", auth.OIDCValidationCategory(err), "认证中心身份令牌校验失败")
return
}
projection, err := s.resolveOIDCUserProjectionForRevision(r.Context(), r, identity, runtime.Revision)
projection, err := s.resolveOIDCUserProjectionForRuntime(r.Context(), r, identity, runtime)
if err != nil {
s.writeOIDCCallbackProjectionError(w, r, err)
return
@@ -140,6 +148,13 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
s.writeOIDCCallbackError(w, r, http.StatusServiceUnavailable, "登录会话保存失败,请稍后重试", errorCodeOIDCSessionStoreUnavailable)
return
}
if err := s.replaceOIDCBrowserSession(r.Context(), r, runtime, rawSession); err != nil {
if _, cleanupErr := runtime.Sessions.Delete(r.Context(), rawSession); cleanupErr != nil {
s.logger.WarnContext(r.Context(), "cleanup replacement OIDC session failed", "error", cleanupErr)
}
s.writeOIDCCallbackError(w, r, http.StatusServiceUnavailable, "旧登录会话清理失败,请稍后重试", errorCodeOIDCSessionStoreUnavailable)
return
}
now := time.Now()
http.SetCookie(w, &http.Cookie{
Name: auth.OIDCSessionCookieName, Value: rawSession, Path: "/",
@@ -151,6 +166,34 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, oidcReturnLocation(runtime.Revision.WebBaseURL, transaction.ReturnTo), http.StatusSeeOther)
}
func (s *Server) replaceOIDCBrowserSession(
ctx context.Context,
r *http.Request,
runtime *identityRequestRuntime,
newRawSession string,
) error {
previous, err := r.Cookie(auth.OIDCSessionCookieName)
if errors.Is(err, http.ErrNoCookie) {
return nil
}
if err != nil {
return err
}
if previous.Value == "" || previous.Value == newRawSession {
return nil
}
bundle, err := runtime.Sessions.Delete(ctx, previous.Value)
if err != nil {
return err
}
if bundle.RefreshToken != "" {
if err := runtime.PublicClient.RevokeRefreshToken(ctx, bundle.RefreshToken); err != nil {
s.logger.WarnContext(ctx, "revoke replaced OIDC refresh token failed", "error", err)
}
}
return nil
}
// logoutOIDCSession godoc
// @Summary 注销 OIDC 登录会话
// @Description 删除 Gateway Session、撤销公共 Client Refresh Token,并跳转认证中心退出地址。