☰
通过16进制获取图片视频等文件的类型:TaoToken 统一 Key 通道下的文件头识别配置与验证
2026/9/29 2:59:28 网站建设 项目流程

1. 为什么扩展名靠不住:从一次上传事故说起

你可能遇到过这种场景:用户上传了一张头像,文件名是avatar.jpg,前端预览正常,后端也按图片处理,结果某天安全扫描报出这个文件其实是个可执行脚本。或者更常见的,运营同学把demo.mp4改成demo.jpg想绕过上传限制,系统照单全收。这类问题的根子在于——扩展名只是文件名的一部分,它不携带任何关于文件真实内容的信息。

文件类型识别的正确姿势是读文件开头的若干字节,也就是常说的文件头或魔数(magic number)。不同格式的文件在开头会写入固定的字节序列,比如 JPEG 通常以FF D8 FF开头,PNG 固定是89 50 4E 47,GIF 是47 49 46 38。这些字节是文件格式规范里写死的,改扩展名改不掉它们。用 16 进制视角去看文件头,就能在扩展名被篡改、缺失甚至伪造的情况下,判断出文件的真实类型。

这篇内容面向的是需要做上传校验、文件网关、内容审核前置判断的后端和全栈同学。我会给出可复制的文件头映射配置、一段能直接跑的校验脚本骨架,并说明怎么通过 TaoToken 的统一 Key 通道把识别流程接进你的服务里,最后用改名文件和截断文件做两轮验证。整套东西不需要你装 UltraEdit 这类工具,用命令行和几行代码就能拿到 16 进制。

2. TaoToken 前置:统一 Key 通道解决什么

在讲配置之前,先说清楚为什么这里要引入 TaoToken。文件类型识别本身是个纯本地逻辑,读字节、查表、返回类型,跟网络没关系。但实际项目里,识别往往只是链路的一环:识别完之后你可能要调用模型做内容理解、要生成缩略图描述、要把结果写进审核日志。如果每个环节都单独配一套 Key、一套鉴权、一套计费口径,维护成本会迅速膨胀。

TaoToken 提供的是统一 Key 通道:一个 API Key 走通模型对话、编码辅助、控制台管理等入口,接入地址统一在https://taotoken.net/api。对文件识别这个场景来说,它的价值在于——你可以把「本地 16 进制识别」作为第一道快速过滤,把「模型理解」作为第二道深度判断,两者共用同一个 Key,不用在代码里塞多套凭证。

具体入口按用途分:

  • 需要调模型做内容判断或生成描述,走模型对话入口;
  • 需要长期跑编码任务、Agent 流程,走 Coding Plan;
  • 需要管理 Key、查看用量,走控制台;
  • 需要创建或轮换 Key,走 API Keys 页面;
  • 需要查接入细节,走接入文档。

我试过把识别脚本和模型调用放在同一个服务里,用同一个 Key 管理,省掉了在配置文件里维护多份密钥的麻烦。下面进入正题。

3. 可复制配置:文件头映射表与校验脚本骨架

3.1 文件头映射配置

先给一份可以直接抄的映射表。左边是文件开头的 16 进制字符串(大写、无空格),右边是对应的类型标识。注意 JPEG 有多种变体,FFD8FFE0是 JFIF,FFD8FFE1是 EXIF,FFD8FFE2是 ICC,实际判断时通常只匹配前三个字节FFD8FF就够了,容错更好。

16 进制文件头类型说明
FFD8FFjpgJPEG 系列,匹配前 3 字节
89504E47pngPNG 固定头
47494638gifGIF87a/GIF89a 共用
424DbmpBMP 头
52494646webp/aviRIFF 容器,需看后续字节
00000018mp4部分 MP4 变体
00000020mp4常见 MP4 头
1A45DFA3mkv/webmMatroska 容器
494433mp3ID3 标签开头
25504446pdfPDF 头

这里有个坑要提前说:52494646是 RIFF 容器的通用头,WebP、AVI、WAV 都用它,光看前 4 字节分不出来,得往后读第 8 到 12 字节。同理00000018和00000020都是 MP4 的 ftyp box 变体,不同编码器写出来的值不一样。所以映射表要留扩展位,别写死。

3.2 校验脚本骨架

下面这段 Java 骨架可以直接放进项目,核心逻辑是读前 12 字节转 16 进制,先按 4 字节精确匹配,匹配不到再按 3 字节前缀匹配。

