Skip to content

Commit 9e0b43b

Browse files
authored
Merge pull request #523 from LydiaCai1203/fix-bind-email-token
fix: 邮件 token 改用 UUID,避免 base32 填充被邮件链路破坏
2 parents 4a89950 + 89e1881 commit 9e0b43b

2 files changed

Lines changed: 16 additions & 39 deletions

File tree

backend/biz/team/usecase/user.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/chaitin/MonkeyCode/backend/db"
1616
"github.com/chaitin/MonkeyCode/backend/domain"
1717
"github.com/chaitin/MonkeyCode/backend/errcode"
18-
"github.com/chaitin/MonkeyCode/backend/pkg/crypto"
1918
"github.com/chaitin/MonkeyCode/backend/pkg/cvt"
2019
)
2120

@@ -269,12 +268,10 @@ func (u *TeamGroupUserUsecase) UpdateUser(ctx context.Context, req *domain.Updat
269268
}
270269

271270
// generateResetPWDToken 生成重置密码的 token
271+
// 使用 UUID 作为随机 handle,实际过期时间由 Redis TTL 控制,
272+
// 避免 base32 填充字符在邮件传输中被破坏。
272273
func (u *TeamGroupUserUsecase) generateResetPWDToken(ctx context.Context, userID uuid.UUID) (string, error) {
273-
token, err := crypto.Simple(userID.String(), time.Now().Add(time.Hour*24*3))
274-
if err != nil {
275-
return "", err
276-
}
277-
return token, nil
274+
return uuid.NewString(), nil
278275
}
279276

280277
// sendResetPasswordEmail 发送重置密码邮件

backend/biz/user/usecase/user.go

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/chaitin/MonkeyCode/backend/db"
1616
"github.com/chaitin/MonkeyCode/backend/domain"
1717
"github.com/chaitin/MonkeyCode/backend/errcode"
18-
"github.com/chaitin/MonkeyCode/backend/pkg/crypto"
1918
"github.com/chaitin/MonkeyCode/backend/pkg/cvt"
2019
)
2120

@@ -159,16 +158,12 @@ func (u *UserUsecase) SendBindEmailVerification(ctx context.Context, userID uuid
159158
return errcode.ErrEmailTaken
160159
}
161160

162-
// 生成验证 token
163-
token, err := crypto.Simple(userID.String(), time.Now().Add(time.Hour*24))
164-
if err != nil {
165-
u.logger.ErrorContext(ctx, "generate bind email token failed", "error", err)
166-
return errcode.ErrInternalServer.Wrap(err)
167-
}
161+
// 生成验证 token(使用 UUID,避免 base32 填充字符在邮件传输中被破坏)
162+
token := uuid.NewString()
168163

169-
// 存储 token 到 Redis,格式:{token}:{email},有效期 24 小时
170-
key := fmt.Sprintf("bind_email_token:%s", userID.String())
171-
value := fmt.Sprintf("%s:%s", token, req.Email)
164+
// 存储 token 到 Redis,key: bind_email_token:{token},value: {userID}:{email},有效期 24 小时
165+
key := fmt.Sprintf("bind_email_token:%s", token)
166+
value := fmt.Sprintf("%s:%s", userID.String(), req.Email)
172167
if err := u.redis.Set(ctx, key, value, time.Hour*24).Err(); err != nil {
173168
u.logger.ErrorContext(ctx, "set redis key failed", "error", err)
174169
return errcode.ErrDatabaseOperation.Wrap(err)
@@ -194,21 +189,8 @@ func (u *UserUsecase) SendBindEmailVerification(ctx context.Context, userID uuid
194189

195190
// VerifyBindEmail 验证邮箱绑定
196191
func (u *UserUsecase) VerifyBindEmail(ctx context.Context, token string) error {
197-
// 验证 token 的有效性(检查签名和过期时间)
198-
userIDStr, err := crypto.ValidateSimple(token)
199-
if err != nil {
200-
u.logger.WarnContext(ctx, "validate token failed", "error", err)
201-
return errcode.ErrEmailVerifyFailed.Wrap(err)
202-
}
203-
204-
userID, err := uuid.Parse(userIDStr)
205-
if err != nil {
206-
u.logger.WarnContext(ctx, "parse user id from token failed", "error", err)
207-
return errcode.ErrEmailVerifyFailed.Wrap(err)
208-
}
209-
210-
// 从 Redis 中取出存储的 token 和邮箱(一次性消费)
211-
key := fmt.Sprintf("bind_email_token:%s", userID.String())
192+
// 以 token 为 key 从 Redis 中取出 userID 和邮箱(一次性消费)
193+
key := fmt.Sprintf("bind_email_token:%s", token)
212194
redisValue, err := u.redis.GetDel(ctx, key).Result()
213195
if err != nil {
214196
if err == redis.Nil {
@@ -218,21 +200,19 @@ func (u *UserUsecase) VerifyBindEmail(ctx context.Context, token string) error {
218200
return errcode.ErrDatabaseOperation.Wrap(err)
219201
}
220202

221-
// 解析 Redis 中的值:{token}:{email}
203+
// 解析 Redis 中的值:{userID}:{email}
222204
parts := strings.SplitN(redisValue, ":", 2)
223205
if len(parts) != 2 {
224206
u.logger.WarnContext(ctx, "invalid redis value format", "value", redisValue)
225207
return errcode.ErrEmailVerifyFailed
226208
}
227209

228-
storedToken := parts[0]
229-
email := parts[1]
230-
231-
// 验证 token 是否匹配(防止 token 替换)
232-
if storedToken != token {
233-
u.logger.WarnContext(ctx, "token mismatch")
234-
return errcode.ErrEmailVerifyFailed
210+
userID, err := uuid.Parse(parts[0])
211+
if err != nil {
212+
u.logger.WarnContext(ctx, "parse user id from redis value failed", "error", err)
213+
return errcode.ErrEmailVerifyFailed.Wrap(err)
235214
}
215+
email := parts[1]
236216

237217
// 再次检查邮箱是否被其他用户占用(防止竞态条件)
238218
existingUsers, err := u.repo.GetUserByEmail(ctx, []string{email})

0 commit comments

Comments
 (0)