LINUX SB 快照站

关于头像精华

标题历史 (1)

原帖: linux.sb/topic/13749 · 共 24 楼 · 标题快照 2026-08-18 19:31:41

楼主
发帖 2026-08-18 18:36:22 · 快照 2026-08-18 18:36:37

头像上传 GIF 支持排查记录

问题:在 https://linux.sb/profile 上传头像时,无法选择 GIF 文件;即使前端绕过限制直接上传,后端也会返回 {"ok":0,"message":"请选择头像图片"}


一、排查过程

1. 确认前端资源位置

站点页面引用的前端资源:

  • https://linux.sb/app/assets/index.js?v=v8.6.5
  • https://linux.sb/app/assets/plugins.js?v=8baf4e8b040d

头像上传逻辑在 plugins.jsavatar_upload 模块中。

2. 定位第一道限制:文件选择框 accept

页面实际渲染的 HTML:

<label class="btn avatar-upload-select">上传头像
  <input type="file" accept="image/jpeg,image/png,image/webp" data-avatar-upload-input="" hidden="">
</label>

accept没有 image/gif,导致文件选择对话框直接过滤掉 GIF。

3. 定位第二道限制:前端强制转 JPEG

plugins.js 中保存按钮的处理逻辑:

save?.addEventListener('click', () => {
    if (!image) return;
    const output = document.createElement('canvas');
    output.width = output.height = 200;
    const out = output.getContext('2d');
    out.fillStyle = '#fff'; out.fillRect(0, 0, 200, 200);
    out.drawImage(image, -x / scale, -y / scale, canvas.width / scale, canvas.height / scale, 0, 0, 200, 200);
    save.disabled = true;
    output.toBlob(async blob => {
        try {
            if (!blob) throw new Error('头像生成失败');
            const body = new FormData();
            body.append('_csrf', csrf);
            body.append('avatar_upload_action', 'upload');
            body.append('avatar', blob, 'avatar.jpg');  // ← 硬编码 jpg
            const response = await fetch(panel.dataset.uploadUrl, {method: 'POST', body, headers: {'X-Requested-With': 'XMLHttpRequest'}});
            ...
        }, 'image/jpeg', .9);  // ← 强制转 jpeg,丢失 GIF 动画
    });
});

这里做了两件事:

  1. 用 Canvas 裁切/缩放头像
  2. 强制输出为 image/jpeg,文件名硬编码为 avatar.jpg

后果:即使前端能选中 GIF,经过 Canvas 后也会变成静态 JPEG,动画帧全部丢失。

4. 前端 bypass 尝试

尝试在页面注入 JS 绕过 Canvas 裁剪,直接将原始 GIF 上传:

(() => {
    const panel = document.querySelector('[data-avatar-upload]');
    const input = panel.querySelector('[data-avatar-upload-input]');
    const csrf = panel.querySelector('input[name=_csrf]')?.value || '';
    const uploadUrl = panel.dataset.uploadUrl;

    // 放开 accept
    input.setAttribute('accept', 'image/jpeg,image/png,image/gif,image/webp');

    // 拦截 change,GIF 直接上传
    document.addEventListener('change', (event) => {
        const file = input.files?.[0];
        if (!file || !(file.type === 'image/gif' || file.name.toLowerCase().endsWith('.gif'))) return;
        event.stopImmediatePropagation();
        const body = new FormData();
        body.append('_csrf', csrf);
        body.append('avatar_upload_action', 'upload');
        body.append('avatar', file, file.name);
        fetch(uploadUrl, {method: 'POST', body, headers: {'X-Requested-With': 'XMLHttpRequest'}})
            .then(r => r.json())
            .then(data => { if (data.ok) window.location.href = data.redirect; });
    }, true);
})();

前端 patch 生效后,文件选择框已能选中 GIF,请求也能发出。

5. 后端拒绝:{"ok":0,"message":"请选择头像图片"}

绕过前端后,后端返回:

{"ok":0,"message":"请选择头像图片"}

说明后端也存在文件类型校验,拒绝了非 JPEG/PNG/WebP 的文件,GIF 在后端也被视为无效头像格式。


二、结论

层级限制说明
前端accept 属性过滤掉 GIF,不让用户选中
前端Canvas 裁剪强制转 JPEG,GIF 动画丢失
后端文件类型校验拒绝 GIF 格式,返回"请选择头像图片"

当前站点(v8.6.5)从头到尾都不支持 GIF 头像,三处都需要修改才能真正支持。


三、如果要支持 GIF,需要改哪些地方