import java.io.InputStream; import java.util.HashMap; import java.util.Map; public class FileTypeDetector { private static final Map<String, String> HEADER_MAP = new HashMap<>(); private static final Map<String, String> PREFIX_MAP = new HashMap<>(); static { // 4 字节精确匹配 HEADER_MAP.put("89504E47", "png"); HEADER_MAP.put("47494638", "gif"); HEADER_MAP.put("1A45DFA3", "mkv"); HEADER_MAP.put("25504446", "pdf"); HEADER_MAP.put("49443300", "mp3"); // 3 字节前缀匹配,容错 JPEG 变体 PREFIX_MAP.put("FFD8FF", "jpg"); PREFIX_MAP.put("424D", "bmp"); } public static String detect(InputStream in) { try { byte[] head = new byte[12]; int read = in.read(head, 0, head.length); if (read < 4) { return "unknown"; } String hex = bytesToHex(head, read); // 先精确匹配前 8 个 hex 字符(4 字节) String fourByte = hex.substring(0, 8); if (HEADER_MAP.containsKey(fourByte)) { return HEADER_MAP.get(fourByte); } // 再前缀匹配 for (Map.Entry<String, String> e : PREFIX_MAP.entrySet()) { if (hex.startsWith(e.getKey())) { return e.getValue(); } } // RIFF 容器细分 if (hex.startsWith("52494646")) { String sub = hex.substring(16, 24); if ("57454250".equals(sub)) return "webp"; if ("41564920".equals(sub)) return "avi"; if ("57415645".equals(sub)) return "wav"; } // MP4 ftyp box 判断 if (hex.startsWith("000000") && hex.substring(8, 16).contains("66747970")) { return "mp4"; } return "unknown"; } catch (Exception e) { return "unknown"; } } private static String bytesToHex(byte[] src, int len) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i++) { String hv = Integer.toHexString(src[i] & 0xFF).toUpperCase(); if (hv.length() == 1) sb.append('0'); sb.append(hv); } return sb.toString(); } }

几个关键点解释一下。bytesToHex里& 0xFF不能省,Java 的 byte 是有符号的,负数直接转十六进制会得到FFFFFFxx这种错误结果。读 12 字节而不是 4 字节,是为了给 RIFF 和 MP4 这类需要看后续字节的格式留空间。read < 4的判断是防截断文件,后面验证环节会专门测这个。

3.3 通过 TaoToken 接入识别流程

识别逻辑跑在本地,但如果你想让识别结果进一步走模型判断,比如「这张图是不是违规内容」「这段视频描述一下」,就可以用同一个 Key 调 TaoToken 的模型对话入口。接入时把 API 地址设为https://taotoken.net/api,Key 从 API Keys 页面创建。代码里建议把 Key 放环境变量,别硬编码。

export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

这样你的服务里,本地识别负责快速拦截明显不对的文件,模型调用负责深度判断,两者共用一套凭证,运维上清爽很多。

4. 验证请求:改名文件与截断文件实测

配置写完了,得验证它真的管用。准备两个测试文件。

第一个是改名文件:找一张真实的 PNG 图片,复制一份改名为fake.jpg。第二个是截断文件:用命令把一张 JPEG 的前 2 个字节截出来,模拟损坏文件。

# 生成改名文件 cp real.png fake.jpg # 生成截断文件,只保留前 2 字节 head -c 2 real.jpg > truncated.jpg # 用 xxd 看真实文件头,确认预期 xxd -l 12 real.png xxd -l 12 real.jpg

xxd -l 12会打印前 12 字节的 16 进制和 ASCII 对照。PNG 应该看到8950 4e47,JPEG 应该看到ffd8 ffe0或类似。然后跑你的检测逻辑:

// 伪代码调用 String type1 = FileTypeDetector.detect(new FileInputStream("fake.jpg")); // 预期返回 "png",而不是 "jpg" String type2 = FileTypeDetector.detect(new FileInputStream("truncated.jpg")); // 预期返回 "unknown",因为读不满 4 字节

实测下来,改名文件会被正确识别为 png,因为文件头没变;截断文件会走到read < 4分支返回 unknown。这两个结果说明识别逻辑对「扩展名欺骗」和「文件损坏」都有防御能力。

如果你要把结果送到模型做进一步判断,可以在这之后调 TaoToken 的模型对话入口,把识别出的类型和文件元信息一起传过去。整个链路是:本地识别 → 类型确认 → 模型理解 → 落库或拦截。

5. 本篇常见错排查

5.1 读到的十六进制全是 FFFFFF 开头

这是 Java 里 byte 转十六进制最经典的坑。Integer.toHexString(byteValue)会把负数补成 32 位,得到 8 个字符。解决办法就是& 0xFF先转成无符号整数。如果你用的是 Python,bytes.hex()不会有这个问题,但用struct解包时要注意字节序。

5.2 MP4 识别不出来

MP4 的文件头不是固定的。它的结构是[4字节大小][4字节 ftyp][4字节品牌],大小字段可能是00000018、00000020等各种值,品牌字段可能是isom、mp42、avc1等。所以不能只匹配前 4 字节,要判断第 5 到 8 字节是不是66747970(即 ASCII 的ftyp)。上面脚本里那段hex.substring(8, 16).contains("66747970")就是干这个的。

5.3 WebP 被误判成 AVI

两者都是 RIFF 容器,前 4 字节都是52494646。区别在第 8 到 12 字节:WebP 是57454250,AVI 是41564920。如果你的映射表只写了52494646就返回 avi,WebP 会被误判。必须往后读。

5.4 上传流被提前消费

很多框架里MultipartFile.getInputStream()拿到的流只能读一次。如果你先读文件头做识别,后面再想读完整内容存盘,会发现流已经到末尾了。解决办法是用mark()和reset(),或者先把文件落到临时目录再读。Spring 的MultipartFile可以用transferTo先存临时文件。

5.5 截断文件导致数组越界

如果文件只有 2 字节,你read到 12 字节的数组里,后面 10 个字节是 0,转出来的 hex 会带一堆00,可能误匹配到某些以 0 开头的格式。所以一定要判断实际读取长度read,只对读到的部分做转换,读不满 4 字节直接返回 unknown。

6. 继续往下走:把识别接进你的链路

文件头识别是个投入产出比很高的防御手段,几十行代码就能挡住大部分扩展名欺骗。真正落地时,建议把它放在上传入口的第一道,识别不通过的直接拒绝,识别通过的再走后续的模型判断或存储流程。

如果你需要把识别结果和模型理解串起来,可以用 TaoToken 的统一 Key 管理整条链路。创建 Key 在 API Keys 页面,接入细节看接入文档,需要跑长期编码或 Agent 任务的话看 Coding Plan,想先试试模型对话效果就直接进模型对话入口。控制台可以统一看用量和 Key 状态。

最后留一个实用技巧:把文件头映射表做成外部配置文件而不是写死在代码里,格式规范更新时改配置就行,不用重新发版。映射表里每个类型至少留一个变体位,JPEG、MP4、RIFF 这些格式的变体比你想的多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询