使用 Go 的 FileServer 安全地提供静态文件

本文介绍了如何使用 Go 语言的 net/http 包中的 FileServer 函数来安全地提供静态文件,特别是如何在只允许客户端访问特定文件(例如 index.html)的同时,保护其他文件(例如 JavaScript 文件)不被直接访问。文章提供了两种实现方法:使用中间件包装 FileServer 和自定义 http.FileSystem 接口。

在使用 Go 构建 Web 应用时,经常需要提供静态文件,例如 HTML、CSS、JavaScript 和图片。net/http 包中的 FileServer 函数提供了一种简单的方式来实现这个功能。然而,默认情况下,FileServer 会暴露指定目录下的所有文件,这可能带来安全风险。例如,你可能希望只允许用户访问 index.html,而阻止直接访问 JavaScript 文件。本文将介绍两种方法来解决这个问题。

方法一:使用中间件过滤文件

第一种方法是使用中间件来包装 FileServer,通过中间件来过滤客户端的请求,只允许访问特定类型的文件。下面是一个示例代码:

package main

import (
    "log"
    "net/http"
    "path/filepath"
)

// GlobFilterHandler 是一个中间件,用于过滤文件,只允许访问符合指定模式的文件。
func GlobFilterHandler(h http.Handler, pattern string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        path := r.URL.Path
        fileName := filepath.Base(path)

        ok, err := filepath.Match(pattern, fileName)
        if err != nil {
            log.Println("Error in pattern match:", err)
            http.Error(w, "Internal Server Error", http.StatusInternalServerError) // 更加友好的错误提示
            return
        }
        if !ok {
            http.NotFound(w, r)
            return
        }

        h.ServeHTTP(w, r)
    })
}

func main() {
    // 假设静态文件位于 /tmp/dtest 目录
    fileHandler := http.FileServer(http.Dir("/tmp/dtest"))

    // 只允许访问 *.js 文件
    wrappedHandler := GlobFilterHandler(fileHandler, "*.js")

    // 将处理程序注册到服务器
    http.Handle("/", wrappedHandler)  // 注意这里需要指定路由

    // 启动服务器
    log.Fatal(http.ListenAndServe(":8080", nil))
}

代码解释:

  1. GlobFilterHandler 函数接收一个 http.Handler 和一个模式字符串作为参数,返回一个新的 http.Handler。
  2. 新的 http.Handler 会检查请求的 URL 路径,提取文件名,并使用 filepath.Match 函数来判断文件名是否符合指定的模式。
  3. 如果文件名不符合模式,或者匹配过程中发生错误,则返回 404 Not Found 错误。
  4. 如果文件名符合模式,则调用原始的 http.Handler 来处理请求。

使用方法:

  1. 将上述代码保存为 main.go 文件。
  2. 创建一个名为 /tmp/dtest 的目录,并在该目录下创建一些文件,例如 index.html 和 script.js。
  3. 运行 go run main.go 命令启动服务器。
  4. 在浏览器中访问 http://localhost:8080/script.js 可以正常访问,访问 http://localhost:8080/index.html 会返回 404 Not Found 错误。

注意事项:

  • 这个示例只允许访问 .js 文件。你可以根据需要修改模式字符串来允许访问其他类型的文件。
  • 需要注意路由的设置,确保中间件能够正确地拦截请求。

方法二:自定义 http.FileSystem 接口

第二种方法是自定义 http.FileSystem 接口,并实现自己的文件访问控制逻辑。下面是一个示例代码:

package main

import (
    "fmt"
    "log"
    "net/http"
    "path/filepath"
)

// GlobDir 是一个自定义的 http.FileSystem 实现,用于过滤文件,只允许访问符合指定模式的文件。
type GlobDir struct {
    Dir     http.Dir
    Pattern string
}

// Open 方法实现了 http.FileSystem 接口,用于打开文件。
func (d GlobDir) Open(name string) (http.File, error) {
    baseName := filepath.Base(name)

    ok, err := filepath.Match(d.Pattern, baseName)
    if err != nil {
        return nil, err
    }
    if !ok {
        return nil, fmt.Errorf("%s not match GlobDir pattern.", baseName)
    }

    return d.Dir.Open(name)
}

func main() {
    // 假设静态文件位于 /tmp/dtest 目录
    fileHandler := http.FileServer(GlobDir{
        Dir:     http.Dir("/tmp/dtest"),
        Pattern: "*.js",
    })

    // 将处理程序注册到服务器
    http.Handle("/", fileHandler) // 注意这里需要指定路由

    // 启动服务器
    log.Fatal(http.ListenAndServe(":8080", nil))
}

代码解释:

  1. GlobDir 结构体实现了 http.FileSystem 接口,包含一个 http.Dir 类型的成员和一个模式字符串。
  2. Open 方法是 http.FileSystem 接口的必须实现的方法,用于打开文件。
  3. Open 方法首先提取文件名,并使用 filepath.Match 函数来判断文件名是否符合指定的模式。
  4. 如果文件名不符合模式,则返回一个错误。
  5. 如果文件名符合模式,则调用原始的 http.Dir 的 Open 方法来打开文件。

使用方法:

  1. 将上述代码保存为 main.go 文件。
  2. 创建一个名为 /tmp/dtest 的目录,并在该目录下创建一些文件,例如 index.html 和 script.js。
  3. 运行 go run main.go 命令启动服务器。
  4. 在浏览器中访问 http://localhost:8080/script.js 可以正常访问,访问 http://localhost:8080/index.html 会返回一个错误。

注意事项:

  • 这个示例只允许访问 .js 文件。你可以根据需要修改模式字符串来允许访问其他类型的文件。
  • 自定义 http.FileSystem 接口可以提供更灵活的文件访问控制逻辑。

总结

本文介绍了两种使用 Go 的 FileServer 安全地提供静态文件的方法:使用中间件过滤文件和自定义 http.FileSystem 接口。你可以根据实际需求选择合适的方法。使用中间件的方法更加简单,但灵活性稍差。自定义 http.FileSystem 接口的方法更加灵活,但实现起来稍微复杂一些。无论选择哪种方法,都需要注意文件访问控制,确保Web应用的安全。