From 2fea315430ebe20c489791cf85fe17b92020b6a0 Mon Sep 17 00:00:00 2001 From: Wang Defa <1+wangdefa@noreply.gitea.bcde.io> Date: Tue, 14 Jul 2026 11:00:55 +0800 Subject: [PATCH] =?UTF-8?q?OIDC=20=E7=99=BB=E5=BD=95/=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E5=86=99=E7=B3=BB=E7=BB=9F=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ internal/api/authx.go | 27 +++++++++++++++++++++++++-- internal/api/routes_auth.go | 2 +- internal/service/oauth.go | 12 ++++++------ internal/service/totp_oauth_test.go | 6 +++--- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b47025..c76d88b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)(版本段不记日期),版本号遵循语义化版本。 +## [0.5.1] + +### Fixed + +- 修复外部身份(OAuth2)登录 / 绑定无系统日志的问题:回调成功记 200(用户名为面板账号)、失败记 401(附失败原因);此前回调为 GET 请求不经写操作日志中间件,完全无记录 + ## [0.5.0] ### Added diff --git a/internal/api/authx.go b/internal/api/authx.go index 965c7b7..d747353 100644 --- a/internal/api/authx.go +++ b/internal/api/authx.go @@ -5,10 +5,11 @@ import ( "net/http" "net/url" "strconv" + "time" "github.com/gin-gonic/gin" - _ "oci-portal/internal/model" // swagger 注解引用 + "oci-portal/internal/model" "oci-portal/internal/service" ) @@ -17,6 +18,7 @@ import ( type authxHandler struct { auth *service.AuthService oauth *service.OAuthService + logs *service.SystemLogService } // ---- TOTP(JWT 组内) ---- @@ -301,10 +303,12 @@ func (h *authxHandler) bearerUser(c *gin.Context) (string, bool) { // @Success 302 "登录 token 经 fragment 回前端,绑定回设置页" // @Router /api/v1/auth/oauth/{provider}/callback [get] func (h *authxHandler) oauthCallback(c *gin.Context) { + start := time.Now() provider := c.Param("provider") - token, _, mode, err := h.oauth.HandleCallback( + token, username, mode, err := h.oauth.HandleCallback( c.Request.Context(), provider, c.Query("state"), c.Query("code")) if err != nil { + h.recordOauth(c, username, http.StatusUnauthorized, oauthErrText(err), start) target := "/login" if mode == "bind" { target = "/settings" @@ -312,6 +316,7 @@ func (h *authxHandler) oauthCallback(c *gin.Context) { c.Redirect(http.StatusFound, target+"?oauthError="+url.QueryEscape(oauthErrText(err))) return } + h.recordOauth(c, username, http.StatusOK, "", start) if mode == "bind" { // 绑定模式:版本已递增,新 token 经 fragment 带回设置页无感换发 c.Redirect(http.StatusFound, "/settings?oauth=bound#oauthToken="+url.QueryEscape(token)) @@ -321,6 +326,24 @@ func (h *authxHandler) oauthCallback(c *gin.Context) { c.Redirect(http.StatusFound, "/login#oauthToken="+url.QueryEscape(token)) } +// recordOauth 把外部登录 / 绑定结果记入系统日志——回调是 GET, +// 不经写方法中间件;实际响应恒为 302,日志按语义记 200 / 401。 +func (h *authxHandler) recordOauth(c *gin.Context, username string, status int, errMsg string, start time.Time) { + if h.logs == nil { + return + } + h.logs.Record(model.SystemLog{ + Username: username, + Method: c.Request.Method, + Path: requestPath(c), + Status: status, + DurationMs: time.Since(start).Milliseconds(), + ClientIP: requestIP(c), + UserAgent: truncateLogField(c.Request.UserAgent(), 256), + ErrMsg: errMsg, + }) +} + // oauthErrText 把流程错误转为用户可读文案;内部错误不透出细节。 func oauthErrText(err error) string { for _, known := range []error{service.ErrOAuthNotConfigured, service.ErrOAuthNoAppURL, service.ErrOAuthDisabled, service.ErrOAuthState, service.ErrOAuthNotBound, service.ErrOAuthBound} { diff --git a/internal/api/routes_auth.go b/internal/api/routes_auth.go index 8b0441d..20c9203 100644 --- a/internal/api/routes_auth.go +++ b/internal/api/routes_auth.go @@ -12,7 +12,7 @@ func registerAuthPublic(v1 *gin.RouterGroup, auth *service.AuthService, oauth *s v1.POST("/auth/login", ah.login) // 外部身份登录:provider 列表 / 授权跳转(bind 模式 handler 内校验 JWT)/ 回调 - ax := &authxHandler{auth: auth, oauth: oauth} + ax := &authxHandler{auth: auth, oauth: oauth, logs: systemLogs} v1.GET("/auth/oauth/providers", ax.oauthProviders) v1.GET("/auth/oauth/:provider/authorize", ax.oauthAuthorize) v1.GET("/auth/oauth/:provider/callback", ax.oauthCallback) diff --git a/internal/service/oauth.go b/internal/service/oauth.go index 0c9a141..39a7531 100644 --- a/internal/service/oauth.go +++ b/internal/service/oauth.go @@ -187,7 +187,7 @@ type externalIdentity struct { // HandleCallback 完成授权码回调:换取身份后,bind 模式写绑定、login 模式签发 JWT; // token 仅 login 模式非空;mode 尽力返回(state 无效时为空),供 api 决定错误回跳页面。 -func (o *OAuthService) HandleCallback(ctx context.Context, provider, state, code string) (token, display, mode string, err error) { +func (o *OAuthService) HandleCallback(ctx context.Context, provider, state, code string) (token, username, mode string, err error) { p, err := o.takeState(provider, state) if err != nil { return "", "", "", err @@ -198,14 +198,14 @@ func (o *OAuthService) HandleCallback(ctx context.Context, provider, state, code } if p.mode == "bind" { if err := o.bind(ctx, p.username, provider, ident); err != nil { - return "", ident.Display, p.mode, err + return "", p.username, p.mode, err } // 绑定属敏感变更:版本递增使旧令牌失效,同时为操作者签新令牌随回跳带回 token, _, err := o.auth.RevokeSessions(ctx, p.username) - return token, ident.Display, p.mode, err + return token, p.username, p.mode, err } - token, display, err = o.loginByIdentity(ctx, provider, ident) - return token, display, p.mode, err + token, username, err = o.loginByIdentity(ctx, provider, ident) + return token, username, p.mode, err } // fetchIdentity 用授权码向 provider 换取稳定 subject 与展示名。 @@ -311,7 +311,7 @@ func (o *OAuthService) loginByIdentity(ctx context.Context, provider string, ide return "", "", fmt.Errorf("find bound user: %w", err) } token, _, err := o.auth.signToken(user.Username, user.TokenVersion) - return token, ident.Display, err + return token, user.Username, err } // Identities 列出账号已绑定的外部身份。 diff --git a/internal/service/totp_oauth_test.go b/internal/service/totp_oauth_test.go index 16e8dc5..8f7e8e8 100644 --- a/internal/service/totp_oauth_test.go +++ b/internal/service/totp_oauth_test.go @@ -169,12 +169,12 @@ func TestOAuthBindLoginUnbind(t *testing.T) { t.Errorf("重复绑定 err = %v, want ErrOAuthBound", err) } // 已绑定身份可登录并拿到有效 JWT - token, display, err := o.loginByIdentity(ctx, "github", externalIdentity{Subject: "10086", Display: "octocat"}) + token, loginUser, err := o.loginByIdentity(ctx, "github", externalIdentity{Subject: "10086", Display: "octocat"}) if err != nil || token == "" { t.Fatalf("loginByIdentity: %v", err) } - if display != "octocat" { - t.Errorf("display = %q", display) + if loginUser != "admin" { + t.Errorf("loginUser = %q, want admin", loginUser) } if username, err := auth.ParseToken(context.Background(), token); err != nil || username != "admin" { t.Errorf("token 应属 admin, got %q (%v)", username, err)