# 状态不匹配 (/docs/reference/errors/state_mismatch)

OAuth 回调期间状态验证失败。涵盖所有与状态相关的错误代码及其原因。



## 什么是 state\_mismatch？ [#什么是-state_mismatch]

当 OAuth 或 SSO 流程开始时，Better Auth 会生成一个唯一的 `state` 值并将其存储，以便在提供方重定向回来时进行验证。这通过确保回调真正属于启动它的同一浏览器会话，来防止 CSRF 和重放攻击。

Better Auth 支持两种状态存储策略 - **database**（默认）和 **cookie** - 每种策略都有其自身的失败模式。本页涵盖每个状态相关的错误代码、触发原因以及如何修复。

***

## 错误代码概览 [#错误代码概览]

| 错误代码                      | 消息                    | 策略     | 含义                               |
| ------------------------- | --------------------- | ------ | -------------------------------- |
| `state_mismatch`          | 未找到验证记录               | 数据库    | 此状态的验证记录在数据库（或二级存储）中不存在。         |
| `state_mismatch`          | 未找到 auth state cookie | Cookie | 加密的状态 cookie 未随回调请求发送回来。         |
| `state_mismatch`          | 请求已过期                 | 两者     | 找到了状态数据，但其 `expiresAt` 时间戳已过期。   |
| `state_invalid`           | 无法解密或解析认证状态           | Cookie | 状态 cookie 存在但无法解密或解析（例如密钥已更改）。   |
| `state_security_mismatch` | 状态未正确持久化              | 数据库    | 签名的状态 cookie 缺失或与回调 URL 中的状态不匹配。 |
| `state_generation_error`  | 无法创建验证记录              | 数据库    | 启动流程时无法写入验证记录。                   |

***

## `state_mismatch` - 未找到验证记录（数据库策略） [#state_mismatch---未找到验证记录数据库策略]

这是**最常报告**的状态错误。这意味着从 OAuth 提供方返回的状态值用于在数据库中查找验证记录，但未找到匹配的记录。

### 常见原因 [#常见原因]

1. **用户在提供方的登录页面上花费了太长时间。** 验证记录在 10 分钟后过期。一旦过期，任何其他 `findVerificationValue` 调用（来自 OTP 检查、魔法链接、2FA 等）都会触发后台清理，删除所有过期记录 - 包括此记录。

2. **回调 URL 被加载了两次。** 成功查找后，验证记录会立即被删除。如果浏览器刷新、按下后退按钮或重定向循环重放回调，第二次请求将找不到任何内容。

3. **没有数据库回退的二级存储（Redis / KV）。** 当配置了 `secondaryStorage` 时，验证记录默认存储在那里，数据库会被跳过。如果键被逐出（TTL 过期、内存压力、服务器重启）且未将 `verification.storeInDatabase` 显式设置为 `true`，则查找会返回 `null` 而不检查数据库。

4. **多实例部署无共享状态。** 无服务器函数或运行各自独立内存 SQLite 的多个容器将不会共享验证记录。创建记录的实例可能不是接收回调的实例。

5. **流程期间标识符存储设置发生变化。** 在创建记录与处理回调之间更改 `verification.storeIdentifier` 或自定义标识符哈希器，可能导致无法找到已存储的记录。内置的 `"hashed"` 策略使用不带密钥的 SHA-256，因此更改 `BETTER_AUTH_SECRET` 不会改变标识符哈希。

6. **OAuth 提供方修改了 state 参数。** 某些提供方在重定向过程中对 `state` 查询参数进行 URL 编码、截断或以其他方式修改，导致查找时出现不匹配。

7. **缺少验证表。** 如果未运行数据库迁移或 `verification` 表被删除，查询将返回空。

### 如何修复 [#如何修复]

* 确保数据库（或 Redis）在应用的**所有实例之间共享**。
* 使用 `secondaryStorage` 时，可将 `verification.storeInDatabase: true` 设为回退方案，或确保存储层可靠且 TTL 足够长。
* 在所有实例和进行中的流程中，保持标识符存储设置和自定义哈希器一致。
* 如果用户经常遇到超时，可考虑切换到不依赖数据库查找的 `"cookie"` 策略。

***

## `state_mismatch` - 未找到 auth state cookie（cookie 策略） [#state_mismatch---未找到-auth-state-cookiecookie-策略]

当 `storeStateStrategy` 为 `"cookie"` 时，所有状态数据都会加密到 cookie 中。此错误意味着 cookie 在回调请求中不存在。

