fix(oidc): 同步认证中心用户资料到本地用户

从已验证的 OIDC Claim 提取用户名、显示名称、邮箱、手机号和头像,并覆盖单租户、多租户及平台用户的 JIT 创建与重复登录同步。\n\n保留 metadata.manualProfile 标记下的人工资料,限制字段长度且仅接收已验证联系方式与 HTTPS 头像。已通过 auth、httpapi、store 测试及临时 PostgreSQL 集成验证。
This commit is contained in:
2026-08-03 15:55:50 +08:00
parent 3c8d9839a4
commit b125599354
10 changed files with 206 additions and 24 deletions
+38 -3
View File
@@ -26,6 +26,10 @@ type ResolveOrProvisionOIDCUserInput struct {
ApplicationID string
Subject string
Username string
DisplayName string
Email string
Phone string
AvatarURL string
Roles []string
ContextType string
TenantID string
@@ -122,16 +126,22 @@ func (s *Store) ResolveOrProvisionOIDCUser(ctx context.Context, input ResolveOrP
createdUser, err := scanUser(tx.QueryRow(ctx, `
INSERT INTO gateway_users (
user_key, source, external_user_id, username, gateway_tenant_id, tenant_id, tenant_key,
user_key, source, external_user_id, username, display_name, email, phone, avatar_url,
gateway_tenant_id, tenant_id, tenant_key,
default_user_group_id, roles, auth_profile, metadata, status, last_login_at, synced_at, source_updated_at
)
VALUES ($1, 'oidc', $2, $3, $4::uuid, $5, $6, $7::uuid, $8::jsonb, '{}'::jsonb, $9::jsonb,
VALUES ($1, 'oidc', $2, $3, NULLIF($4, ''), NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''),
$8::uuid, $9, $10, $11::uuid, $12::jsonb, '{}'::jsonb, $13::jsonb,
'active', now(), now(), now())
ON CONFLICT DO NOTHING
RETURNING `+userColumns,
userKey,
input.Subject,
username,
input.DisplayName,
input.Email,
input.Phone,
input.AvatarURL,
tenantID,
input.TenantID,
input.GatewayTenantKey,
@@ -244,7 +254,11 @@ func (s *Store) syncExistingOIDCUser(ctx context.Context, tx pgx.Tx, projection
updated, err := scanUser(tx.QueryRow(ctx, `
UPDATE gateway_users
SET username = COALESCE(NULLIF($2, ''), username),
roles = $3::jsonb,
display_name = CASE WHEN metadata->>'manualProfile' = 'true' THEN display_name ELSE COALESCE(NULLIF($3, ''), display_name) END,
email = CASE WHEN metadata->>'manualProfile' = 'true' THEN email ELSE COALESCE(NULLIF($4, ''), email) END,
phone = CASE WHEN metadata->>'manualProfile' = 'true' THEN phone ELSE COALESCE(NULLIF($5, ''), phone) END,
avatar_url = CASE WHEN metadata->>'manualProfile' = 'true' THEN avatar_url ELSE COALESCE(NULLIF($6, ''), avatar_url) END,
roles = $7::jsonb,
last_login_at = now(),
synced_at = now(),
source_updated_at = now(),
@@ -256,6 +270,10 @@ WHERE id = $1::uuid
RETURNING `+userColumns,
projection.user.ID,
input.Username,
input.DisplayName,
input.Email,
input.Phone,
input.AvatarURL,
string(rolesJSON),
))
if err != nil {
@@ -343,6 +361,10 @@ func normalizeOIDCUserInput(input ResolveOrProvisionOIDCUserInput) ResolveOrProv
input.Issuer = strings.TrimRight(strings.TrimSpace(input.Issuer), "/")
input.Subject = strings.TrimSpace(input.Subject)
input.Username = strings.TrimSpace(input.Username)
input.DisplayName = limitOIDCProfileText(input.DisplayName, 200)
input.Email = limitOIDCProfileText(input.Email, 320)
input.Phone = limitOIDCProfileText(input.Phone, 64)
input.AvatarURL = limitOIDCProfileText(input.AvatarURL, 2048)
input.ContextType = strings.TrimSpace(input.ContextType)
input.TenantID = strings.TrimSpace(input.TenantID)
input.ApplicationID = strings.TrimSpace(input.ApplicationID)
@@ -359,6 +381,15 @@ func normalizeOIDCUserInput(input ResolveOrProvisionOIDCUserInput) ResolveOrProv
return input
}
func limitOIDCProfileText(value string, limit int) string {
value = strings.TrimSpace(value)
runes := []rune(value)
if len(runes) > limit {
return string(runes[:limit])
}
return value
}
func normalizeOIDCRoles(roles []string) []string {
result := make([]string, 0, len(roles))
seen := make(map[string]struct{}, len(roles))
@@ -390,6 +421,10 @@ func authUserFromOIDCProjection(user GatewayUser, userGroupKey string) *auth.Use
return &auth.User{
ID: user.ExternalUserID,
Username: user.Username,
DisplayName: user.DisplayName,
Email: user.Email,
Phone: user.Phone,
AvatarURL: user.AvatarURL,
Roles: user.Roles,
ContextType: "tenant",
TenantID: user.TenantID,