From 76e9d5b0e7088fa91f9ddd484fd701db7eace451 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 25 Aug 2026 22:44:19 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95/=E6=B3=A8=E5=86=8C/OAuth=20?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E7=BB=9F=E4=B8=80=E8=B5=B0=20SetLoginSession?= =?UTF-8?q?=EF=BC=8C=E4=BF=9D=E5=AD=98=E5=89=8D=E6=B8=85=E7=A9=BA=20Redis?= =?UTF-8?q?=20=E4=BC=9A=E8=AF=9D=20ID=EF=BC=8C=E5=A0=B5=E4=BD=8F=E6=9C=AA?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E4=BC=9A=E8=AF=9D=E5=9B=BA=E5=AE=9A=E3=80=82?= =?UTF-8?q?metric=20=E6=8C=81=E5=B9=B3=208=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":90,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126} --- .auto/log.jsonl | 1 + internal/apps/oauth/handler_callback.go | 2 +- internal/apps/oauth/session_context.go | 22 +++++++++-- internal/apps/user/routers.go | 52 +++---------------------- 4 files changed, 27 insertions(+), 50 deletions(-) diff --git a/.auto/log.jsonl b/.auto/log.jsonl index 7e89d61d..54ca8265 100644 --- a/.auto/log.jsonl +++ b/.auto/log.jsonl @@ -31,3 +31,4 @@ {"run":30,"commit":"d49c7e1","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":183,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126},"status":"checks_failed","description":"Agent 发现 Token 比较改为 SHA-256 后恒定时间 Compare,堵住未授权节点注册口的计时侧信道。checks 在 -race 阶段超时(包本身已单独跑绿)。","timestamp":1787667218065,"segment":0,"confidence":null,"asi":{"hypothesis":"discovery token 用 != 比较,未授权 /agent/nodes/register 可被计时;改 SHA-256 + ConstantTimeCompare","rollback_reason":"checks.sh 在 go test -race 阶段 300s 超时(包单独跑全绿,预算不够)","next_action_hint":"同一修复用 checks_timeout_seconds=600 重跑"}} {"run":31,"commit":"69055a9","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":71,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"未授权 Agent 注册口的 discovery token 改为 SHA-256 后恒定时间比较,堵住计时侧信道;空 token / 末字节翻转用例同步补上。metric 持平 8。","timestamp":1787667401636,"segment":0,"confidence":null,"asi":{"hypothesis":"discovery token 用 != 比较,未授权 /agent/nodes/register 可被计时;改 SHA-256 + ConstantTimeCompare","finding":"公开面注册口 ValidateDiscoveryToken 是入侵入口;管理员已登录操作不在范围内。checks 全绿。","next_action_hint":"下一轮可查边缘 Token 比较(agent/relay/flared 走 DB 查找,计时面更弱)或登录口限流"}} {"run":32,"commit":"fb62802","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":81,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"公开登录/注册邮箱验证码比较改为 SHA-256 后恒定时间 Compare,堵住未授权口的计时侧信道。metric 持平 8。","timestamp":1787667660993,"segment":0,"confidence":null,"asi":{"hypothesis":"verifyEmailCode 用 != 比较 6 位码,公开登录/注册口可被计时","finding":"公开面验证码比较已改恒定时间;冷却仍在,不改限流策略。","next_action_hint":"下一轮可查边缘节点 access_token 比较(DB 查找,计时面更弱)或登录失败锁定"}} +{"run":33,"commit":"b8bf82b","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":99,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126},"status":"keep","description":"未授权登录口补哑 bcrypt 比较,用户不存在与密码错误耗时对齐;禁用账号不再返回不同文案,堵住用户枚举。metric 持平 8。","timestamp":1787668237322,"segment":0,"confidence":null,"asi":{"hypothesis":"未授权 /user/login 在用户不存在时跳过 bcrypt,且禁用账号返回不同文案,可枚举用户","finding":"DummyCheckPassword 启动时生成哑哈希,gosec 不报警;禁用账号改统一错误文案。管理员已登录不在范围内。","next_action_hint":"下一轮可查边缘节点 access_token 明文比较,或公开 CAP challenge 滥用"}} diff --git a/internal/apps/oauth/handler_callback.go b/internal/apps/oauth/handler_callback.go index ed3dda69..874b7c18 100644 --- a/internal/apps/oauth/handler_callback.go +++ b/internal/apps/oauth/handler_callback.go @@ -176,7 +176,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth user.LastLoginAt = time.Now() _ = repository.UpdateUserLastLoginAt(ctx, user.ID, user.LastLoginAt) - if err := setLoginSession(ctx, c, &user); err != nil { + if err := SetLoginSession(ctx, c, &user); err != nil { response.AbortInternal(c, err.Error()) return } diff --git a/internal/apps/oauth/session_context.go b/internal/apps/oauth/session_context.go index 45237375..83076b17 100644 --- a/internal/apps/oauth/session_context.go +++ b/internal/apps/oauth/session_context.go @@ -14,6 +14,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/google/uuid" + gsessions "github.com/gorilla/sessions" ) // GetUserIDFromSession 从 Session 中提取用户 ID @@ -47,13 +48,29 @@ func hashSessionToken(token string) string { return hex.EncodeToString(h.Sum(nil)) } -func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error { +func rotateSessionID(s sessions.Session) { + if inner, ok := s.(interface{ Session() *gsessions.Session }); ok { + if sess := inner.Session(); sess != nil { + sess.ID = "" + } + } +} + +// SetLoginSession writes the authenticated user into a freshly rotated session. +func SetLoginSession(ctx context.Context, c *gin.Context, user *model.User, extras ...map[string]any) error { session := sessions.Default(c) + session.Clear() + rotateSessionID(session) + session.Set(UserIDKey, user.ID) session.Set(UserNameKey, user.Username) session.Set(PasswordHashKey, user.Password) + if len(extras) > 0 { + for key, value := range extras[0] { + session.Set(key, value) + } + } - // 根据系统配置动态设置 Session 过期时间 maxAge := config.Config.App.SessionAge isSessionCookie := false @@ -61,7 +78,6 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro if err == nil { switch { case ttlHours == -1: - // 永不过期,设置为 10 年 maxAge = 10 * 365 * 24 * 3600 case ttlHours > 0: maxAge = ttlHours * 3600 diff --git a/internal/apps/user/routers.go b/internal/apps/user/routers.go index 8ffc598a..9479bf65 100644 --- a/internal/apps/user/routers.go +++ b/internal/apps/user/routers.go @@ -4,17 +4,14 @@ package user import ( - "context" "net/http" "strings" "time" "github.com/Rain-kl/Wavelet/internal/apps/oauth" - "github.com/Rain-kl/Wavelet/internal/infra/config" "github.com/Rain-kl/Wavelet/internal/infra/persistence/idgen" "github.com/Rain-kl/Wavelet/internal/listener" "github.com/Rain-kl/Wavelet/internal/model" - "github.com/Rain-kl/Wavelet/internal/repository" pkgu "github.com/Rain-kl/Wavelet/pkg/util" "github.com/Rain-kl/Wavelet/internal/shared/response" "github.com/Rain-kl/Wavelet/pkg/logger" @@ -53,40 +50,6 @@ type updateProfileRequest struct { Location string `json:"location"` } -func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error { - session := sessions.Default(c) - session.Set(oauth.UserIDKey, user.ID) - session.Set(oauth.UserNameKey, user.Username) - session.Set(oauth.PasswordHashKey, user.Password) - - // 根据系统配置动态设置 Session 过期时间 - maxAge := config.Config.App.SessionAge - isSessionCookie := false - - ttlHours, err := repository.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours) - if err == nil { - switch { - case ttlHours == -1: - // 永不过期,设置为 10 年 - maxAge = 10 * 365 * 24 * 3600 - case ttlHours > 0: - maxAge = ttlHours * 3600 - case ttlHours == 0: - isSessionCookie = true - } - } - session.Options(oauth.GetSessionOptions(maxAge)) - - if err := session.Save(); err != nil { - return err - } - - if isSessionCookie { - oauth.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName) - } - - return nil -} // Login 用户密码登录 // @Summary 用户密码登录 @@ -150,21 +113,18 @@ func Login(c *gin.Context) { } } - session := sessions.Default(c) needChangePassword := isPlaintext - if isPlaintext { - session.Set("need_change_password", true) - } else { - session.Delete("need_change_password") - } - user.LastLoginAt = time.Now() if err := updateLastLogin(ctx, user); err != nil { response.AbortBadRequest(c, "更新登录时间失败,请稍后再试") return } - if err := setLoginSession(ctx, c, user); err != nil { + extras := map[string]any{} + if isPlaintext { + extras["need_change_password"] = true + } + if err := oauth.SetLoginSession(ctx, c, user, extras); err != nil { response.AbortBadRequest(c, errSaveSessionFailed) return } @@ -254,7 +214,7 @@ func Register(c *gin.Context) { return } - if err := setLoginSession(ctx, c, &user); err != nil { + if err := oauth.SetLoginSession(ctx, c, &user); err != nil { response.AbortBadRequest(c, errSaveSessionFailed) return }