Golang中如何限制FileServer仅允许服务端访问

Golang中如何限制FileServer仅允许服务端访问 我有一个MySQL数据库,包含3张表:

  1. session表包含:id序列、邮箱、UUID、lastUpdate(用于刷新会话的时间戳)
  2. users-data表包含:id序列、邮箱、加密密码、名、姓、年龄、最喜欢的饮料
  3. user_images表包含:id序列、邮箱、图片路径

用户可以注册、登录(使用HttpOnly会话cookie)并上传图片(目前一次一张)。 所有用户图片都存储在服务器文件系统的user_images目录中,而文件路径则存储在数据库中。当用户注册时,会使用他们在user_data表中的id(例如user_id_6)在user_images目录内创建一个新目录,后续上传的图片都会放在这个目录里。

当用户成功登录后,他们会被引导到信息页面,我希望在该页面连续显示他们所有的图片。我从数据库中获取路径,然后将其放入字符串切片,并传递给一个HTML页面。

在HTML模板中,我遍历这个切片并为图片列表设置<img alt="一张图片">

现在有两个问题,希望能得到任何建议/帮助:

  1. src属性字符串中的""被转义,变成了类似src="user_images%5cuser_id_6%5cdog.jpg"的样子。 有没有办法防止正斜杠被转义?在Go中如何动态设置图片标签的src属性?

  2. 除非通过http.FileServer托管,否则文件本身无法通过路径访问。 有没有办法做到以下之一: a. 只有服务器可以访问提供的文件,外部连接的用户无法访问,这样我就可以提供整个user_images目录并挑选正确的图片来提供;或者 b. 为文件服务器添加身份验证,这样只有拥有有效会话的用户才能访问对应目录(根据他们的user_id)中的图片,而无法访问其他人的图片。

谢谢


更多关于Golang中如何限制FileServer仅允许服务端访问的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

一位朋友告诉我,可以使用 template.URL 来防止转义。反斜杠是因为我在 Windows 系统上,并且使用了 filepath.Join

能否请您提供一些示例代码,用于将会话 ID 映射到允许的文件?我是 Go 语言的新手,完全不知道从哪里开始。如果能提供一些文章或开源项目的链接,我将不胜感激。

更多关于Golang中如何限制FileServer仅允许服务端访问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


  1. %5c 是反斜杠的编码,而不是正斜杠。URL 不使用反斜杠来分隔路径组件,因此我不确定这是否能解决你的问题,但 StackOverflow 上的这个答案似乎回答了如何阻止转义斜杠的问题:https://stackoverflow.com/questions/38037615/prevent-escaping-forward-slashes-in-templates

  2. 你可能无法直接使用 FileServer 来实现这一点(也许可以通过包装器实现?),但我认为没有什么能阻止你自己实现这个功能。你可以维护一些服务器端状态,将有效的会话 ID 映射到允许访问的目录,如果请求中没有携带有效的用户会话 ID,则不显示任何图片。

针对你的两个问题,这里提供具体的解决方案:

问题1:防止路径转义

在HTML模板中使用template.URL类型来避免URL转义:

// 在处理器中准备数据
func userImagesHandler(w http.ResponseWriter, r *http.Request) {
    // 从数据库获取图片路径
    var imagePaths []string
    // ... 数据库查询代码
    
    // 转换为template.URL类型
    templatePaths := make([]template.URL, len(imagePaths))
    for i, path := range imagePaths {
        templatePaths[i] = template.URL(path)
    }
    
    data := struct {
        Images []template.URL
    }{
        Images: templatePaths,
    }
    
    // 渲染模板
    tmpl.Execute(w, data)
}

在HTML模板中:

{{range .Images}}
    <img src="{{.}}" alt="用户图片">
{{end}}

问题2:限制文件服务器访问

方案A:使用中间件验证会话

创建一个带身份验证的文件服务器:

