package auth import ( "backend/internal/model" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "os" "strconv" "strings" "time" "gorm.io/gorm" ) // 刷新令牌参数:不入库明文,只存 SHA-256;有效期默认 30 天(REFRESH_TOKEN_TTL_DAYS 可调) // Logan.Li const ( // refreshTokenPrefix 令牌前缀,便于在日志/抓包里识别类型 // Logan.Li refreshTokenPrefix = "opr_" // defaultRefreshTTLDays 默认有效期(天) // Logan.Li defaultRefreshTTLDays = 30 // maxRefreshTTLDays 有效期上限(天),防止配置成永久 // Logan.Li maxRefreshTTLDays = 365 ) // refreshTokenTTL 刷新令牌有效期,读环境变量 REFRESH_TOKEN_TTL_DAYS(非法值回落默认 30 天) // Logan.Li func refreshTokenTTL() time.Duration { days := defaultRefreshTTLDays if raw := strings.TrimSpace(os.Getenv("REFRESH_TOKEN_TTL_DAYS")); raw != "" { if n, err := strconv.Atoi(raw); err == nil && n > 0 && n <= maxRefreshTTLDays { days = n } } return time.Duration(days) * 24 * time.Hour } // hashRefreshToken 计算令牌摘要(SHA-256 十六进制) // Logan.Li func hashRefreshToken(raw string) string { sum := sha256.Sum256([]byte(raw)) return hex.EncodeToString(sum[:]) } // newRefreshTokenValue 生成 256 位随机令牌 // Logan.Li func newRefreshTokenValue() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return refreshTokenPrefix + base64.RawURLEncoding.EncodeToString(buf), nil } // truncate 截断超长 UA,避免撑爆列宽 // Logan.Li func truncate(text string, limit int) string { if len(text) <= limit { return text } return text[:limit] } // IssueRefreshToken 签发刷新令牌(返回明文与到期时间;库里只留摘要) // Logan.Li func (s *AuthService) IssueRefreshToken(userID uint, ip, userAgent string) (string, time.Time, error) { raw, err := newRefreshTokenValue() if err != nil { return "", time.Time{}, errors.New("刷新令牌生成失败") } now := time.Now() expiresAt := now.Add(refreshTokenTTL()) row := model.RefreshToken{ UserID: userID, TokenHash: hashRefreshToken(raw), ExpiresAt: expiresAt, IP: truncate(ip, 64), UserAgent: truncate(userAgent, 255), } if err := s.db.Create(&row).Error; err != nil { return "", time.Time{}, errors.New("刷新令牌保存失败") } // 顺手清理该用户的过期令牌,避免表无限增长 // Logan.Li s.db.Where("user_id = ? AND expires_at < ?", userID, now).Delete(&model.RefreshToken{}) return raw, expiresAt, nil } // Refresh 用刷新令牌换取新的访问令牌(一次性轮换 + 重放检测)。 // 返回值与登录响应同构,前端只需替换本地令牌对。 // Logan.Li func (s *AuthService) Refresh(raw, ip, userAgent string) (*LoginResult, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, errors.New("缺少刷新令牌") } var row model.RefreshToken if err := s.db.Where("token_hash = ?", hashRefreshToken(raw)).First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("登录状态已失效,请重新登录") } return nil, errors.New("读取登录状态失败") } // 重放检测:正常轮换后旧令牌必然已撤销,再次使用说明令牌被复制 → 撤销该用户全部令牌 // Logan.Li if row.RevokedAt != nil { s.RevokeAllForUser(row.UserID) return nil, errors.New("登录状态已失效,请重新登录") } if !row.ExpiresAt.After(time.Now()) { s.revokeRow(row.ID) return nil, errors.New("登录状态已过期,请重新登录") } var user model.User if err := s.db.First(&user, row.UserID).Error; err != nil { return nil, errors.New("账号不存在,请重新登录") } if !user.IsActive { return nil, errors.New("账号已被禁用") } accessToken, err := s.issueAccessToken(&user) if err != nil { return nil, err } newRaw, expiresAt, err := s.IssueRefreshToken(row.UserID, ip, userAgent) if err != nil { return nil, err } // 旧令牌置为已撤销,并记录轮换链 // Logan.Li now := time.Now() s.db.Model(&model.RefreshToken{}).Where("id = ?", row.ID).Updates(map[string]interface{}{ "revoked_at": &now, "rotated_to": hashRefreshToken(newRaw), "last_used": &now, }) return &LoginResult{ AccessToken: accessToken, RefreshToken: newRaw, RefreshExpiresAt: expiresAt, User: &user, }, nil } // Revoke 撤销指定刷新令牌(登出;幂等,找不到也返回成功) // Logan.Li func (s *AuthService) Revoke(raw string) { raw = strings.TrimSpace(raw) if raw == "" { return } now := time.Now() s.db.Model(&model.RefreshToken{}). Where("token_hash = ? AND revoked_at IS NULL", hashRefreshToken(raw)). Updates(map[string]interface{}{"revoked_at": &now}) } // RevokeAllForUser 撤销该用户全部刷新令牌(改密 / 重放检测 / 禁用账号时调用) // Logan.Li func (s *AuthService) RevokeAllForUser(userID uint) { if userID == 0 { return } now := time.Now() s.db.Model(&model.RefreshToken{}). Where("user_id = ? AND revoked_at IS NULL", userID). Updates(map[string]interface{}{"revoked_at": &now}) } // revokeRow 按主键撤销单行(内部用) // Logan.Li func (s *AuthService) revokeRow(id uint) { now := time.Now() s.db.Model(&model.RefreshToken{}).Where("id = ?", id). Updates(map[string]interface{}{"revoked_at": &now}) }