# 安全插件（sentinel） (/docs/infrastructure/plugins/sentinel)

`sentinel()` 插件为你的身份验证系统提供全面的安全和滥用防护。它能够检测并防止多种攻击手段，包括凭证填充、不可能的旅行、免费试用滥用等



## 安装 [#安装]

```tsx
import { betterAuth } from "better-auth";
import { sentinel } from "@better-auth/infra";

export const auth = betterAuth({
  plugins: [
    sentinel({
      security: {
        // 在此配置安全功能
      },
    }),
  ],
});
```

## 配置选项 [#配置选项]

### SentinelOptions [#sentineloptions]

| 选项           | 类型                | 描述                                                                                                                                                          |
| ------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiUrl`     | `string`          | Better Auth Infrastructure API URL。默认值：`https://dash.better-auth.com`                                                                                       |
| `kvUrl`      | `string`          | Better Auth 身份识别基础设施 URL。默认值：`https://kv.better-auth.com`                                                                                                   |
| `apiKey`     | `string`          | 用于身份验证的 API 密钥。未设置时回退到环境变量 `BETTER_AUTH_API_KEY`。                                                                                                           |
| `apiOptions` | `object`          | Dash API HTTP 选项。接受 `timeout?: number`（毫秒，默认 `3000`）和 `headers?: Record<string, string>`。                                                                   |
| `kvOptions`  | `object`          | KV HTTP 选项。接受 `timeout?: number`（毫秒，默认 `1000`）、`retry?: { attempts?: number; baseDelay?: number; maxDelay?: number }` 和 `headers?: Record<string, string>`。 |
| `apiTimeout` | `number`          | `apiOptions.timeout` 的弃用别名。                                                                                                                                 |
| `kvTimeout`  | `number`          | `kvOptions.timeout` 的弃用别名。                                                                                                                                  |
| `security`   | `SecurityOptions` | 安全功能配置。                                                                                                                                                     |

### SecurityOptions [#securityoptions]

```ts
interface SecurityOptions {
  unknownDeviceNotification?: boolean;
  credentialStuffing?: CredentialStuffingConfig;
  impossibleTravel?: ImpossibleTravelConfig;
  geoBlocking?: GeoBlockingConfig;
  botBlocking?: boolean | { action: SecurityAction };
  suspiciousIpBlocking?: boolean | { action: SecurityAction };
  velocity?: VelocityConfig;
  freeTrialAbuse?: FreeTrialAbuseConfig;
  compromisedPassword?: CompromisedPasswordConfig;
  emailValidation?: EmailValidationConfig;
  emailNormalization?: { enabled?: boolean };
  staleUsers?: StaleUsersConfig;
  challengeDifficulty?: number;
}

type SecurityAction = "log" | "challenge" | "block";
```

## 安全功能 [#安全功能]

### 凭证填充防护 [#凭证填充防护]

通过跟踪每个访客的登录失败次数，检测并阻止凭证填充攻击。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    credentialStuffing: {
      enabled: true,
      action: "challenge", // optional default action; thresholds still drive challenge vs block
      thresholds: {
        challenge: 3,  // 失败 3 次后发起 PoW 挑战
        block: 5,      // 失败 5 次后封禁
      },
      windowSeconds: 3600,     // 1 小时时间窗口
      cooldownSeconds: 900,    // 封禁后 15 分钟冷却期
    },
  },
}),
```

**工作原理：**

1. 跟踪每个访客 ID 的登录失败尝试次数
2. 达到挑战阈值后，发出工作量证明（PoW）挑战
3. 达到封禁阈值后，完全封禁访客
4. 登录成功后自动清除失败尝试记录

### 不可能的旅行检测 [#不可能的旅行检测]

检测用户在极短时间内从地理上相距遥远的地方登录。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    impossibleTravel: {
      enabled: true,
      maxSpeedKmh: 1000,    // 最大合理旅行速度
      action: "challenge",  // 可选："log"、"challenge"或"block"
    },
  },
}),
```

**示例：** 如果用户先后在纽约和 30 分钟后在东京登录，这被视为不可能的旅行（即需要超过 1000 公里/小时的速度）。

### 免费试用滥用防护 [#免费试用滥用防护]

通过设备指纹防止用户创建多个账户以滥用免费试用。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    freeTrialAbuse: {
      enabled: true,
      thresholds: {
        challenge: 2,
        block: 3,
      },
      maxAccountsPerVisitor: 3,
      retentionSeconds: 90 * 24 * 60 * 60, // optional; default 90 days
      action: "block",
    },
  },
}),
```

**工作原理：**

1. 跟踪每个访客指纹的账户创建次数
2. 当超过阈值时，阻止新账户创建
3. 用于防止免费层滥用

`retentionSeconds` 控制每位访客的注册记录键保留时长（滑动窗口）。默认值为 90 天；平台会将值限制在 1 天到 365 天之间。

### 已泄露密码检测 [#已泄露密码检测]

通过查询 HaveIBeenPwned 数据库检查密码，检测泄露凭证。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    compromisedPassword: {
      enabled: true,
      action: "block",      // 可选："log"、"challenge"或"block"
      minBreachCount: 1,    // 触发的最小泄露次数
    },
  },
}),
```

