> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/zh/configuration/security-options.md).

# 安全选项

MyIP 的后端代理了多个第三方 API，其中一些是付费的。若完全暴露，公共实例就会成为任何发现它的人的匿名免费代理。

把防护看作两层，按这个顺序：

1. **边缘层（推荐的主防线）。** 位于源站前方的 CDN/WAF——下面以 Cloudflare 为示例——会在恶意流量到达你的服务器之前将其拦截，而且它具备任何应用都无法自带的更强工具：托管式机器人检测、灵活的限流规则、挑战验证，以及对攻击者 IP 的全局视图。
2. **应用本身（安全网）。** 四个环境变量会在后端构建最后一道防线。即使在边缘层之后，它们也值得设置——当有人找到了你源站的真实地址并完全绕过边缘层时，正是它们在保护你——但它们是后备措施，不是主防线。

## 先在边缘层防护（推荐）

任何支持按 IP 设定限流规则的 CDN/WAF 都是同样的工作方式；下面的步骤使用 Cloudflare，因为它是最常见的选择，而且其免费套餐已经覆盖了基本需求。

{% stepper %}
{% step %}

#### 通过 Cloudflare 代理你的 DNS 记录

在 Cloudflare 的 DNS 面板中，保留你的 MyIP 主机名对应的记录 **已代理** （橙色云），这样所有流量都会通过 Cloudflare 的边缘进入。MyIP 就是为此而设计的：后端已经读取 `CF-Connecting-IP` 用于识别真实客户端 IP，而且其可缓存的 `/api/*` 路由会由边缘缓存直接提供，在请求到达你这里之前就吸收掉了很大一部分负载。
{% endstep %}

{% step %}

#### 为……添加限流规则 `/api/*`

在 **安全 → WAF → 限流规则**，创建一条匹配你的 API 路径的规则——例如，表达式 `(http.request.uri.path wildcard "/api/*")` ——并在源 IP 超过阈值时将其阻止。所有套餐都支持限流；规则数量和窗口选项因套餐而异。由于边缘缓存已经会回答重复查询，正常访客很少需要很高的请求量——先把规则设得比你想象中更严格一些，如果真实用户抱怨，再放宽。
{% endstep %}

{% step %}

#### 开启机器人防护

启用 **Bot Fight 模式** （安全 → 机器人）。公共 MyIP 实例最常见的滥用，就是对地理位置端点进行脚本化抓取，而这正是它针对的目标。如果你的套餐支持自定义 WAF 规则， **托管挑战** 用于非浏览器流量的 `/api/*` 是一个更温和的替代方案，它不会硬性阻止真实用户。
{% endstep %}

{% step %}

#### 锁定你的源站