3.1 前端 plugins.jsavatar_upload 模块

// 1. input accept 加上 gif
input.setAttribute('accept', 'image/jpeg,image/png,image/gif,image/webp');

// 2. 保存时保留原始格式,不要强制转 jpeg
save?.addEventListener('click', () => {
    if (!image) return;
    const output = document.createElement('canvas');
    output.width = output.height = 200;
    const out = output.getContext('2d');
    out.fillStyle = '#fff'; out.fillRect(0, 0, 200, 200);
    out.drawImage(image, -x / scale, -y / scale, canvas.width / scale, canvas.height / scale, 0, 0, 200, 200);
    save.disabled = true;

    // 根据原始文件决定输出格式
    const mime = (file && file.type) ? file.type : 'image/jpeg';
    const extMap = {'image/jpeg':'jpg','image/png':'png','image/gif':'gif','image/webp':'webp'};
    const ext = extMap[mime] || 'jpg';

    output.toBlob(async blob => {
        try {
            if (!blob) throw new Error('头像生成失败');
            const body = new FormData();
            body.append('_csrf', csrf);
            body.append('avatar_upload_action', 'upload');
            body.append('avatar', blob, 'avatar.' + ext);
            const response = await fetch(panel.dataset.uploadUrl, {method: 'POST', body, headers: {'X-Requested-With': 'XMLHttpRequest'}});
            const data = await response.json();
            if (!data.ok) throw new Error(data.message || '头像上传失败');
            window.location.href = data.redirect || window.location.href;
        } catch (error) {
            setStatus(error?.message || '头像上传失败', true);
        } finally { save.disabled = false; }
    }, mime, 0.92);
});

注意:Canvas 本身不支持动画帧,上述改法上传的 GIF 会是静态帧。要支持动态 GIF 需要额外引入 GIF 帧解析/合成方案(如 gif.jsomggif 等),改动量较大。

3.2 后端 — 头像上传接口

需要放宽文件类型校验,允许 image/gif 通过。

3.3 存储与展示

  • 存储:保持原样,文件系统 / OSS 存原始文件即可
  • 展示:确保页面 <img> 标签能正常渲染 GIF(浏览器原生支持,通常无需改动)

四、关于 Canvas 裁剪的必要性说明

站点在头像上传中使用 Canvas 进行裁剪/缩放,这一设计是必要的,原因如下:

4.1 统一头像尺寸

Canvas 将任意尺寸的原始图片统一输出为 200×200 的正方形头像。如果没有这一步:

  • 用户上传 4K 大图 → 头像区域显示异常,浪费带宽
  • 用户上传超长条图 → 页面布局被撑开
  • 各用户头像尺寸不一 → 列表/卡片对齐混乱

4.2 服务端降本

统一尺寸后,服务器可以对头像文件进行:

  • 压缩(减小存储和 CDN 流量)
  • 生成多档位缩略图(列表用小图,详情用大图)
  • 缓存策略优化(固定尺寸便于 CDN 缓存)

如果允许任意尺寸原图直接存储,服务端需要做实时裁切/缩放,增加 CPU 和内存开销。

4.3 安全考虑

Canvas 处理可以:

  • 过滤潜在恶意构造的图片(虽然不能完全替代服务端校验,但作为第一道防线有意义)
  • 去除 EXIF 信息(避免泄露用户隐私数据)

4.4 交互一致性

Canvas 裁剪提供了统一的用户体验:

  • 拖拽调整位置
  • 缩放控制
  • 实时预览最终效果

4.5 为什么不接受 GIF

在当前的 Canvas 裁剪方案下,GIF 动画帧会被丢弃,因为:

  1. drawImage() 只绘制当前帧
  2. toBlob() 输出单帧静态图
  3. 输出格式被固定为 image/jpeg

