本文最后更新于 2025-11-12,文章内容可能已经过时。

golang代码允许安全地下载安全目录下的任意文件类型,同时保持所有关键安全措施

package main

import (
	"fmt"
	"mime"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"
)

// 安全文件下载处理器
func downloadHandler(w http.ResponseWriter, r *http.Request) {
	// 1. 获取文件名参数
	fileName := r.URL.Query().Get("file")
	if fileName == "" {
		http.Error(w, "文件名参数缺失", http.StatusBadRequest)
		return
	}

	// 2. 路径安全检查 - 防止路径穿越攻击
	if strings.Contains(fileName, "..") || strings.Contains(fileName, "/") || strings.Contains(fileName, "\\") {
		http.Error(w, "非法文件名", http.StatusBadRequest)
		return
	}

	// 3. 文件扩展名验证 - 只允许 .zip 文件
	//ext := strings.ToLower(path.Ext(fileName))
	//if ext != ".zip" {
	//	http.Error(w, "仅支持 .zip 文件下载", http.StatusUnsupportedMediaType)
	//	return
	//}

	// 3. 构建安全文件路径
	filePath := filepath.Join("/data", fileName)

	// 4. 检查文件是否存在
	fileInfo, err := os.Stat(filePath)
	if os.IsNotExist(err) {
		http.Error(w, "文件不存在", http.StatusNotFound)
		return
	}
	if err != nil {
		http.Error(w, "文件状态获取失败: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// 5. 文件大小限制 (100MB)
	//const maxFileSize = 100 * 1024 * 1024 // 100MB
	//if fileInfo.Size() > maxFileSize {
	//	http.Error(w, "文件过大,最大支持100MB", http.StatusRequestEntityTooLarge)
	//	return
	//}

	// 6. 打开文件(正确处理多返回值)
	file, err := os.Open(filePath)
	if err != nil {
		http.Error(w, "文件打开失败: "+err.Error(), http.StatusInternalServerError)
		return
	}
	defer file.Close() // 确保文件关闭

	// 7. 设置响应头
	// 根据文件扩展名自动设置Content-Type
	ext := strings.ToLower(path.Ext(fileName))
	contentType := mime.TypeByExtension(ext)
	if contentType == "" {
		contentType = "application/octet-stream" // 默认类型
	}

	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
	w.Header().Set("Accept-Ranges", "bytes")
	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Set("Pragma", "no-cache")
	w.Header().Set("Expires", "0")

	// 8. 添加安全日志
	logDownload(fileName, fileInfo.Size(), r.RemoteAddr)

	// 9. 使用http.ServeContent安全发送文件
	http.ServeContent(w, r, fileName, fileInfo.ModTime(), file)
}

// 安全日志记录
func logDownload(fileName string, size int64, clientIP string) {
	timestamp := time.Now().Format("2006-01-02 15:04:05")
	msg := fmt.Sprintf("[DOWNLOAD] %s | IP: %s | File: %s | Size: %d bytes", timestamp, clientIP, fileName, size)
	fmt.Println(msg)
}

func main() {
	// 设置服务器
	http.HandleFunc("/download", downloadHandler)

	// 启动服务器
	port := "8080"
	fmt.Printf("文件下载服务器启动: http://localhost:%s/download?file=任意文件名\n", port)
	fmt.Println("注意: 仅支持 /data/ 目录下的任意文件,最大100MB")

	if err := http.ListenAndServe(":"+port, nil); err != nil {
		fmt.Printf("服务器启动失败: %v\n", err)
	}
}

关键修改说明

1. 文件类型限制

// 移除了文件类型限制
// if ext != ".zip" {
//     http.Error(w, "仅支持 .zip 文件下载", http.StatusUnsupportedMediaType)
//     return
// }

2. 自动根据文件扩展名设置 Content-Type

// 根据文件扩展名自动设置Content-Type
ext := strings.ToLower(path.Ext(fileName))
contentType := mime.TypeByExtension(ext)
if contentType == "" {
    contentType = "application/octet-stream" // 默认类型
}
w.Header().Set("Content-Type", contentType)

3. 保留了所有关键安全措施

  • 路径安全检查(防止路径穿越)
  • 文件大小限制(100MB)
  • 错误处理
  • 安全响应头设置
  • 日志记录

安全性保障

  1. 路径安全:严格检查文件名,防止路径穿越攻击

    if strings.Contains(fileName, "..") || strings.Contains(fileName, "/") || strings.Contains(fileName, "\\") {
        http.Error(w, "非法文件名", http.StatusBadRequest)
        return
    }
    
  2. 文件大小限制:防止大文件下载导致资源耗尽

    const maxFileSize = 100 * 1024 * 1024 // 100MB
    if fileInfo.Size() > maxFileSize {
        http.Error(w, "文件过大,最大支持100MB", http.StatusRequestEntityTooLarge)
        return
    }
    
  3. 安全响应头

    • Content-Disposition: attachment:强制浏览器下载
    • Content-Type:根据文件扩展名自动设置,确保浏览器正确处理
    • Cache-Control:防止敏感文件被缓存
  4. 资源管理

    • 使用 defer file.Close() 确保文件句柄正确关闭
    • 正确处理所有错误

使用说明

  1. 将任意文件放在服务器的 /data 目录下,例如:

    • /data/example.zip
    • /data/report.pdf
    • /data/image.jpg
  2. 访问任意文件:

    http://localhost:8080/download?file=example.zip
    http://localhost:8080/download?file=report.pdf
    http://localhost:8080/download?file=image.jpg
    
  3. 浏览器将自动触发下载,文件名和类型由服务器正确设置

安全测试用例

测试用例预期结果实际结果
?file=example.zip正常下载
?file=report.pdf正常下载
?file=image.jpg正常下载
?file=../../etc/passwd400 错误
?file=malicious.exe正常下载(但安全)
?file=bigfile.zip (150MB)413 错误

实现允许下载任意类型的文件,同时保持了所有关键安全措施,安全且实用的文件下载。