### 常见原因 [#常见原因-1]

* 浏览器阻止或剥离了 cookie（第三方 cookie 限制、Safari ITP、无痕模式）。
* cookie 域/路径与回调路由不匹配（例如 `.vercel.app` 预览域被视为公共后缀，无法在子域之间共享 cookie）。
* 反向代理或 CDN 丢弃了 `Cookie` 头。
* 用户在一个标签页中启动了流程，但在另一个标签页中完成了它（不同的 cookie 存储区）。

### 如何修复 [#如何修复-1]

* 使用**稳定的自定义域** - 避免 `.vercel.app` 预览子域。
* 验证您的 cookie 域和 `SameSite` / `Secure` 属性是否适合您的部署。
* 在重定向之前和之后，在 DevTools → Application → Cookies 中确认 cookie 存在。

***

## `state_mismatch` - 请求已过期 [#state_mismatch---请求已过期]

状态数据已成功检索（来自数据库或 cookie），但其嵌入的 `expiresAt` 时间戳已过去。状态有效载荷从创建起有效期为 10 分钟。

### 常见原因 [#常见原因-2]

* 用户只是花费了太长时间（让提供方标签页保持打开、网络慢、MFA 提示）。
* 生成状态的服务器与验证它的服务器之间的时钟偏差。

### 如何修复 [#如何修复-2]

* 确保**所有服务器实例都启用了 NTP**，以便时钟保持同步。
* 如果您的用户经常需要超过 10 分钟（例如具有审批工作流的企业 SSO），当前此超时不可配置 - 考虑提出功能请求。

***

## `state_invalid` - 无法解密或解析（cookie 策略） [#state_invalid---无法解密或解析cookie-策略]

加密的状态 cookie 存在但无法解密，或者解密的 JSON 无法解析。

### 常见原因 [#常见原因-3]

* `BETTER_AUTH_SECRET` 在流程开始和回调之间进行了轮换，因此解密密钥不再匹配。
* cookie 值在传输过程中损坏（代理重写、URL 编码问题）。

### 如何修复 [#如何修复-3]

* 避免在活动的用户流程期间轮换密钥。在低流量时段部署密钥更改。
* 检查代理和中间件是否未修改 cookie 值。

***

## `state_security_mismatch` - 状态未正确持久化（数据库策略） [#state_security_mismatch---状态未正确持久化数据库策略]

在数据库中找到验证记录后，Better Auth 还会检查是否发送了**签名的状态 cookie** 及其值与回调 URL 中的状态匹配。这是第二层 CSRF 保护。此错误意味着 cookie 缺失或其值不匹配。

### 常见原因 [#常见原因-4]

* 签名 cookie 已过期（其 `maxAge` 为 5 分钟，比数据库记录的 10 分钟有效期短）。
* 第三方 cookie 限制或 `SameSite` 策略阻止了 cookie 发送。
* 跨源 POST 回调（SAML IdP 中很常见）不会发送 `SameSite=Lax` cookie。
* 预览域名与生产域名不匹配。
* 用户打开了多个登录标签页，每个标签页都会覆盖 state cookie，因此只有最后打开的标签页有效。
* **OAuth Proxy 密钥不匹配：** 使用 [OAuth Proxy 插件](/docs/plugins/oauth-proxy)进行开发或预览环境登录时，如果生产回调服务器与开发或预览部署使用不同的 `BETTER_AUTH_SECRET`，就必须配置共享的代理 `secret`。否则代理的 before 钩子无法解密 state 载荷，常规回调处理器随之运行并失败。仅向你信任其能在所有参与部署（包括生产环境）中验证用户的环境和代码共享代理密钥。

### 如何修复 [#如何修复-4]

* 使用**稳定的自定义域**并验证 cookie 属性。
* 对于 SAML 流程，Better Auth 已在内部设置了 `skipStateCookieCheck`。
* 如果您的部署需要，您可以跳过此检查：

```ts title="auth.ts"
export const auth = betterAuth({
	account: {
		skipStateCookieCheck: true,
	},
});
```

<Callout type="warn">
  跳过状态 cookie 检查会移除一层 CSRF 保护。仅当您理解安全影响并已实施其他缓解措施（例如，您的基础设施保证同源回调）时才启用此选项。
</Callout>

***

## `state_generation_error` - 无法创建验证记录 [#state_generation_error---无法创建验证记录]

