# 访问图片 (/zh/docs/guides/access)

图片链接的两种形式、缓存与条件请求、按需缩放与 WebP 协商、代理与 302 两种访问模式、防盗链，以及访问计数的口径。

## 链接形式 [#链接形式]

| 链接 | 谁有 | 说明 |
|---|---|---|
| `/i/<短ID>` | 每张图 | 上传响应里的 `url`；短 ID 为 10 位，字符取自 `0-9A-Za-z` |
| `/o/<用户 id>/<对象键>` | 经 S3 协议上传的图 | 按对象键的别名，对象键可含 `/`，见 [上传客户端](./clients.mdx#object-key-aliases) |

两种链接都公开访问、不需要登录，走同一条输出管线：防盗链校验、访问计数、按需处理、渠道容灾读取完全一致。

- 图片不存在、已删除或已到期时返回 404（纯文本）。删除后链接立即失效；设了有效期的图一到期就返回 404，不等清理扫描把它回收，见 [生命周期与查重](./lifecycle.mdx)。
- 健康渠道里都没有这张图的可用副本，或全部候选渠道读取失败，返回 503（纯文本）。
- 查询图片元数据失败（数据库故障）时返回 500（纯文本），可以稍后重试。
- 支持 GET 与 HEAD。HEAD 返回与 GET 相同的状态码与响应头、不带正文，也不计访问；不需要按需处理时不读取渠道里的对象，需要处理的 HEAD 请求照常生成衍生图。

完整接口定义见 [接口参考：图片访问](../api/image-access.mdx)。

## 缓存与条件请求 [#caching]

短链背后的图片固定不变，可以长期缓存；`/o/` 别名可能被同键覆盖、改指新图，所以要求缓存方每次使用前确认：

| 响应头 | 短链 `/i/` | 别名 `/o/` |
|---|---|---|
| `Cache-Control` | `public, max-age=31536000, immutable` | `public, no-cache` |
| `ETag` | 原图为 `"<SHA-256>"`，衍生图为 `"<SHA-256>-<宽度>-<格式>"` | 同左 |
| `Vary` | 输出格式由 `Accept` 协商决定时为 `Accept`，见 [WebP 协商](#webp-协商) | 同左 |
| `Last-Modified` | 原图为上传时刻；衍生图无 | 同左 |
| `Content-Type` | 原图为上传时按内容判定的类型；衍生图为目标格式的类型 | 同左 |
| `X-Content-Type-Options` | `nosniff` | `nosniff` |

请求带 `If-None-Match` 且与当前 `ETag` 完全一致时返回 304，304 同样带 `Cache-Control`、`ETag` 与需要时的 `Vary`。只按 `If-None-Match` 协商，不处理 `If-Modified-Since`。
`public, no-cache` 允许浏览器与 CDN 存下响应，但每次使用前都要带 `ETag` 向本服务确认，内容没变时只回 304；同键覆盖后，下一次确认就拿到新图。

两种响应不缓存，带 `Cache-Control: no-store`：按需处理失败后的降级输出，以及 302 直链跳转，分别见下文。

短链背后的内容只有一种情况会变：管理员合并疑似重复图，被合并一方的链接改为输出保留方的内容，而浏览器与 CDN 已缓存的旧内容在缓存过期前不会更新。见 [生命周期与查重](./lifecycle.mdx#deduplication)。

## 按需缩放与格式 [#on-demand-processing]

在链接后加查询参数，即可拿到缩放或转换格式后的版本：

```text
https://img.example.com/i/R0RjUyP1UW?w=400
https://img.example.com/i/R0RjUyP1UW?w=800&format=webp
https://img.example.com/i/R0RjUyP1UW?format=jpeg
```

| 参数 | 取值 | 说明 |
|---|---|---|
| `w` | 「宽度档」`process_widths` 中的值，出厂 `100,200,400,800,1600` | 按宽度等比缩小；不在档位里返回 400。目标宽度不小于原图宽度时不放大，只按目标格式重新编码 |
| `format` | 「目标格式」`process_formats` 中的值（出厂 `webp`、`jpeg`、`png`），或 `auto` | 显式指定输出格式；不在列表里返回 400 |

宽度与格式都是白名单：放开任意取值会被人用来打满 CPU 与磁盘缓存，所以只开放预设的几档；档位固定，同一组参数的结果也能被反复命中缓存。参数在「设置 → 运行配置」的「按需处理」分组里修改，即时生效。

### WebP 协商 [#webp-协商]

不带 `format`，或 `format=auto` 时，按请求的 `Accept` 头决定输出格式：

- `Accept` 里有 `image/webp`，且 `webp` 在「目标格式」里，输出 WebP。
- 否则保持原格式。协商只会升级到 WebP，不会把 WebP 原图转成别的格式给旧客户端。
- 走协商的响应都带 `Vary: Accept`，包括协商后无需处理、直接输出的原图与 304 响应；显式指定了 `format` 的响应不带。GIF 不参与协商，也不带。

现代浏览器的 `Accept` 都含 `image/webp`，所以浏览器直接打开 JPEG / PNG 的 `/i/` 链接，默认拿到的是 WebP。
要拿原始字节，显式写原图自己的格式，例如 JPEG 原图加 `?format=jpeg`：目标格式与原格式相同且不缩放时，直接输出原图，不重新编码。

### 处理规则 [#处理规则]

- 既不缩放、格式又与原图相同时，不做处理，直接输出原图。
- GIF 一律输出原图，`w` 与 `format` 都被忽略（也不会因为参数非法返回 400），动画得以保留。
- JPEG 原图带 EXIF 方向标记时，先按标记转正再缩放，宽度作用在视觉上的宽度。
- 编码档位：WebP 质量 `process_quality_webp`（出厂 80）、速度权衡 `process_webp_method`（出厂 4）；JPEG 质量 `process_quality_jpeg`（出厂 85）；PNG 无损、最高压缩。输出不带任何元数据。
- 处理结果缓存在数据目录的 `variants/` 下，总量上限 `variants_cache_mb`（出厂 10240 MB），超出按最近访问时间淘汰。编码档位一改，之后的请求生成新的缓存项，旧的随淘汰清掉。
- 同一张图同一组参数的并发首次请求只处理一次，结果共享。
- 同时处理的数量上限 `process_workers`（出厂 4）；等待处理名额与读取原图共用一个超时 `process_timeout_ms`（出厂 5000 毫秒）。
- 原图分辨率超过像素预算，或字节数超过当前的单图上限时，不处理。

处理失败（名额繁忙超时、解码失败等）时降级输出原图，响应为 `Cache-Control: no-store` 且不带 `ETag`，避免把全尺寸原图当成这个带参数链接的内容缓存下来。

## 访问模式：代理与 302 [#访问模式代理与-302]

「设置 → 运行配置」里的「访问模式」`access_mode` 有两档：

- **proxy（出厂）**：ImgYoung 从持有就绪副本的健康渠道里按优先级读取并转发给访客，读失败立即换下一个渠道。
- **redirect**：对能直链的渠道返回 302，访客直接从渠道的公开地址下载，不经本机转发。

出厂默认代理，是因为图片字节经过本机，下面几件事才能同时成立：防盗链每次都生效，按需处理与 WebP 协商可用，渠道读失败即时切换，
访问计数准确，渠道的直链不对外暴露。流量压力交给前置的 CDN，见 [CDN](../operations/cdn.mdx)。redirect 面向没有 CDN、带宽有限的小型 VPS，代价见下表。

redirect 模式下，按优先级在持有就绪副本的健康渠道中找第一个同时满足以下条件的渠道：

- 渠道设置里开了「允许 302 直链」；
- 不是加密渠道（密文不能直接给访客）；
- 配置了 `public_base_url`。

302 的目标是 `<public_base_url>/<渠道内对象键>`，渠道内对象键形如 `<短ID>_<SHA-256 前 8 位>`，不带扩展名。`public_base_url` 要指向渠道 `path` 所对应的那一层目录。
找不到这样的渠道时，照常走代理。渠道的直链与加密设置见 [存储渠道](../channels/index.mdx)。

即使是 redirect 模式，以下请求仍由本机代理输出：

- 需要按需处理的请求，包括带 `w`、显式指定了与原图不同的 `format`，以及协商到 WebP 的请求。由于浏览器都声明支持 WebP，浏览器访问 JPEG / PNG 时默认会走代理转码；想让浏览器访问也走 302，可以把 `webp` 从「目标格式」里去掉。
- 处理失败后的降级输出。
- 命中 `If-None-Match` 的请求直接由本机回 304，不跳转。

| | proxy | redirect |
|---|---|---|
| 图片流量 | 经过本机 | 由渠道直接给访客 |
| 容灾 | 每次请求都可以换渠道读 | 302 指向的渠道出错时访客直接看到错误；本机要等健康探测把该渠道判为异常后才换目标 |
| 按需处理 | 可用 | 需要处理的请求仍走代理 |
| 防盗链 | 每次访问都校验 | 只校验 302 这一步，渠道公开地址本身不受约束 |
| 加密渠道 | 可用 | 不能直链，回落代理 |

302 响应带 `Cache-Control: no-store`，浏览器与 CDN 都不缓存这次跳转：跳转目标会随渠道健康、优先级与直链设置变化，图片删除或到期也要立即生效。跳转本身不带图片字节，每次回到本机的代价只是几次数据库查询。访客每次访问都会先到本机，访问计数照常。

渠道里的对象键不带扩展名，写入渠道时会按内容把 `Content-Type` 一并交给存储后端；对象存储类渠道据此保存类型，浏览器直接打开 302 链接也能正常显示图片。

## 防盗链 [#hotlink-protection]

防盗链按请求的 `Referer` 头决定是否放行，规则在「设置 → 防盗链」管理（仅管理员）。

### 规则写法 [#规则写法]

| 写法 | 匹配 |
|---|---|
| `example.com` | 只匹配 `example.com` 本身 |
| `*.example.com` | 匹配 `example.com` 的任意子域（如 `blog.example.com`、`a.b.example.com`），不匹配 `example.com` 本身 |

- 规则只写主机名，不带协议、端口与路径；不区分大小写，保存时转为小写。
- 允许单段主机名（如 `localhost`）与 IPv4 地址形式；长度不超过 253 个字符。
- 匹配只看 `Referer` 里的主机名，端口不参与。
- 重复添加返回 409。添加与删除都即时生效、不需要重启，并记入审计日志。

### 判定规则 [#判定规则]

- 规则列表为空时，防盗链不启用，任何来源都放行。
- 有规则时：
  - 带 `Referer`：主机名命中任一规则才放行；`Referer` 无法解析视为未命中。
  - 不带 `Referer`（地址栏直接打开、下载工具、设置了 `Referrer-Policy: no-referrer` 的页面）：由「空 Referer 放行」开关决定，即配置 `allow_empty_referer`，出厂开启。
- 被拒绝的请求返回 403（纯文本），不计访问。

作用范围只有 `/i/` 与 `/o/` 两个公开入口。管理后台里的缩略图与预览、S3 的 GetObject 不受影响。

规则只保存在元数据库里，通过后台或 [接口](../api/admin-referer.mdx) 增删，没有对应的环境变量或配置文件。
别忘了把你自己用图的站点（博客、文档站）加进去，包括需要的子域。

<Callout type="info" title="防盗链是轻量防护">
`Referer` 可以被伪造或省略；开启「空 Referer 放行」时，不带 `Referer` 的请求都能拿到图。
图片经过 CDN 时，CDN 缓存命中的请求不会回到本机，本机的规则管不到它们，需要在 CDN 侧另配规则；redirect 模式下渠道的公开地址也不在保护范围内。
</Callout>

## 没有私有图片 [#no-private-images]

所有图片链接都是公开的：知道链接就能访问，不需要登录，也没有签名链接或其他访问鉴权。
短 ID 只是一个随机标识，不是密钥：私密性从不依赖它的长度，链接被猜中或枚举到也无妨，因为图片本来就是公开的。

真正的私有图需要短时效的签名链接，而且每次访问都必须由本机校验：不能按一年缓存、不能 302 交给渠道直链。
这与当前围绕「公开、内容不可变、可长期缓存」设计的访问路径相冲突，所以 ImgYoung 不提供私有图片。
不能公开的图片不要传进 ImgYoung；只想限制哪些站点能嵌入，用[防盗链](#hotlink-protection)。

## 后台浏览不计访问 [#console-browsing]

管理后台展示缩略图与预览走的是 `/api/admin/images/<短ID>/thumb`：

- 需要登录会话，不受防盗链约束；
- 与 `/i/` 同一条处理管线（同样支持 `w`、`format`）；
- 不计访问次数、不更新最近访问时间、不顺延有效期，也不会把图从待清理列表里摘掉。

管理员翻看图片因此不会干扰访问统计与冷图判定。从后台复制出去的分享链接仍是 `/i/` 地址。

## 访问计数与热门图 [#访问计数与热门图]

计入访问的，是到达图片的 `/i/` 与 `/o/` GET 请求（已通过防盗链、处理参数合法），包括 304 与 302 响应。不计入的有：HEAD 请求、`w` 或 `format` 不合法而返回 400 的请求、后台缩略图与预览、S3 GetObject、被防盗链拒绝的请求。

计数先记在内存里，每隔 `activity_flush_seconds`（出厂 60 秒）批量写入数据库。同一张图在一个周期内无论被访问多少次只加 1，所以「访问次数」反映的是有访问的周期数，不是精确的 PV。不逐请求写库，是因为 SQLite 同一时刻只有一个写者，逐次更新会与上传、清理争抢写锁；冷图阈值以天计，计数用于趋势与排行，这个精度足够。
每次写入时同时：

- 更新「最近访问」时间；
- 摘掉待清理标记；
- 开启「访问即续期」时，把带有效期、尚未到期的图的到期时刻顺延，见 [生命周期与查重](./lifecycle.mdx)。

在后台可以看到：

- 图片详情里的「访问次数」与「最近访问」；
- 管理员总览的「访问次数」合计与「估算流量」，以及按访问次数排序的前 10 张「热门图片」。
  估算流量按「访问次数 × 原图大小」粗算：304、302 与衍生图响应都按整张原图计（偏高），同一周期内的重复访问只计一次（偏低）。

需要精确的请求量与流量，用反向代理或 CDN 的日志与 [监控指标](../operations/monitoring.mdx)。
