package middleware import ( "crypto/rand" "crypto/sha256" "encoding/hex" "net/http" "strconv" "strings" "time" "sql-platform/config" "sql-platform/internal/model" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // RefreshTokenTTL refresh token 有效期;access token 有效期仍走 session_timeout_hours const RefreshTokenTTL = 7 * 24 * time.Hour func hashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } // GenerateRefreshToken 签发 refresh token(明文只返回给客户端,库里只存 SHA-256 摘要) func GenerateRefreshToken(userID int) (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } token := hex.EncodeToString(buf) rt := model.RefreshToken{ UserID: userID, TokenHash: hashToken(token), ExpiresAt: time.Now().Add(RefreshTokenTTL), } if err := model.DB.Create(&rt).Error; err != nil { return "", err } // 顺手清理该用户已过期/已撤销的记录 model.DB.Where("user_id = ? AND (expires_at < ? OR revoked_at IS NOT NULL)", userID, time.Now()).Delete(&model.RefreshToken{}) return token, nil } // ConsumeRefreshToken 校验并轮换 refresh token(旧 token 撤销、签发新对),返回所属用户与新 refresh token。 func ConsumeRefreshToken(tokenStr string) (*model.User, string, error) { var rt model.RefreshToken if err := model.DB.Where("token_hash = ?", hashToken(tokenStr)).First(&rt).Error; err != nil { return nil, "", err } if rt.RevokedAt != nil || rt.ExpiresAt.Before(time.Now()) { return nil, "", ErrTokenRevoked } var user model.User if err := model.DB.First(&user, rt.UserID).Error; err != nil { return nil, "", err } if user.IsActive != nil && !*user.IsActive { return nil, "", ErrTokenRevoked } now := time.Now() rt.RevokedAt = &now model.DB.Save(&rt) newToken, err := GenerateRefreshToken(rt.UserID) if err != nil { return nil, "", err } return &user, newToken, nil } // RevokeUserRefreshTokens 撤销用户全部 refresh token(登出/改密时调用) func RevokeUserRefreshTokens(userID int) { now := time.Now() model.DB.Model(&model.RefreshToken{}). Where("user_id = ? AND revoked_at IS NULL", userID). Update("revoked_at", now) } // getSessionTimeoutHours reads session_timeout_hours from system_settings, // falls back to config/env, then default 24h. func getSessionTimeoutHours() int { var s model.SystemSetting if model.DB.Where("`key` = ?", "session_timeout_hours").Take(&s).Error == nil { if v, err := strconv.Atoi(s.Value); err == nil && v > 0 { return v } } if config.C.JWTExpire > 0 { return config.C.JWTExpire } return 24 } type Claims struct { UserID int `json:"user_id"` Username string `json:"username"` Role string `json:"role"` jwt.RegisteredClaims } // ErrTokenRevoked refresh token 已撤销/过期 var ErrTokenRevoked = errTokenRevoked{} type errTokenRevoked struct{} func (errTokenRevoked) Error() string { return "refresh token revoked or expired" } func GenerateToken(user *model.User) (string, error) { claims := Claims{ UserID: user.ID, Username: user.Username, Role: user.Role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(getSessionTimeoutHours()) * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.C.JWTSecret)) } func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Not authenticated"}) c.Abort() return } tokenStr := strings.TrimPrefix(authHeader, "Bearer ") if tokenStr == authHeader { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid token format"}) c.Abort() return } claims := &Claims{} token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { return []byte(config.C.JWTSecret), nil }) if err != nil || !token.Valid { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid or expired token"}) c.Abort() return } c.Set("user_id", claims.UserID) c.Set("username", claims.Username) c.Set("role", claims.Role) c.Next() } } func GetCurrentUser(c *gin.Context) (int, string, string) { return c.GetInt("user_id"), c.GetString("username"), c.GetString("role") } // RBAC middleware — allow specified roles (superadmin is treated as admin) func RequireRole(roles ...string) gin.HandlerFunc { roleSet := make(map[string]bool) for _, r := range roles { roleSet[r] = true } // superadmin inherits all admin privileges roleSet["superadmin"] = true return func(c *gin.Context) { role := c.GetString("role") if !roleSet[role] { c.JSON(http.StatusForbidden, gin.H{"detail": "Permission denied"}) c.Abort() return } c.Next() } }