// 认证中间件
func authFileServer(next http.Handler, db *sql.DB) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 1. 验证会话cookie
        cookie, err := r.Cookie("session_id")
        if err != nil {
            http.Error(w, "未授权", http.StatusUnauthorized)
            return
        }
        
        // 2. 验证会话有效性
        var userEmail string
        err = db.QueryRow("SELECT email FROM session WHERE uuid = ?", cookie.Value).Scan(&userEmail)
        if err != nil {
            http.Error(w, "会话无效", http.StatusUnauthorized)
            return
        }
        
        // 3. 验证用户是否有权访问该路径
        // 获取请求的路径并提取user_id
        path := r.URL.Path
        // 路径格式: /user_images/user_id_6/filename.jpg
        parts := strings.Split(path, "/")
        if len(parts) < 3 {
            http.Error(w, "无效路径", http.StatusForbidden)
            return
        }
        
        userDir := parts[2] // user_id_6
        expectedDir := "user_id_" + getUserIDFromEmail(db, userEmail)
        
        // 4. 检查用户是否只能访问自己的目录
        if userDir != expectedDir {
            http.Error(w, "禁止访问", http.StatusForbidden)
            return
        }
        
        // 5. 继续处理文件请求
        next.ServeHTTP(w, r)
    })
}

// 获取用户ID
func getUserIDFromEmail(db *sql.DB, email string) string {
    var userID string
    db.QueryRow("SELECT id FROM users_data WHERE email = ?", email).Scan(&userID)
    return userID
}

// 在主函数中设置路由
func main() {
    db := initDB()
    
    // 创建文件服务器
    fs := http.FileServer(http.Dir("./user_images"))
    
    // 使用认证中间件包装文件服务器
    authFs := authFileServer(fs, db)
    
    // 注册路由
    http.Handle("/user_images/", http.StripPrefix("/user_images/", authFs))
    
    http.ListenAndServe(":8080", nil)
}

方案B:通过处理器动态提供文件

func serveUserImage(w http.ResponseWriter, r *http.Request) {
    // 1. 验证会话
    cookie, err := r.Cookie("session_id")
    if err != nil {
        http.Error(w, "未授权", http.StatusUnauthorized)
        return
    }
    
    // 2. 获取用户信息
    var userEmail string
    err = db.QueryRow("SELECT email FROM session WHERE uuid = ?", cookie.Value).Scan(&userEmail)
    if err != nil {
        http.Error(w, "会话无效", http.StatusUnauthorized)
        return
    }
    
    // 3. 获取用户ID
    var userID string
    err = db.QueryRow("SELECT id FROM users_data WHERE email = ?", userEmail).Scan(&userID)
    if err != nil {
        http.Error(w, "用户不存在", http.StatusNotFound)
        return
    }
    
    // 4. 从URL参数获取文件名
    filename := r.URL.Query().Get("file")
    if filename == "" {
        http.Error(w, "文件名不能为空", http.StatusBadRequest)
        return
    }
    
    // 5. 构建完整路径
    imagePath := filepath.Join("user_images", "user_id_"+userID, filename)
    
    // 6. 验证文件是否存在且属于该用户
    // 可以添加数据库验证,确保该文件确实属于该用户
    var dbPath string
    err = db.QueryRow("SELECT image_path FROM user_images WHERE email = ? AND image_path LIKE ?", 
        userEmail, "%"+filename).Scan(&dbPath)
    if err != nil {
        http.Error(w, "文件不存在或无权访问", http.StatusForbidden)
        return
    }
    
    // 7. 提供文件
    http.ServeFile(w, r, imagePath)
}

// 在HTML模板中生成链接
func generateImageLinks(userID string, filenames []string) []template.URL {
    var links []template.URL
    for _, filename := range filenames {
        link := template.URL(fmt.Sprintf("/image?file=%s", filename))
        links = append(links, link)
    }
    return links
}

方案C:使用符号链接和私有目录

// 创建安全的文件服务器结构
func setupSecureFileServer() {
    // 主目录结构
    // ./private_images/ - 实际存储位置(不对外公开)
    // ./public_links/ - 符号链接目录(通过文件服务器提供)
    
    // 用户登录时创建符号链接
    func createUserSymlink(userID string) error {
        privatePath := filepath.Join("private_images", "user_id_"+userID)
        publicPath := filepath.Join("public_links", "user_id_"+userID)
        
        // 创建私有目录
        os.MkdirAll(privatePath, 0755)
        
        // 创建符号链接(仅Unix系统)
        return os.Symlink(privatePath, publicPath)
    }
    
    // 文件服务器只提供public_links目录
    http.Handle("/images/", 
        http.StripPrefix("/images/", 
            http.FileServer(http.Dir("./public_links/"))))
}

这些方案可以确保只有经过身份验证的用户才能访问他们自己的图片文件,同时防止路径遍历攻击和未授权访问。

回到顶部