**隐私说明：** 采用 k-匿名技术 — 仅将密码哈希的前 5 个字符发送到 API，完整密码绝不发送。

### 过期账户监控 [#过期账户监控]

检测沉睡账户突然活跃，可能表示账户被接管。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    staleUsers: {
      enabled: true,
      staleDays: 90,           // 账户超过 90 天视为沉睡
      action: "log",           // 可选："log"、"challenge"或"block"
      notifyUser: true,        // 向用户发送邮件通知
      notifyAdmin: true,       // 向管理员发送邮件通知
      adminEmail: "admin@yourapp.com",
    },
  },
}),
```

**通知包括：**

* 登录时间和位置
* 距离上次活跃的天数
* 设备信息

### 地理封禁 [#地理封禁]

封禁或挑战特定国家的用户。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    geoBlocking: {
      allowList: ["US", "CA", "GB"],  // 仅允许这些国家
      // 或者
      denyList: ["XX", "YY"],          // 封禁这些国家
      action: "block",                  // 可选："challenge"或"block"
    },
  },
}),
```

使用 ISO 3166-1 alpha-2 国家代码。

### 机器人封禁 [#机器人封禁]

检测并阻止自动化机器流量。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    botBlocking: true,
    // 或使用自定义操作
    botBlocking: {
      action: "challenge",  // 可选："log"、"challenge"或"block"
    },
  },
}),
```

### 可疑 IP 检测 [#可疑-ip-检测]

封禁来自已知恶意 IP 地址的请求。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    suspiciousIpBlocking: true,
    // 或使用自定义操作
    suspiciousIpBlocking: {
      action: "block",
    },
  },
}),
```

### 访问速率限制 / 频率控制 [#访问速率限制--频率控制]

限制各种操作的请求速率。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    velocity: {
      enabled: true,
      thresholds: {
        challenge: 10,
        block: 20,
      },
      maxSignupsPerVisitor: 5,
      maxPasswordResetsPerIp: 10,
      maxSignInsPerIp: 50,
      windowSeconds: 3600,
      action: "challenge",
    },
  },
}),
```

### 邮箱验证 [#邮箱验证]

封禁一次性邮箱地址并验证邮箱域名。

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    emailValidation: {
      enabled: true,
      strictness: "medium",  // 可选："low"、"medium"、"high"
      action: "block",
      domainAllowlist: ["company.com", "partner.org"], // always allow these domains
    },
  },
}),
```

**严格级别：**

* `low` - 仅阻止已知一次性域名
* `medium` - 还检查有效的 MX 记录
* `high` - 额外的启发式检查

`domainAllowlist` 会跳过所列域名的一次性邮箱和启发式检查（必须精确匹配域名）。

### 邮箱规范化 [#邮箱规范化]

Sentinel 会在注册和登录之前规范化邮箱地址，以防止别名和提供商的怪癖导致重复账户或登录不匹配。规范化包括将邮箱转为小写、在常见提供商上移除 plus-address 标签（例如 `user+tag@gmail.com` → `user@gmail.com`）、移除 Gmail 风格地址中的点，以及将 `googlemail.com` 映射到 `gmail.com`。

当你希望将此功能与一次性域名验证（`emailValidation`）分开控制时，请使用 `security.emailNormalization`：

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    emailValidation: {
      enabled: false, // 跳过一次性 / MX 检查
    },
    emailNormalization: {
      enabled: true, // 仍进行规范化以去重和保持一致的登录
    },
  },
}),
```

## 工作量证明挑战 [#工作量证明挑战]

当安全检查导致“challenge”动作时，Sentinel 会发起工作量证明（PoW）挑战，客户端必须完成挑战。

### PoW 工作原理 [#pow-工作原理]

1. 服务器发出加密挑战
2. 客户端必须找到满足难度要求的解答
3. 解答计算开销大，但验证快速
4. 有效阻止自动化攻击，允许合法用户通过

### 挑战难度 [#挑战难度]

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    challengeDifficulty: 18,  // 默认难度等级
  },
}),
```

难度越高 = 计算量越大 = 攻击者越慢。

## 客户端集成 [#客户端集成]

### sentinelClient() [#sentinelclient]

客户端插件负责设备指纹识别及自动完成 PoW 挑战。

