From a51501909a6fe8fccc80f294608611a80666dba8 Mon Sep 17 00:00:00 2001 From: becomer Date: Wed, 21 Oct 2026 17:33:41 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=91=E5=AE=9A=E9=82=80=E8=AF=B7=E7=A0=81?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BD=A2=E6=80=81=E9=A2=84=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=85=BC=E5=AE=B9=206=20=E4=BD=8D=E5=AD=98?= =?UTF-8?q?=E9=87=8F=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 预校验(避免无用查库) 原先任何非空字符串都会直接拿去查库。邀请码固定长度且只用 inviteCodeChars(去掉 0/O/1/L 后的 31 个字符),不合规的输入 可以无需查库就拒绝;用统一文案「邀请码无效」,不泄露额外信息。 2. 存量兼容(关键,差点造成线上事故) 最初把校验写成「必须 8 位」。查生产库发现: 6 位:551 个用户 8 位:1 个用户 即绝大多数用户的邀请码仍是 6 位旧码。若只接受 8 位, 这 551 人的码会被判无效,他们将无法被新用户填写邀请—— 影响面很大。已改为同时接受 6 位与 8 位,字符集校验不变。 测试覆盖:8 位正常、6 位存量、5/7 位、空串、含易混淆字符 (0/O/1/I/L)、含小写、含符号、含中文。 --- backed/internal/handler/auth.go | 8 ++++- backed/internal/handler/invite_code.go | 24 +++++++++++++ backed/internal/handler/invite_code_test.go | 39 +++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/backed/internal/handler/auth.go b/backed/internal/handler/auth.go index 9d0342d..944680b 100644 --- a/backed/internal/handler/auth.go +++ b/backed/internal/handler/auth.go @@ -1,13 +1,13 @@ package handler import ( + "context" "cpdaily-new/internal/config" "cpdaily-new/internal/database" "cpdaily-new/internal/model" "cpdaily-new/internal/service" cpd "cpdaily-new/internal/service/cpdaily" "cpdaily-new/internal/utils" - "context" "crypto/rand" "encoding/hex" "encoding/json" @@ -585,6 +585,12 @@ func BindInviteCode(c *fiber.Ctx) error { if code == "" { return utils.FailParam(c, "请输入邀请码") } + // 格式预校验:邀请码固定 8 位且只用 inviteCodeChars 字符集。 + // 不合规的输入直接拒绝,避免每次都在库里做一次无用查询; + // 用统一文案,不泄露「码存在但格式对」这类信息。 + if !isValidInviteCodeShape(code) { + return utils.Fail(c, 400, "邀请码无效") + } var user model.SysUser if err := database.DB.First(&user, userID).Error; err != nil { diff --git a/backed/internal/handler/invite_code.go b/backed/internal/handler/invite_code.go index d9ba103..9cdffed 100644 --- a/backed/internal/handler/invite_code.go +++ b/backed/internal/handler/invite_code.go @@ -205,3 +205,27 @@ func ResetInviteCode(c *fiber.Ctx) error { slog.Info("用户重置邀请码", "userId", userID, "oldCode", oldCode, "newCode", code) return utils.Success(c, fiber.Map{"inviteCode": code}) } + +// inviteCodeLegacyLength 旧版邀请码长度。 +// +// 2026-10 之前生成为 6 位,生产库中仍有大量存量用户沿用旧码 +// (实测 551 个 6 位 / 1 个 8 位)。校验必须同时接受两种长度, +// 否则存量用户的码会被判为无效,导致他们无法被邀请。 +const inviteCodeLegacyLength = 6 + +// isValidInviteCodeShape 校验邀请码的形态(长度 + 字符集),不查库。 +// +// 长度接受 6(存量)与 8(当前)两种,字符集为 inviteCodeChars +// (已去掉易混淆的 0/O/1/L)。不合规的输入无需查库即可拒绝, +// 也避免把用户的任意长字符串当码去查。 +func isValidInviteCodeShape(code string) bool { + if len(code) != inviteCodeLength && len(code) != inviteCodeLegacyLength { + return false + } + for i := 0; i < len(code); i++ { + if !strings.ContainsRune(inviteCodeChars, rune(code[i])) { + return false + } + } + return true +} diff --git a/backed/internal/handler/invite_code_test.go b/backed/internal/handler/invite_code_test.go index 63ddf0f..76beb38 100644 --- a/backed/internal/handler/invite_code_test.go +++ b/backed/internal/handler/invite_code_test.go @@ -163,3 +163,42 @@ func TestErrInviteCodeChangedIsDistinct(t *testing.T) { t.Fatal("errors.Is 自反性失败") } } + +// isValidInviteCodeShape:只校验形态(长度 + 字符集),不查库 +func TestIsValidInviteCodeShape(t *testing.T) { + valid, err := newInviteCode() + if err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + code string + want bool + }{ + {"正常 8 位", valid, true}, + {"空串", "", false}, + {"过短", "ABC", false}, + {"过长", "ABCDEFGHJ", false}, + {"含易混淆字符 0", "ABCD0EFG", false}, + {"含易混淆字符 O", "ABCDOEFG", false}, + {"含易混淆字符 1", "ABCD1EFG", false}, + {"含易混淆字符 I", "ABCDIEFG", false}, + {"含易混淆字符 L", "ABCDLEFG", false}, + {"含小写", "abcdefgh", false}, + {"含符号", "ABCD-EFG", false}, + {"含中文", "邀请码邀请码邀请", false}, + // 存量兼容:生产库仍有 551 个 6 位旧码,必须仍能正常绑定 + {"存量 6 位码", "ABCDEF", true}, + {"存量 6 位含易混淆字符", "ABCD0F", false}, + {"5 位", "ABCDE", false}, + {"7 位", "ABCDEFG", false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := isValidInviteCodeShape(c.code); got != c.want { + t.Fatalf("isValidInviteCodeShape(%q)=%v, want %v", c.code, got, c.want) + } + }) + } +}