如果要支持动态 GIF,需要在 Canvas 之外额外实现:

  • GIF 帧解析(omggif / gifuct-js
  • 帧合成/循环播放
  • 或放弃 Canvas 裁剪,改用纯前端预览 + 后端处理

这属于需求层面的取舍,不是技术 impossibility


五、后续建议

  1. 短期:如需临时支持 GIF,可手动修改本地 plugins.js 缓存(浏览器 DevTools → Sources → Overrides),加上上述 accept + 保留原格式的 patch
  2. 中期:向站点维护者反馈,在 plugins.js 和头像上传接口中统一放开 GIF 支持
  3. 长期:如果要支持动态 GIF 头像,需要重新设计前端裁剪方案,引入 GIF 帧处理库
#1
发帖 2026-08-18 18:37:09 · 快照 2026-08-18 18:38:25

顺便说一句,没gpt用,蹭的zcode,这玩意好像不能控制浏览器,有点不好玩~

#2
发帖 2026-08-18 18:40:23 · 快照 2026-08-18 18:42:26

这技术贴真扎实,我也看懂为什么不能用gif了,亏我还想用

#3
发帖 2026-08-18 18:58:54 · 快照 2026-08-18 19:00:02

那就没辙了 就这样吧 不改了

#4
发帖 2026-08-18 19:00:39 · 快照 2026-08-18 19:01:52

@痛失姓名的站长 #3 不是现在该干的事,这个真发展起来可以大积分换,因为要占服务器存储空间,毕竟一个gif比jpeg肯定大,而且你这个还经过裁剪,大小固定。

#5
发帖 2026-08-18 20:22:20 · 快照 2026-08-18 20:22:56

webp也支持动图来着,试试绕过裁剪直接传动态webp

#6
发帖 2026-08-18 20:24:48 · 快照 2026-08-18 20:24:49

@小永高吖 #5 gif传不上去就说明校验了后缀吧,那webp肯定也上不去了...

#7
发帖 2026-08-18 20:25:21 · 快照 2026-08-18 20:30:03

@小永高吖 #5 首先是正常的图片,不管什么格式,前端现在都是canvas裁剪成jpeg,估计服务器只允许jpeg

#8
发帖 2026-08-18 20:26:51 · 快照 2026-08-18 20:30:03

@小永高吖 #5 算了 我来试一下,验证一下,这个号改不了头像了,还得弄个小号

#9
发帖 2026-08-18 20:40:25 · 快照 2026-08-18 20:42:01

@痛失姓名的站长 #3 现在动图早就不是GIF的天下了,各种多样化的格式支持一下的话,也可以达到很好的效果,甚至svg头像也不是不行

#10
发帖 2026-08-18 20:41:50 · 快照 2026-08-18 20:42:01

不用估计啦,你去bbs1.org把源码下下来看一下不就知道了@Se7en #7

#11
发帖 2026-08-18 20:44:14 · 快照 2026-08-18 20:45:33

@altzin #10 源码里头像不能传,我早就下过了。AI看了,这个自定义头像好像还是站长加的

#12
发帖 2026-08-18 20:44:28 · 快照 2026-08-18 20:45:33

@小永高吖 #5 {"ok":0,"message":"头像格式无效,请重新裁切"} 换了个报错,估计就是校验死了jpeg了

最后由 Se7en 编辑于 2026-08-18 20:44
#13
发帖 2026-08-18 20:46:04 · 快照 2026-08-18 20:47:32

@StellaFortuna林可欣 #9 我还真不知道诶,受教了,不懂,一直以为还是gif的时代。那我觉得后期可以开动图,webp比gif小80%,其实一张图已经占不了多少空间

#15
发帖 2026-08-18 20:48:37 · 快照 2026-08-18 20:49:35

@Se7en #13 比如说现代的QQ使用的现代大表情动画,就是一堆图片拼出来的 格式为apng
这里就不是大小之间的比较了,但是apng是绝对碾压gif的()

最后由 StellaFortuna林可欣 编辑于 2026-08-18 20:49
#16
发帖 2026-08-18 20:50:25 · 快照 2026-08-18 20:54:07

我下来也解决不了现在不支持动图认死了jpeg吧

#17
发帖 2026-08-18 20:51:01 · 快照 2026-08-18 20:54:07

@altzin #14 我擦 bbs1org感觉就是有实力的 皇黑搞出来的吧... 富可敌国都做好了

#19
发帖 2026-08-18 20:54:24 · 快照 2026-08-18 20:56:06

谁把这种东西带到这里来了@Se7en #18

#20
发帖 2026-08-18 21:05:13 · 快照 2026-08-18 21:06:22

@altzin #19 bbs1.org的站长啥的都戴上了,我也不知道是谁啊,你懂的。

#21
发帖 2026-08-18 22:23:46 · 快照 2026-08-18 22:59:30

@Se7en #12 不清楚是检查文件后缀还是检查文件头,检查文件后缀那就在后面加上.jpeg就行,检查文件头那就没办法了

#22
发帖 2026-08-18 22:37:36 · 快照 2026-08-18 22:59:30

实际上是解析了文件 然后看看是不是这个格式@小永高吖 #21

#23
发帖 2026-08-18 23:45:25 · 快照 2026-08-18 23:45:47

@altzin #22 尝试了一小时 没搞定… 没gpt用 其他太笨