只有流量无法跳过边缘层时，边缘规则才有用。请将源站服务器的防火墙配置为仅接受来自 [Cloudflare 的 IP 段](https://www.cloudflare.com/ips/) 的 HTTP(S) 连接——或者使用 Cloudflare Tunnel 让源站完全脱离公共互联网。如果源站能够直接响应请求，攻击者一旦发现其地址，就会绕过上述所有规则；这正是下面这些应用层变量所要防范的场景。
{% endstep %}
{% endstepper %}

{% hint style="info" %}
由 Cloudflare 缓存提供的请求永远不会到达源站，因此应用层的限流器根本看不到这些请求——这也是为什么流量控制应放在边缘层的另一个原因。
{% endhint %}

## 应用自己的安全网

四个环境变量会在后端构建后备层。默认情况下，这四个变量全部是可选的，并且都是关闭（或宽松）状态。

| 变量                                 | 用途                | 默认值           |
| ---------------------------------- | ----------------- | ------------- |
| `ALLOWED_DOMAINS`                  | 哪些站点可以调用 `/api/*` | `localhost` 仅 |
| `SECURITY_RATE_LIMIT`              | 每个 IP 的硬性请求上限     | `0` — 已禁用     |
| `SECURITY_DELAY_AFTER`             | 每个 IP 的渐进式减速      | `0` — 已禁用     |
| `SECURITY_BLACKLIST_LOG_FILE_PATH` | 限流 IP 的磁盘日志       | 为空——不写文件      |

## `ALLOWED_DOMAINS` — Referer 检查门

每个 `/api/*` 路由都会经过一次 referer 检查。后端读取 `Referer` 头部，提取其中的 **主机名**，并要求该主机名必须在允许列表中。

允许列表为 `localhost` 加上 `ALLOWED_DOMAINS`.

拒绝结果为 `403`:

| 情况                         | 响应                    |
| -------------------------- | --------------------- |
| 没有 `Referer` 任何头部          | `{"error": "你在做什么？"}` |
| 存在 Referer，但主机名不被允许（或无法解析） | `{"error": "访问被拒绝"}`  |

{% hint style="danger" %}
**如果你把 MyIP 部署在真实域名上，就必须设置这个。** 当 `ALLOWED_DOMAINS` 为空时，仅 `localhost` 会通过——因此通过以下地址访问的部署 `https://ip.example.com` 或 `http://192.168.1.10:18966` 会在 `403` 每次 API 调用时都会被拦下，而静态页面却能正常加载，看起来就像应用坏了一样。
{% endhint %}

需要记住的规则：

* **仅限主机名。** 不要带协议、端口或路径： `example.com`，而不是 `https://example.com:443/`.
* **必须精确匹配。** `example.com` 不包含 `www.example.com`。两个都要列出。
* **IP 地址也算作主机名。** 以以下地址访问应用 `http://192.168.1.10:18966` 就意味着要添加 `192.168.1.10`.
* **`localhost` 始终允许**，因此本地开发无需配置。

{% code title=".env" %}

```bash
ALLOWED_DOMAINS="example.com,www.example.com,ip.example.com"
```

{% endcode %}

{% hint style="warning" %}
referer 检查是一种反滥用手段，而不是身份验证。任何客户端都可以发送任意的 `Referer` 头部。它可以阻止随手盗链和顺手写成的脚本；但阻止不了有决心的抓取器。请将它与限流配合使用。
{% endhint %}

在反向代理之后，请确保你的代理会原样转发 `Referer` 头部。参见 [反向代理与域名](/developer/zh/getting-started/reverse-proxy-and-domains.md).

## `SECURITY_RATE_LIMIT` — 硬性上限

对 `/api/*`按 IP 的请求上限，由 `express-rate-limit`.

* **时间窗口**：20 分钟，滚动窗口。
* **限制**：你设置的数字。 `0` 或为空意味着限流中间件根本不会挂载。
* **超过上限时**: `429` 返回 `{"message": "请求过多"}`.
* **豁免路由**: `/api/monitoring`，这是 Sentry 隧道，它有自己的限流器——参见 [错误监控](/developer/zh/configuration/error-monitoring.md).

{% hint style="info" %}
启动时会打印 `🛡️ 已启用限流器——每 60 分钟 N 个请求`。代码中实际执行的时间窗口是 **20 分钟**.
{% endhint %}

### 记录什么

后端会在某个 IP 越过阈值的那一刻记录日志—— **仅一次**，在状态切换时记录，而不是在之后每一次被阻止的请求上都记录。这样可以避免一个持续轰击受限端点的恶意客户端把你的日志冲爆。

```
警告：IP 已被限流
    ip: "203.0.113.45"
```

当配置了 Sentry 后端 DSN 时，这一行也会同步到 Sentry。

### 客户端 IP 的判定方式

限流器和日志行会按以下顺序解析调用者的 IP：

1. `CF-Connecting-IP`
2. X-Forwarded-For 的第一项 `X-Forwarded-For`
3. `CF-Connecting-IPv6`
4. Express 看到的套接字地址

应用运行时的 `trust proxy` 设置为 `1`，这意味着它只信任一跳代理头部。

{% hint style="warning" %}
如果你的反向代理没有设置 `X-Forwarded-For`，那么每个请求看起来都像是来自代理——一个 IP、一个共享配额，而第一个最忙的访客就会把其他所有人都挡在外面。在启用限流器之前，请先验证头部转发是否正确。
{% endhint %}

## `SECURITY_DELAY_AFTER` — 渐进式减速

更温和的配套方案，由 `express-slow-down`提供。它不会拒绝请求，而是会延迟。

* **时间窗口**：60 分钟，滚动窗口。
* **免费请求**：你设置的数字。超过这个数的请求会被缓慢响应。
* **延迟**: `400 毫秒 × 在窗口内发出的总请求数`.
* `0` 或为空意味着中间件根本不会挂载。
* 同样的 `/api/monitoring` 豁免。

{% hint style="warning" %}
延迟是根据 **总** 命中次数计算的，而不是按超出量计算——所以一开始就很高，并且上升很快。使用 `SECURITY_DELAY_AFTER="40"`，第 41 个请求就已经要等待约 16 秒；而使用 `"100"`，第 101 个请求就要等待约 40 秒。选择这个值时要明白，第一个被限流的请求就已经是很长的等待了，并预期其后会出现客户端超时。
{% endhint %}

减速和限流是叠加的。将 `SECURITY_RATE_LIMIT` 视为主要防线，只有在你希望脚本化突发流量被拖慢而不是直接失败时，才使用减速。

## `SECURITY_BLACKLIST_LOG_FILE_PATH` — 磁盘日志账本

可选启用。设置后，每一次限流状态切换也会追加到一个纯文本文件中。

{% code title=".env" %}

```bash
SECURITY_BLACKLIST_LOG_FILE_PATH="logs/blacklist-ip.log"
```

{% endcode %}

* 该路径会解析为 **相对于应用根目录**。缺失的目录会自动创建。
* 每个 IP 一行 CSV： `ip,count,first-seen-timestamp`.
* 时间戳是带有明确 UTC 偏移的主机本地时间，例如 `2026-07-14 10:23:45 +0800`.
* 对于重复违规者， **count 会递增，而原始时间戳保持**不变，这样你就能看出某个 IP 最初是何时出现的。

```
203.0.113.45,7,2026-07-14 10:23:45 +0800
198.51.100.9,1,2026-07-15 02:11:07 +0800
```

把它留空不会改变执行方式——警告日志仍然会触发。该文件是为那些需要永久记录的部署准备的，例如用于喂给防火墙或 fail2ban 风格的脚本。

{% hint style="info" %}
在 Docker 中，请把账本写到挂载卷里。容器可写层中的路径在容器重建时会消失。
{% endhint %}

## 中间件顺序

在你调试一个 `403` 或一个 `429`:

1. HTTP 请求日志记录时值得了解，如果 `LOG_HTTP=true` ——因此 429 会出现在日志中
2. 限流器（如启用）
3. 减速器（如启用）
4. JSON 请求体解析
5. Referer 门禁
6. 路由处理器

因此限流发生在 **早于** referer 检查。带着错误 referer 的请求洪流仍然会消耗违规者的配额——这正是预期的行为。

另外还要注意，位于应用前方的 CDN 会直接提供缓存的 `/api/*` 响应，而不会到达源站，因此这些请求对限流器是不可见的。大多数读多写少的路由都可以在边缘层缓存。

## 公共实例的推荐后备值

即使边缘层已经配置好了，也请设置这些值，让源站能够独立自保：

{% tabs %}
{% tab title="Node（.env）" %}
{% code title=".env" %}

```bash
ALLOWED_DOMAINS="example.com,www.example.com"
SECURITY_RATE_LIMIT="600"
SECURITY_BLACKLIST_LOG_FILE_PATH="logs/blacklist-ip.log"
```

{% endcode %}
{% endtab %}

{% tab title="Docker" %}

```bash
docker run -d -p 18966:18966 \
  -e ALLOWED_DOMAINS="example.com,www.example.com" \
  -e SECURITY_RATE_LIMIT="600" \
  -e SECURITY_BLACKLIST_LOG_FILE_PATH="logs/blacklist-ip.log" \
  -v "$(pwd)/logs:/app/logs" \
  --name myip \
  jason5ng32/myip:latest
```

{% endtab %}
{% endtabs %}

这些数字只是起点，不是规则。请按实际情况校准：

* 一次完整页面加载会触发 **许多** `/api/*` 调用——IP 源、连通性检查、DNS 探测。单个访客在一次会话中轻松就能消耗几十个请求。
* 如果把限额设得太低，正常用户就会在 `429` 诊断中途被拦住。
* 请观察账本和 `IP 已被限流` 警告一周，然后再收紧。
* 在 CGNAT 或企业 NAT 后面，很多真实用户会共享同一个 IP。请留足余量。

{% hint style="success" %}
一句话概括分层：让边缘层吸收并过滤流量，并把这些变量设置得足够宽松，使它们只会在已经漏过边缘层的流量上触发。
{% endhint %}

## 相关页面

* [环境变量](/developer/zh/reference/environment-variables.md) — 完整列表
* [反向代理与域名](/developer/zh/getting-started/reverse-proxy-and-domains.md) — 你的代理必须转发的头部
* [日志记录](/developer/zh/configuration/logging.md) — 警告显示在哪里
* [可选 API 密钥](/developer/zh/configuration/optional-api-keys.md) — 你正在保护的配额


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/zh/configuration/security-options.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
