package handler import ( "crypto/rand" "encoding/hex" "fmt" "net/http" "os" "path/filepath" "strings" "time" "github.com/gabriel-vasile/mimetype" "github.com/gin-gonic/gin" ) // ImageHandler 图片上传处理器 type ImageHandler struct { uploadDir string } // NewImageHandler 创建图片处理器 func NewImageHandler(uploadDir string) *ImageHandler { return &ImageHandler{uploadDir: uploadDir} } // Init 初始化上传目录 func (h *ImageHandler) Init() error { return os.MkdirAll(h.uploadDir, 0755) } // 允许的图片 MIME 类型(基于内容嗅探,而非仅扩展名) var allowedImageTypes = map[string]bool{ "image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true, "image/bmp": true, "image/svg+xml": false, // SVG 可含脚本,默认禁止,避免 XSS "image/x-icon": false, } // allowedExts 扩展名白名单(与内容嗅探双重校验) var allowedExts = map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, ".bmp": true, } // Upload 上传图片 func (h *ImageHandler) Upload(c *gin.Context) { file, err := c.FormFile("image") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请选择图片文件"}) return } // 验证扩展名 ext := strings.ToLower(filepath.Ext(file.Filename)) if !allowedExts[ext] { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "不支持的图片格式,仅支持 jpg、png、gif、webp、bmp"}) return } // 验证文件大小 (最大 5MB) if file.Size > 5*1024*1024 { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "图片大小不能超过 5MB"}) return } // 打开文件做内容嗅探,防止伪造扩展名上传恶意内容 src, err := file.Open() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "读取文件失败"}) return } defer src.Close() head := make([]byte, 512) if _, err := src.Read(head); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "读取文件失败"}) return } mime := mimetype.Detect(head) if !allowedImageTypes[mime.String()] && mime.String() != "" { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "文件内容不是合法图片"}) return } // 生成唯一文件名(使用安全随机源) filename := fmt.Sprintf("%d_%s%s", osTimeNano(), secureRandomString(8), ext) targetPath := filepath.Join(h.uploadDir, filename) // 保存文件 if err := c.SaveUploadedFile(file, targetPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "保存图片失败"}) return } // 返回访问 URL c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "上传成功", "data": gin.H{ "url": "/uploads/" + filename, "mime": mime.String(), "width": 0, "height": 0, }, }) } // osTimeNano 返回当前纳秒时间戳 func osTimeNano() int64 { return time.Now().UnixNano() } // secureRandomString 使用 crypto/rand 生成安全的随机十六进制字符串 func secureRandomString(bytesLen int) string { b := make([]byte, bytesLen) if _, err := rand.Read(b); err != nil { // 兜底(几乎不会发生) return fmt.Sprintf("%d", time.Now().UnixNano()) } return hex.EncodeToString(b) }