做 Web 应用程序时,经常需要对用户上传的文件类型做一下检查,比如判断上传的是否是 png 、gif、jpg 等图片类型,还是 pdf。并针对不同的类型做一些处理,比如在需要图片的场合,如果上传的文件类型为非图片,那么就要拒绝并告诉用户需要一张图片。

这种需求,往往我们都是通过上传的文件的扩展名来判断,比如如果以 .jpg 结尾,那么我们可能就认为是 jpg 图片。但这种判断方式往往存在一个隐患,就是恶意用户可能会把一些恶意程序简单的以 .jpg 结尾来骗过我们的逻辑。

我们也可以通过Content-Type判断文件的MIME类型进行判断,我们在通过form表单上传文件时,在上传的request域里面会获取当前文件的MIME类型,我们可以通过控制接收文件的MIME类型进行判断。这个方法如果通过抓包的形式进行修改类型也不安全。

在这种情况下,最好的方式就是根据上传内容的前几个字节来判断。根据上传的内容来判断上传的内容类型,一般情况前三个字节或者前八个字节就可以了。比如 PNG 格式的图片,以十六进制 89504E47 开头

Go 语言的 net/http 包下的方法 http.DetectContentType() 就是使用了刚刚我们提到的前几个字节判断文件类型的方式。

http.DetectContentType() 方法会读取内容的前 512 个字节的内容,并根据它们判断文件的类型,然后返回该文件的 MIME 类型。如果是未知类型,则会返回 application/octet-stream

其实一般情况下用不了 512 个字节,最多使用前 32 个字节。

我们写一个范例,使用 http.DetectContentType() 返回文件的 MINE 类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
    "os"
    "fmt"
    "net/http"
)

func main() {

    // Open File
    f, err := os.Open("test.pdf")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    // Get the content
    contentType, err := GetFileContentType(f)
    if err != nil {
        panic(err)
    }

    fmt.Println("Content Type: " + contentType)
}

func GetFileContentType(out *os.File) (string, error) {

    // 只需要前 512 个字节就可以了
    buffer := make([]byte, 512)

    _, err := out.Read(buffer)
    if err != nil {
        return "", err
    }

    contentType := http.DetectContentType(buffer)

    return contentType, nil
}