fix/status-review-sync-20260409 #1
@@ -117,10 +117,16 @@ type UserInfo struct {
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
User *UserInfo `json:"user"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
User *UserInfo `json:"user,omitempty"`
|
||||
// RequiresTOTP 指示登录需要额外的TOTP验证(当设备未信任时)
|
||||
RequiresTOTP bool `json:"requires_totp,omitempty"`
|
||||
// TempToken 临时令牌,用于TOTP验证阶段(短生命周期,不可用于常规API)
|
||||
TempToken string `json:"temp_token,omitempty"`
|
||||
// UserID 当RequiresTOTP为true时返回,用于后续TOTP验证
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
type LogoutRequest struct {
|
||||
@@ -751,6 +757,16 @@ func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) (
|
||||
_ = s.cache.Delete(ctx, attemptKey)
|
||||
}
|
||||
|
||||
// P0-07 安全修复:检查是否需要TOTP验证(用户启用了TOTP且设备未信任)
|
||||
if s.isTOTPRequiredForLogin(ctx, user, req.DeviceID) {
|
||||
// 返回RequiresTOTP指示前端需要完成TOTP验证
|
||||
// 前端应调用 /auth/login/totp-verify 接口完成验证
|
||||
return &LoginResponse{
|
||||
RequiresTOTP: true,
|
||||
UserID: user.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
s.bestEffortUpdateLastLogin(ctx, user.ID, ip, "password")
|
||||
s.cacheUserInfo(ctx, user)
|
||||
s.writeLoginLog(ctx, &user.ID, domain.LoginTypePassword, ip, true, "")
|
||||
@@ -766,6 +782,55 @@ func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) (
|
||||
return s.generateLoginResponse(ctx, user, req.Remember)
|
||||
}
|
||||
|
||||
// isTOTPRequiredForLogin 检查登录是否需要TOTP验证
|
||||
// 条件:用户启用了TOTP且尝试登录的设备未信任
|
||||
func (s *AuthService) isTOTPRequiredForLogin(ctx context.Context, user *domain.User, deviceID string) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
// 检查用户是否启用了TOTP
|
||||
if !user.TOTPEnabled || strings.TrimSpace(user.TOTPSecret) == "" {
|
||||
return false
|
||||
}
|
||||
// 检查设备是否已信任
|
||||
if deviceID != "" && s.deviceService != nil {
|
||||
device, err := s.deviceService.GetDeviceByDeviceID(ctx, user.ID, deviceID)
|
||||
if err == nil && device.IsTrusted {
|
||||
// 设备已信任,检查信任是否过期
|
||||
if device.TrustExpiresAt == nil || device.TrustExpiresAt.After(time.Now()) {
|
||||
return false // 设备已信任且未过期,不需要TOTP
|
||||
}
|
||||
}
|
||||
}
|
||||
return true // 需要TOTP验证
|
||||
}
|
||||
|
||||
// VerifyTOTPAfterPasswordLogin 完成密码登录后的TOTP验证
|
||||
// 当用户启用了TOTP但设备未信任时,密码登录会返回RequiresTOTP=true
|
||||
// 前端需要调用此接口完成TOTP验证以获取令牌
|
||||
func (s *AuthService) VerifyTOTPAfterPasswordLogin(ctx context.Context, userID int64, totpCode, deviceID string) (*LoginResponse, error) {
|
||||
if s == nil {
|
||||
return nil, errors.New("auth service is not initialized")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
|
||||
if err := s.ensureUserActive(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证TOTP
|
||||
if err := s.VerifyTOTP(ctx, userID, totpCode, deviceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TOTP验证成功,返回完整登录响应
|
||||
return s.generateLoginResponseWithoutRemember(ctx, user)
|
||||
}
|
||||
|
||||
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||
if s == nil || s.jwtManager == nil || s.userRepo == nil {
|
||||
return nil, errors.New("auth service is not fully configured")
|
||||
|
||||
Reference in New Issue
Block a user