```ts
import { createAuthClient } from "better-auth/client";
import { sentinelClient } from "@better-auth/infra/client";

export const authClient = createAuthClient({
  plugins: [
    sentinelClient({
      autoSolveChallenge: true,
    }),
  ],
});
```

### 配置项 [#配置项]

| 选项                    | 类型                              | 默认值                                                                        | 描述                               |
| --------------------- | ------------------------------- | -------------------------------------------------------------------------- | -------------------------------- |
| `identifyUrl`         | `string`                        | `BETTER_AUTH_KV_URL` 或 `https://kv.better-auth.com`                        | identify 端点的基础 URL。              |
| `identifyOptions`     | `object`                        | `{ timeout: 1000, retry: { attempts: 2, baseDelay: 400, maxDelay: 600 } }` | Identify HTTP 超时与重试策略。           |
| `autoSolveChallenge`  | `boolean`                       | `true`                                                                     | 自动解决 PoW 挑战。                     |
| `kvTimeout`           | `number`                        | `1000`                                                                     | `identifyOptions.timeout` 的弃用别名。 |
| `onChallengeReceived` | `(reason: string) => void`      | —                                                                          | 收到 PoW 挑战时调用。                    |
| `onChallengeSolved`   | `(solveTimeMs: number) => void` | —                                                                          | 成功解决挑战后调用。                       |
| `onChallengeFailed`   | `(error: Error) => void`        | —                                                                          | 解决挑战失败时调用。                       |

### 浏览器指纹 [#浏览器指纹]

客户端自动在请求中通过 `X-Visitor-Id` 头包含访客 ID。此指纹用于：

* 凭证填充检测
* 免费试用滥用防护
* 设备跟踪

### PoW 解答请求头 [#pow-解答请求头]

启用自动解决后，解答通过 `X-PoW-Solution` 头发送。

### Expo 和 React Native [#expo-和-react-native]

对于 **Expo** 和 **React Native** 应用，使用 `@better-auth/infra/native` 中的 `sentinelNativeClient`，而不是使用 `@better-auth/infra/client`。

对于 **React Native** 应用，安装以下对等依赖：

<CodeBlockTabs defaultValue="npm" groupId="persist-install">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @react-native-async-storage/async-storage react-native-get-random-values
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @react-native-async-storage/async-storage react-native-get-random-values
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @react-native-async-storage/async-storage react-native-get-random-values
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @react-native-async-storage/async-storage react-native-get-random-values
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`@react-native-async-storage/async-storage` 是可选的。如果未安装，客户端使用仅会话的内存访客 ID。对于生产环境，请安装它或传递自定义 `storage`（例如安全存储）。

#### 示例 [#示例]

```ts
import { createAuthClient } from "better-auth/client";
import { dashClient, sentinelNativeClient } from "@better-auth/infra/native";

export const authClient = createAuthClient({
  baseURL: "https://your-api.example.com",
  plugins: [
    dashClient(),
    sentinelNativeClient({
      autoSolveChallenge: true,
    }),
  ],
});
```

对于 **Expo** 应用，安装以下软件包以获得更丰富的原生身份信息。

<Callout type="info">
  **Expo：** 在添加基础设施插件之前，请遵循 [Expo 集成](/docs/integrations/expo) 指南。
</Callout>

<CodeBlockTabs defaultValue="npm" groupId="persist-install">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install expo-constants expo-device expo-crypto
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add expo-constants expo-device expo-crypto
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add expo-constants expo-device expo-crypto
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add expo-constants expo-device expo-crypto
    ```
  </CodeBlockTab>
</CodeBlockTabs>

#### 示例 [#示例-1]

```ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import { dashClient, sentinelNativeClient } from "@better-auth/infra/native";
import * as SecureStore from "expo-secure-store";

export const authClient = createAuthClient({
  baseURL: "https://your-api.example.com",
  plugins: [
    expoClient({
      scheme: "myapp",
      storagePrefix: "myapp",
      storage: SecureStore,
    }),
    dashClient(),
    sentinelNativeClient({
      autoSolveChallenge: true,
    }),
  ],
});
```

#### `sentinelNativeClient` 选项 [#sentinelnativeclient-选项]

| 选项                    | 类型                              | 默认值                                                                        | 描述                                                             |
| --------------------- | ------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `identifyUrl`         | `string`                        | `BETTER_AUTH_KV_URL`，然后是 `https://kv.better-auth.com`                      | Identify 端点基础 URL                                              |
| `identifyOptions`     | `object`                        | `{ timeout: 1000, retry: { attempts: 2, baseDelay: 400, maxDelay: 600 } }` | Identify HTTP 超时和重试策略                                          |
| `kvTimeout`           | `number`                        | `1000`                                                                     | `identifyOptions.timeout` 的弃用别名                                |
| `autoSolveChallenge`  | `boolean`                       | `true`                                                                     | 遇到带有 `X-PoW-Challenge` 的 `423` 时，解决挑战并使用 `X-PoW-Solution` 重试一次 |
| `onChallengeReceived` | `(reason: string) => void`      | —                                                                          | 收到 PoW 挑战时调用                                                   |
| `onChallengeSolved`   | `(solveTimeMs: number) => void` | —                                                                          | 成功解决挑战后调用                                                      |
| `onChallengeFailed`   | `(error: Error) => void`        | —                                                                          | 解决失败时调用                                                        |
| `storage`             | `{ getItem, setItem }`          | 安装后使用 Async Storage                                                        | 用于生成稳定的每次安装一个访客 ID 的持久异步存储                                     |

