---
type: tutorial
title: 用 nginx 设置代理门禁
summary: 在一个站点密钥上启用门禁，把一个 nginx 反向代理从头到尾接好，并在它拦下哪怕一个真实访客之前，在预览模式里观察它。
---

到这篇教程结束时，门禁将运行在一个 nginx 之后的站点前面：一个未放行的访客被送去一局游戏，一局解开的游戏设置一个 Cookie，之后的请求直接通过。你会先在 **预览模式** 里观察整件事，这样在你确认接线之前不会拦下任何东西。这里用的是 nginx；[Traefik、Caddy 和 Authelia 食谱](/docs/proxy-page-gate/reverse-proxy-recipes) 遵循同样的形状。

<Callout type="tip">
在你碰你的代理之前先做第 1 步。预览模式让门禁只在监视模式下运行：仪表盘记录它 *本会* 做什么，但每个请求仍然通过，所以一个接线错误绝不会把任何人锁在外面。
</Callout>

## 1. 打开预览模式

在仪表盘里，打开团队并打开 **预览模式**。它开着时，门禁授权方把挑战对直接放行的决定记录到 [统计页](/docs/proxy-page-gate/statistics)，但绝不拦截。你会在最后把它关掉。

## 2. 在你的站点密钥上启用门禁

打开你的站点密钥，去 **代理门禁** 页。打开 **状态 → 已启用**，然后设置：

- **通行 TTL**：一次解题放行一个访客多久（默认 30 分钟）。更短意味着更多复查，但如果一个 Cookie 被偷，窗口更小。
- **失败模式**：当你的代理够不到 Caputchin 的授权方时它怎么做。**失败即关闭**（fail closed）拦截（对登录门户更安全）；**失败即开放**（fail open）放行请求（在公开站点上避免停机）。

该页也展示一段随取随用的 nginx 片段，你的站点密钥已经填好在里面。

## 3. 允许你正在设门禁的源

挑战只会重定向回一个 **被允许的源**，就是组件所用的那份相同的源允许列表。确保它包含你正在设门禁的源，例如 `https://auth.example.com`。如果它还不在那里，就在站点密钥的 Cap 配置里设上它。

## 4. 接好授权方和挑战重定向

两个 `location` 块：一个内部的 **授权方** 子请求，和一个代理在 `401` 时跳去的 **挑战** 重定向。把 `YOUR_SITE_KEY` 换成你的公钥。

```nginx
# Gate the protected paths.
location / {
  auth_request /_cpt_authz;             # 204 = allow, 401 = challenge
  error_page 401 = @cpt_challenge;
  # ...proxy_pass to your app...
}

location = /_cpt_authz {
  internal;
  proxy_pass https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY;
  proxy_pass_request_body off;
  proxy_set_header Content-Length "";
  proxy_set_header Cookie $http_cookie;  # forward the cpt_gate cookie
}

location @cpt_challenge {
  return 302 https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=$scheme://$host$request_uri;
}
```

给入口路径设门禁（门户的 HTML 和登录 POST）。**不要** 给授权方子请求、健康检查、或下面的回调设门禁。

## 5. 加上设置 Cookie 的回调

挑战页在结束时会向你源上的 `/__cpt/callback` 自动提交一个小 `POST`，在 **正文** 里带着 `cpt_gate`（通行证）和 `to`（把访客送去哪里）。一个微小的处理器设置 Cookie 并重定向。完整契约，包括每个 Cookie 属性为什么重要，在 [反向代理食谱](/docs/proxy-page-gate/reverse-proxy-recipes#the-callback) 里。最低限度：

```js
app.post("/__cpt/callback", express.urlencoded({ extended: false }), (req, res) => {
  const pass = String(req.body.cpt_gate || "");
  const to = String(req.body.to || "/");
  const target = new URL(to, `https://${req.headers.host}`);
  if (target.host !== req.headers.host) return res.status(400).end(); // same-origin only
  res.setHeader(
    "Set-Cookie",
    `cpt_gate=${pass}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=1800`
  );
  res.redirect(303, target.pathname + target.search);
});
```

## 6. 在预览里观察它

加载那个受门禁的 URL。因为预览开着，你直接通过，但 [统计页](/docs/proxy-page-gate/statistics) 记录一个 `challenge_served`（你没有 Cookie），并且，一旦你解开题、回调跑起来，一个 `pass_issued`。刷新，你应该看到 `passthrough` 在爬升。如果挑战从不出现，那说明授权方块没被命中；如果回调返回 400，就检查 `to` 源。

## 7. 上线

当数字看起来对了，就把 **预览模式** 关掉。门禁现在开始执行：未放行的访客被送去游戏，解了题的访客漫游到他们的通行证过期为止。

## 接下来去哪

- [反向代理食谱](/docs/proxy-page-gate/reverse-proxy-recipes)：Traefik、Caddy，以及完整的 Authelia 演练。
- [代理门禁统计](/docs/proxy-page-gate/statistics)：你上线后每个数字意味着什么。
- [总览](/docs/proxy-page-gate/overview)：概念，以及门禁刻意不做什么。
