网站图片 403/404 破图排查实战:从磁盘权限、alias 路径到防盗链与 CDN 缓存的五步定位

“图片传上去了”和“图片显示了”是两件事

我见过最多的图片失效事故,不是图片丢了,而是图片还存在,只是取不到。这两种情况的处理方式完全不同:前者要找备份恢复文件,后者只需要修一行路径或者一个权限。很多站长一看到页面上的破图就先慌了,四处找备份,其实 90% 的情况下文件好端端躺在磁盘上,只是 URL 拼错了、权限不对、或者被防盗链拦了。

下面这套排查流程是我处理图片 403/404/破图问题时固定走的顺序,从磁盘一路查到浏览器,基本能在十分钟内定位到具体环节。

第一步:确认文件在不在,以及是不是“空文件”

先按 URL 反推磁盘路径。假设页面引用的是 https://example.com/uploads/2026/09/cover.jpg,站点根目录是 /www/wwwroot/example.com,那么文件应该在 /www/wwwroot/example.com/uploads/2026/09/cover.jpg

# 直接问文件在不在
ls -l /www/wwwroot/example.com/uploads/2026/09/cover.jpg

# 如果路径记不清,用文件名全盘找回真实位置
find /www/wwwroot -name 'cover.jpg' 2>/dev/null

这里有两个很常见的坑。第一个是文件名被改了:上传时中文名或含空格的名字被转义、截断、或者加了时间戳前缀,页面里引用的还是原名。第二个是“0 字节文件”——上传过程被中断、磁盘写满(df -h 看一眼)、或者 PHP 上传临时目录不可写,都会留下一个大小为 0 的文件。文件存在不等于图片可用:

find /www/wwwroot/example.com/uploads -type f -size 0 -name '*.jpg' -o -type f -size 0 -name '*.png'

顺带用一个命令确认图片本身不是损坏的(判断魔数即可,不需要真的解图):

file /www/wwwroot/example.com/uploads/2026/09/cover.jpg
# 正常输出: JPEG image data, JFIF standard 1.01 ...
# 异常输出: HTML document / ASCII text  ← 说明存成了错误页或被写坏

如果 file 说它是 HTML 或者 ASCII text,那基本可以确定是被重定向页面覆盖了,通常发生在用脚本抓图但没校验响应的场景里。

第二步:权限与属主——403 的头号原因

文件在、内容也对,页面依然 403,那多半是权限或属主的问题。Nginx 的 worker 进程以某个用户运行(Debian/Ubuntu 上通常是 www-data,CentOS 上是 nginx),PHP-FPM 也可能以自己的用户跑。这个用户必须对图片有读权限,并且对路径中每一级目录都有执行权限(x)。目录缺 x 权限是最容易被忽略的一种:文件本身 644 完全正常,但它所在目录是 700 且属主是 root,Nginx 走不进去,一样 403。

# 逐级检查路径权限,从站点根目录一路到文件
namei -l /www/wwwroot/example.com/uploads/2026/09/cover.jpg

namei -l 会把这个路径每一级的权限和属主都列出来,一眼就能看出哪一级断了链。修复时正确的姿势是按类型分别设置,不要图省事用 chmod -R 777

# 目录: 755(含执行位,能让 Nginx 进入)
find /www/wwwroot/example.com/uploads -type d -exec chmod 755 {} \;
# 文件: 644(可读不可执行)
find /www/wwwroot/example.com/uploads -type f -exec chmod 644 {} \;
# 属主统一到 Nginx 运行用户
chown -R www-data:www-data /www/wwwroot/example.com/uploads

关于 777 我想多说一句:它能让图片立刻显示,但同时意味着任何人(包括被入侵的进程)都能改写你的图片文件,等于把上传目录变成了写入后门。图片是静态资源,只要读权限就够了,正确的组合永远是 644/755 加上正确的属主。

另外一个高频坑:PHP 上传的新文件继承了错误的 umask。如果你用脚本上传图片,PHP-FPM 的 umask 决定了新文件权限,不同发行版默认值不一样。当“本地上传的图能显示、程序上传的图 403”时,先看 umask 和上传目录的 setgid 位:

# 上传目录设置 setgid,新文件自动继承属组
chmod g+s /www/wwwroot/example.com/uploads
ls -ld /www/wwwroot/example.com/uploads

第三步:404 时,先分清是“路径错”还是“被重写吃掉”

404 和 403 是两种完全不同的病。404 意味着 Nginx 在磁盘上找不到对应文件,常见原因有三个。

原因一:URL 与磁盘路径的映射不是简单拼接。如果站点配置里对 /uploads/ 做了 alias,那 URL 和磁盘路径的对应关系就变了:

location /uploads/ {
    alias /data/images/;      # 注意 alias 结尾的斜杠必须与 location 一致
}

这里有个经典的 alias 陷阱:location /uploads/alias /data/images(结尾没有斜杠)会导致路径拼接错误,实际去找 /data/images2026/09/cover.jpg 这种不存在的路径,全站图片 404 而 Nginx 日志里一切正常。用 tail -f /var/log/nginx/error.log 看真实报错,它会明确写出“open() ... failed (2: No such file or directory)”以及完整路径,直接暴露拼接错误。

原因二:被 location 规则抢先匹配。如果你的图片目录里有 .php 或特殊后缀,或者配置了 location ~* \.(jpg|png)$ 之类的正则且有 try_files 兜底到 index.php,那么一旦文件不存在,请求会被交给 PHP 处理,最终返回的可能是一个 302 到首页而不是 404。这种“图片变成了小 HTML 页面”的情况,页面上的表现是破图,而服务器上文件其实是缺的——回到第一步用 find 确认。