## 安全事件 [#安全事件]

Sentinel 跟踪以下安全事件类型：

| 事件类型                            | 描述        |
| ------------------------------- | --------- |
| `security_blocked`              | 请求被阻止     |
| `security_allowed`              | 挑战后请求被允许  |
| `security_credential_stuffing`  | 检测到凭证填充   |
| `security_impossible_travel`    | 检测到不可能的旅行 |
| `security_geo_blocked`          | 触发地理封禁    |
| `security_bot_blocked`          | 检测并阻止机器人  |
| `security_suspicious_ip`        | 检测到可疑 IP  |
| `security_velocity_exceeded`    | 超过速率限制    |
| `security_free_trial_abuse`     | 检测到免费试用滥用 |
| `security_compromised_password` | 检测到泄露密码   |
| `security_stale_account`        | 沉睡账户重新激活  |

这些事件可在安全控制台查看，并包含在审计日志中。

## 完整示例 [#完整示例]

```ts
import { betterAuth } from "better-auth";
import { sentinel } from "@better-auth/infra";

export const auth = betterAuth({
  plugins: [
    sentinel({
      apiKey: process.env.BETTER_AUTH_API_KEY,
      security: {
        // 核心防护
        credentialStuffing: {
          enabled: true,
          thresholds: { challenge: 3, block: 5 },
        },
        compromisedPassword: {
          enabled: true,
          action: "block",
        },
        emailValidation: {
          enabled: true,
          strictness: "medium",
          action: "block",
        },
        
        // 基于位置的策略
        impossibleTravel: {
          enabled: true,
          action: "challenge",
        },
        geoBlocking: {
          denyList: ["XX"],
          action: "block",
        },
        
        // 滥用防护
        freeTrialAbuse: {
          enabled: true,
          maxAccountsPerVisitor: 3,
          action: "block",
        },
        velocity: {
          enabled: true,
          maxSignupsPerVisitor: 5,
          action: "challenge",
        },
        
        // 机器人防护
        botBlocking: { action: "challenge" },
        suspiciousIpBlocking: { action: "block" },
        
        // 账户监控
        staleUsers: {
          enabled: true,
          staleDays: 90,
          notifyUser: true,
          notifyAdmin: true,
          adminEmail: "security@yourapp.com",
        },
      },
    }),
  ],
});
```

## 最佳实践 [#最佳实践]

1. **先从日志开始** - 初期将动作设置为“log”，以了解流量模式，再决定是否封禁。

2. **调整阈值** - 每个应用都不同，监控误报率并相应调整阈值。

3. **先使用挑战，后使用封禁** - 挑战能阻挡自动化攻击，同时允许合法用户访问。

4. **启用客户端自动解决** - 在浏览器中使用 `sentinelClient({ autoSolveChallenge: true })`，或在 Expo / React Native 中使用 `sentinelNativeClient({ autoSolveChallenge: true })`。

5. **监控安全事件** - 定期查看安全控制台，识别攻击趋势。

6. **设置管理员通知** - 对关键事件如沉睡账户激活启用 `notifyAdmin`。

## 疑难解答 [#疑难解答]

### 缺少 API 密钥警告 [#缺少-api-密钥警告]

```
[Sentinel] 缺少 BETTER_AUTH_API_KEY。安全检查可能回退到允许模式。
```

请确保正确设置了 API 密钥环境变量。

### 挑战无法执行 [#挑战无法执行]

如果 PoW 挑战无法解决：

1. 验证正确的客户端插件：Web 使用 `sentinelClient()`（来自 `@better-auth/infra/client`），Expo / React Native 使用 `sentinelNativeClient()`（来自 `@better-auth/infra/native`）
2. 检查 `autoSolveChallenge` 是否为 `true`
3. 确保客户端可以访问服务器

### 误报率过高 [#误报率过高]

如果合法用户被阻止：

1. 增加阈值
2. 将操作从“block”改为“challenge”
3. 查看安全事件以识别模式