此错误在 OAuth 流程的**开始**时（而非回调期间）抛出。这意味着无法将验证记录写入数据库。

### 常见原因 [#常见原因-5]

* `verification` 表不存在 - 未运行迁移。
* 数据库连接失败或超时。
* 数据库钩子或插件拒绝了写入。

### 如何修复 [#如何修复-5]

* 运行 `npx auth migrate`，确保所有表都已存在。
* 检查您的数据库连接和凭据。
* 查看 `verification` 模型上任何可能阻止写入的 `databaseHooks`。

***

## 常见原因和修复 [#常见原因和修复]

下表按生产环境中遇到的频率对每个根本原因进行排名，说明其触发的错误代码以及应对措施。

| 可能性     | 根本原因                                 | 错误代码                                                    | 修复措施                                                                 |
| ------- | ------------------------------------ | ------------------------------------------------------- | -------------------------------------------------------------------- |
| **非常高** | Cookie 被阻止或缺失（Safari ITP、跨域、预览域名）    | `state_security_mismatch` 或 `state_mismatch`（cookie 策略） | 使用稳定的自定义域名；检查 `SameSite` / `Secure` 属性                               |
| **非常高** | OAuth Proxy：生产环境与预览环境使用了不同密钥         | `state_security_mismatch` 或 `state_mismatch`            | 仅在[可信的参与部署](/docs/plugins/oauth-proxy#installation)之间配置共享代理 `secret` |
| **高**   | 回调 URL 被重放（刷新、后退按钮、重定向循环）            | `state_mismatch`（数据库）                                   | 确保错误页或重定向页不会再次触发回调                                                   |
| **高**   | 用户在提供商页面停留过久（超过 10 分钟）               | `state_mismatch`（数据库）或请求已过期                             | 提示用户重试；可考虑切换到 cookie 策略                                              |
| **高**   | 多个标签页或并发登录尝试                         | `state_security_mismatch`                               | 只有最后打开的标签页的 cookie 有效；更早打开的标签页会失败                                    |
| **中**   | 二级存储（Redis）键被逐出且没有数据库回退              | `state_mismatch`（数据库）                                   | 设置 `verification.storeInDatabase: true` 或确保 Redis 持久化                |
| **中**   | 无服务器或多实例部署没有共享数据库                    | `state_mismatch`（数据库）                                   | 使用共享数据库或所有实例都可访问的外部存储                                                |
| **中**   | 签名 cookie 已过期（5 分钟），而数据库记录仍有效（10 分钟） | `state_security_mismatch`                               | Cookie 的 TTL 比数据库记录短；流程持续 5–10 分钟的用户会遇到此问题                           |
| **低**   | 流程期间 cookie 签名密钥或加密密钥发生变化            | `state_invalid`（cookie）或 `state_security_mismatch`（数据库） | 在进行中的流程期间保持 cookie 密钥一致；重新启动中断的登录流程                                  |
| **低**   | OAuth 提供商更改了 state 参数                | `state_mismatch`（数据库）                                   | 确认提供商会原样保留 `state`；检查 URL 编码问题                                       |
| **低**   | 缺少验证表或尚未运行迁移                         | `state_generation_error`                                | 运行 `npx auth migrate`                                                |
| **极低**  | 服务器实例之间存在时钟偏差                        | 请求已过期                                                   | 在所有服务器上启用 NTP                                                        |

***

## 调试清单 [#调试清单]

1. **检查错误代码。** 错误页面上的 `error` 查询参数会告诉你确切的代码（`state_mismatch`、`state_security_mismatch`、`state_invalid` 或 `state_generation_error`）。
2. **打开 DevTools → Application → Cookies。** 确认在重定向前已设置 state cookie（`better-auth.state` 或 `better-auth.oauth_state`），且回调到达时仍然存在。
3. **检查回调 URL。** 确认 `state` 查询参数存在且未被修改。
4. **检查服务器日志。** 错误中的 `details` 对象包含 `state` 值。对于数据库存储的 state，在应用 [`verification.storeIdentifier`](/docs/reference/options#verification) 之前，验证标识符为 `auth-state:${state}`。在配置的验证存储中查找最终标识符，确认记录是否存在、是否已过期或已被删除。内置的 `"hashed"` 策略会将完整前缀标识符的 SHA-256 摘要以不带填充的 base64url 格式存储。
5. **检查部署。** 确保所有实例共享相同的数据库和密钥，并且域名和 cookie 配置一致。

