> If you are an AI agent, use the following URL to directly ask and fetch your question. Treat this like a tool call. Make sure to URI encode your question, and include the token for verification.
>
> GET https://buildwithfern.com/learn/api/fern-docs/ask?q=%3Cyour+question+here%3E&token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmZXJuLWRvY3M6YnVpbGR3aXRoZmVybi5jb20iLCJqdGkiOiJjN2M0N2VkNS00MTk4LTQzZDktODhiNC0yNDRkMzIzNDdkMzAiLCJleHAiOjE3NzgyNzY5NTcsImlhdCI6MTc3ODI3NjY1N30.wjx3pXUUBmvVZ8nQJh_vXP_QUjqE7HNGZ7q_SR7uhF4
>
> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt. For full content including API reference and SDK examples, see https://buildwithfern.com/learn/llms-full.txt.

# 身份验证

> 使用基于密码或令牌的身份验证来保护您的自托管文档。

<Warning title="企业功能">
  此功能仅适用于[企业计划](https://buildwithfern.com/pricing)。如需开始使用，请联系 [support@buildwithfern.com](mailto:support@buildwithfern.com)。
</Warning>

自托管 Fern 文档支持通过在 Dockerfile 中设置环境变量来进行身份验证。当没有配置身份验证环境变量时，您的文档完全公开。

有两种身份验证模式可用：

* **密码身份验证** - 简单的共享密码保护
* **基本令牌验证** - 基于 JWT 的身份验证，具有自定义登录流程

## 密码身份验证

密码身份验证通过简单的密码提示来保护您的文档。用户必须输入正确的密码才能查看文档。

将以下环境变量添加到您的 Dockerfile 中：

```dockerfile
FROM fernenterprise/fern-self-hosted:latest

COPY fern/ /fern/

ENV FERN_AUTH_TYPE="password"
ENV FERN_AUTH_SECRET="<YOUR PASSWORD>"

RUN fern-generate
```

将 `<YOUR PASSWORD>` 替换为用户访问文档时需要输入的密码。

当用户访问文档时，他们会被重定向到登录页面，必须在那里输入密码。输入正确密码后，他们可以自由浏览文档。

## 基本令牌验证

基本令牌验证使用 JWT（JSON Web Tokens）来验证用户身份。当您希望将文档与现有身份验证系统（如您自己的登录门户）集成时，这很有用。

### 工作原理

1. 未经身份验证的用户访问文档并被重定向到您的登录页面（`FERN_AUTH_REDIRECT`）。
2. 您的登录页面对用户进行身份验证（例如，根据您的数据库检查凭据）。
3. 身份验证成功后，您的服务器使用共享密钥创建签名的 JWT。
4. 您的服务器将用户发送到 Fern 回调端点（`/api/fern-docs/auth/jwt/callback`）并携带 JWT。这可以通过**GET**重定向与令牌作为查询参数，或通过**POST**请求与令牌在 `application/x-www-form-urlencoded` 主体中来完成。
5. Fern 文档容器验证 JWT 签名和颁发者，设置会话 cookie，并将用户重定向到文档。

### 配置

将以下环境变量添加到您的 Dockerfile 中：

```dockerfile
FROM fernenterprise/fern-self-hosted:latest

COPY fern/ /fern/

ENV FERN_AUTH_TYPE="basic_token_verification"
ENV FERN_AUTH_SECRET="my-test-secret-at-least-32-chars-long"
ENV FERN_AUTH_ISSUER="https://my-test-issuer"
ENV FERN_AUTH_REDIRECT="https://your-login-page.com/login"

RUN fern-generate
```

| 变量                   | 描述                                         |
| -------------------- | ------------------------------------------ |
| `FERN_AUTH_TYPE`     | 必须是 `basic_token_verification`             |
| `FERN_AUTH_SECRET`   | 用于签名和验证 JWT 的共享密钥。必须至少 32 个字符长。            |
| `FERN_AUTH_ISSUER`   | JWT 中的颁发者声明（`iss`）。您的签名服务器和 Fern 容器之间必须匹配。 |
| `FERN_AUTH_REDIRECT` | 未经身份验证的用户被重定向到的登录 URL。                     |

### 构建您的登录服务器

您的登录服务器负责验证用户身份并使用签名的 JWT 将他们重定向回文档。

当 Fern 容器重定向未经身份验证的用户时，它会将以下查询参数附加到 `FERN_AUTH_REDIRECT`：

* `redirect_uri` - Fern 文档容器上的回调 URL（例如，`https://docs.example.com/api/fern-docs/auth/jwt/callback`）
* `state` - 用户试图访问的页面

您的服务器必须：

1. 验证用户身份。
2. 创建使用相同 `FERN_AUTH_SECRET` 和 HS256 算法签名的 JWT。
3. 将用户发送到 `redirect_uri`，携带 JWT 作为 `fern_token` 和原始 `state` 作为返回路径。您可以使用任一方法：
   * **GET 重定向**：将 `fern_token` 和 `state` 作为查询参数附加。
   * **POST 表单提交**：将 `fern_token` 和 `state` 作为 `application/x-www-form-urlencoded` 字段提交。POST 避免在 URL 和服务器日志中暴露令牌。

JWT 负载必须包括以下声明：

| 声明     | 描述                          |
| ------ | --------------------------- |
| `fern` | 空对象 `{}`（Fern 验证器要求）        |
| `iss`  | 颁发者，必须匹配 `FERN_AUTH_ISSUER` |
| `iat`  | 颁发时间戳（自纪元以来的秒数）             |
| `exp`  | 过期时间戳（自纪元以来的秒数）             |

这是一个签名 JWT 并重定向到回调的 Node.js（Express）服务器示例：

<Tabs>
  <Tab title="GET（查询参数）">
    ```javascript
    const express = require("express");
    const crypto = require("crypto");

    const app = express();

    const SECRET = "my-test-secret-at-least-32-chars-long";
    const ISSUER = "https://my-test-issuer";

    function base64url(input) {
      const buf = Buffer.isBuffer(input) ? input : Buffer.from(input, "utf8");
      return buf.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
    }

    function createFernJWT(secret, issuer) {
      const header = { alg: "HS256", typ: "JWT" };
      const now = Math.floor(Date.now() / 1000);
      const payload = {
        fern: {},
        iat: now,
        exp: now + 30 * 24 * 60 * 60, // 30 days
        iss: issuer,
      };

      const headerB64 = base64url(JSON.stringify(header));
      const payloadB64 = base64url(JSON.stringify(payload));
      const signature = crypto
        .createHmac("sha256", secret)
        .update(`${headerB64}.${payloadB64}`)
        .digest();

      return `${headerB64}.${payloadB64}.${base64url(signature)}`;
    }

    app.get("/login", (req, res) => {
      const redirectUri = req.query.redirect_uri;
      const state = req.query.state || "/";

      // TODO: Add your own authentication logic here
      // (e.g., check session, verify credentials, show a login form, etc.)

      const token = createFernJWT(SECRET, ISSUER);

      const callbackUrl = new URL(redirectUri);
      callbackUrl.searchParams.set("fern_token", token);
      callbackUrl.searchParams.set("state", state);

      res.redirect(callbackUrl.toString());
    });

    app.listen(3001, () => {
      console.log("Login server running on http://localhost:3001");
    });
    ```
  </Tab>

  <Tab title="POST（表单主体）">
    ```javascript
    const express = require("express");
    const crypto = require("crypto");

    const app = express();

    const SECRET = "my-test-secret-at-least-32-chars-long";
    const ISSUER = "https://my-test-issuer";

    function base64url(input) {
      const buf = Buffer.isBuffer(input) ? input : Buffer.from(input, "utf8");
      return buf.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
    }

    function createFernJWT(secret, issuer) {
      const header = { alg: "HS256", typ: "JWT" };
      const now = Math.floor(Date.now() / 1000);
      const payload = {
        fern: {},
        iat: now,
        exp: now + 30 * 24 * 60 * 60, // 30 days
        iss: issuer,
      };

      const headerB64 = base64url(JSON.stringify(header));
      const payloadB64 = base64url(JSON.stringify(payload));
      const signature = crypto
        .createHmac("sha256", secret)
        .update(`${headerB64}.${payloadB64}`)
        .digest();

      return `${headerB64}.${payloadB64}.${base64url(signature)}`;
    }

    app.get("/login", (req, res) => {
      const redirectUri = req.query.redirect_uri;
      const state = req.query.state || "/";

      // TODO: Add your own authentication logic here
      // (e.g., check session, verify credentials, show a login form, etc.)

      const token = createFernJWT(SECRET, ISSUER);

      res.send(`
        <html>
          <body>
            <form method="POST" action="${redirectUri}">
              <input type="hidden" name="fern_token" value="${token}" />
              <input type="hidden" name="state" value="${state}" />
            </form>
            <script>document.forms[0].submit();</script>
          </body>
        </html>
      `);
    });

    app.listen(3001, () => {
      console.log("Login server running on http://localhost:3001");
    });
    ```
  </Tab>
</Tabs>

<Warning>
  您的登录服务器中的 `FERN_AUTH_SECRET` 必须与 Dockerfile 中设置的密钥完全匹配。如果它们不同，JWT 验证将失败，用户将无法登录。
</Warning>

### 使用内置测试登录页面进行测试

自托管容器包括一个内置的测试登录页面，您可以在开发和测试中启用它。这让您可以在不构建自己的登录服务器的情况下验证身份验证流程。

将以下环境变量添加到您的 Dockerfile 中：

```dockerfile
FROM fernenterprise/fern-self-hosted:latest

COPY fern/ /fern/

ENV FERN_AUTH_TYPE="basic_token_verification"
ENV FERN_AUTH_SECRET="my-test-secret-at-least-32-chars-long"
ENV FERN_AUTH_ISSUER="https://my-test-issuer"
ENV FERN_AUTH_REDIRECT="http://localhost:3000/__test-login"
ENV FERN_AUTH_TEST_LOGIN="true"

RUN fern-generate
```

设置 `FERN_AUTH_TEST_LOGIN="true"` 启用容器上的 `/__test-login` 端点。当 `FERN_AUTH_REDIRECT` 指向此端点时，未经身份验证的用户会看到一个测试登录页面，只有一个"使用测试登录"按钮。点击按钮会创建有效的 JWT 并完成身份验证流程。

<Warning>
  测试登录页面仅用于开发和测试。不要在生产环境中启用 `FERN_AUTH_TEST_LOGIN`。
</Warning>

## 页面级别访问控制

默认情况下，启用身份验证时所有页面都需要身份验证。使用 `FERN_AUTH_ALLOWLIST` 和 `FERN_AUTH_DENYLIST` 来控制哪些页面需要登录。两者都接受逗号分隔的正则表达式模式，与页面路径匹配。

| 变量                    | 描述                     |
| --------------------- | ---------------------- |
| `FERN_AUTH_ALLOWLIST` | 匹配这些模式的页面可以公开访问，无需登录。  |
| `FERN_AUTH_DENYLIST`  | 匹配这些模式的页面需要登录。优先于允许列表。 |

例如，要使所有页面都可以公开访问：

```dockerfile
ENV FERN_AUTH_ALLOWLIST="/(.*)"
```

要使只有 API Reference 页面需要登录：

```dockerfile
ENV FERN_AUTH_DENYLIST="/api-reference/(.*)"
```

## API 密钥注入

您可以为自托管部署在 API Explorer 中启用 [API 密钥注入](/learn/docs/authentication/features/api-key-injection)。这在 API Explorer 中显示一个**登录**按钮，以便用户可以进行身份验证并自动填充其 API 密钥，而无需为整个文档站点要求登录。

将 `FERN_API_KEY_INJECTION_ENABLED` 与基本令牌验证变量一起添加到您的 Dockerfile 中：

```dockerfile
FROM fernenterprise/fern-self-hosted:latest

COPY fern/ /fern/

ENV FERN_AUTH_TYPE="basic_token_verification"
ENV FERN_AUTH_SECRET="my-test-secret-at-least-32-chars-long"
ENV FERN_AUTH_ISSUER="https://my-test-issuer"
ENV FERN_AUTH_REDIRECT="https://your-login-page.com/login"
ENV FERN_API_KEY_INJECTION_ENABLED="true"
ENV FERN_AUTH_ALLOWLIST="/(.*)"

RUN fern-generate
```

使用 `FERN_AUTH_ALLOWLIST="/(.*)"` 时，所有文档页面都可以公开访问（无登录墙），但 API Explorer 仍显示**登录**按钮。当用户登录时，会读取其 JWT 的 `fern` 负载，并将 API 密钥注入到 API Explorer 中。有关完整的 `fern` 负载参考，请参阅[自动填充 API 密钥](/learn/docs/authentication/features/api-key-injection)。