原因三:大小写敏感。Windows 上传的图片经常是 Cover.JPG,页面里写的是 cover.jpg,在 Linux 上就是两个不同的文件。批量修复的时候可以顺手统计一下这类不一致:

# 找出大写扩展名的图片,统一改成小写并同步改引用
find /www/wwwroot/example.com/uploads -name '*.JPG' -o -name '*.PNG'

第四步:403 但权限看起来都正常——查防盗链

这是最容易被误判的一类。文件在、权限 644、属主正确、路径也对,但访问返回 403,而且只在特定场景下发生(比如从搜索引擎点进来、或者从别的网站跳转过来)。这时候十有八九是防盗链规则在拦你自己

location ~* \.(jpg|jpeg|png|gif|webp)$ {
    valid_referers none blocked server_names example.com *.example.com;
    if ($invalid_referer) {
        return 403;
    }
}

排查时不要用浏览器测试,因为浏览器会带上真实的 Referer,结果可能是“你自己能看、别人看不到”。用 curl 精确模拟三种场景:

# 1) 无 Referer(直接访问,none 通常允许)
curl -sI -o /dev/null -w "%{http_code}\n" "https://example.com/uploads/2026/09/cover.jpg"

# 2) 带一个外站 Referer —— 若防盗链生效,这里应该被拒
curl -sI -o /dev/null -w "%{http_code}\n" \
  -e "https://other-site.com/" "https://example.com/uploads/2026/09/cover.jpg"

# 3) 带本站 Referer —— 正常应该 200
curl -sI -o /dev/null -w "%{http_code}\n" \
  -e "https://example.com/post/123.html" "https://example.com/uploads/2026/09/cover.jpg"

三个结果一对比,问题就很清楚了:如果 (1) 就返回 403,说明你的 valid_referers 没写 none,导致所有直接访问(包括很多 App 内打开链接、部分爬虫)都被拒。搜索引擎的图片抓取常常不带 Referer 或者带的是自己的域名,这也是为什么“别人写的防盗链”经常导致自家图片在搜索结果里集体消失。补上 blocked 与主流搜索引擎域名,是更稳妥的写法。

第五步:磁盘上全对,浏览器还是破图——缓存与内容协商

走到这一步,说明服务端链路基本没问题了,剩下的怀疑对象是缓存和响应头。

最常见的是浏览器缓存了一个早期的 404 或破损响应。前面我们改权限、修路径时,浏览器可能已经把“404”这个结果缓存了一段时间。此时用 curl -I 能看到 200,但用户端还是破图。确认响应头:

curl -sI "https://example.com/uploads/2026/09/cover.jpg?_=$(date +%s)" \
  | grep -Ei 'http/|content-type|content-length|cache-control|age|via|x-cache'

几个关键点:content-type 必须是 image/jpeg 之类,如果显示 text/html 说明返回的其实是错误页;content-length 应该是图片的真实大小,如果是 0 或者很小,说明响应体不对;出现 x-cache: HITage 很大,说明 CDN 缓存了旧结果,需要去 CDN 后台刷新该 URL。

另一个隐蔽的问题是WebP 内容协商。如果站点用 Nginx 做了“支持 WebP 就返回 .webp,不支持就返回原图”的协商,某些中间代理会把 Vary: Accept 丢掉,导致浏览器拿到的格式与 content-type 不符——某些浏览器就直接不显示了。这时把协商规则收紧,或者干脆统一输出一种格式更省心。

常见问题答疑

Q:只有部分用户反馈图片不显示,我自己看正常,怎么排查?先怀疑 CDN 边缘节点与本地 DNS 的差异。让反馈用户 ping/dig 一下图片域名,对比解析到的 IP;同时在响应头里看 x-cache 状态。这类“部分节点坏”的问题,刷新 CDN 缓存后通常就好了。

Q:图片 URL 里带了双斜杠 // 会有影响吗?多数情况下 Nginx 会正常处理,但某些 CDN 与 alias 组合下会把路径解析错,导致 404。建议在生成 URL 的模板里就做一次规范化,把重复斜杠压掉。

Q:迁移服务器后图片全部 404,但文件明明拷过去了。优先查三件事:站点根目录配置是否指向新路径、软链接是否有效(ls -l 看箭头指向)、以及上传目录是否被你漏在 rsync 排除规则里。用 find /www -name 'cover.jpg' 确认文件真的在新机器上。

Q:要不要给图片目录关闭 PHP 解析?强烈建议。图片目录里正常情况下不应该存在可执行脚本,用 Nginx 禁掉解析能显著降低被上传 webshell 的风险:

location ^~ /uploads/ {
    location ~ \.(php|phtml|php5)$ {
        return 403;
    }
}

Q:图片数量多了,一个个查太慢怎么办?写个批量探测脚本,把站点 sitemap 里的页面抓一遍,提取所有 img src,逐个 curl -sI 看状态码,输出非 200 的清单。这类脚本用 curl + awk 就能写,跑一次能覆盖全站,比手动点快得多。

这套流程走下来,我发现图片问题的分布其实是相对固定的:大约一半是权限/属主问题,三成是路径与规则匹配问题,剩下的才是缓存与 CDN。所以顺序不要乱——先确认磁盘上的文件是对的,再确认服务端能读到它,最后才去怀疑缓存。反过来排查,你会在缓存层反复刷来刷去,越查越糊涂。

Last modification:September 21st, 2026 at 09:25 pm

Leave a Comment