---
title: Layer 2 — 身份准入规则
description: 配置应用允许哪些身份完成认证 —— 通过邮箱（支持 `*` 通配）、Steam ID、账户别名或扇区主体。
editUrl: true
head: []
template: doc
sidebar:
  order: 3
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

import { CardGrid, LinkButton, LinkCard } from "@astrojs/starlight/components";

:::tip[三层规则模型的一部分]
身份准入规则（Layer 2）是三层规则之一。概述介绍了允许列表、默认拒绝、评估顺序，以及三层如何组合。

<LinkButton href="/zh-cn/application-rules/overview/" variant="secondary" icon="left-arrow">阅读概述</LinkButton>
:::

身份准入规则控制**应用允许哪些身份完成登录**。它在用户成功证明身份之后、认证请求被标记为 realized 之前运行。

将它作为独立的一层，是因为“确认用户拥有 `alice@example.com`”与“决定应用是否接受 `alice@example.com`”是两个不同的问题。前者由认证规则处理，后者由身份准入规则处理。

## 支持的 constraint type

身份准入规则支持五种 constraint type，同一应用可以混合使用。层内规则按 OR 组合，任意一条匹配即可放行。

| Type | Payload | 匹配语义 |
|---|---|---|
| `EMAIL` | `{ "allowedEmails": string[] }` | 大小写不敏感的 glob，仅 `*` 特殊 |
| `STEAM_ID` | `{ "allowedSteamIds": (string \| "*")[] }` | 十进制 SteamID64 字符串精确匹配，或字面量 `"*"` 通配 |
| `ACCOUNT_ALIAS` | `{ "allowedAccountAliases": string[] }` | 对 realize 时账户的**账户别名**（用户可见、应用不可见、可轮换的句柄）做精确匹配 —— **没有通配** |
| `SECTOR_SUBJECT` | `{ "allowedSectorSubjects": string[] }` | 对 realize 时账户在该应用所属扇区（Sector）下的**扇区主体（sector subject）**（应用可见的令牌 `sub`）做精确匹配 —— **没有通配** |
| `EVERYONE` | `{}` | 无条件放行：匹配每个已通过认证的账户，包括没有任何已验证邮箱的账户。`payload` 为空。 |

匹配对象都来自当前正在进行准入判定的账户或会话：

- `EMAIL` 对应**账户持有的所有已验证邮箱**，即账户的全部 `EmailIdentity` 记录，而不只是本次登录使用的邮箱。只要其中**任意一个**匹配允许列表即可通过。使用邮箱 OTP 注册时账户尚不存在，此时匹配正在注册的邮箱。
- `STEAM_ID` 对应**两条 Steam 路径**验证后得到的 SteamID64 —— 既包括游戏端内通过 Steamworks 的 `STEAM_TICKET` direct-issue，也包括浏览器端的 `STEAM_OPENID` "Sign in with Steam"。两条路径解析到同一份 Steam 身份。从未关联邮箱的 Steam-first 账户必须依赖 `STEAM_ID`（或 `ACCOUNT_ALIAS` / `SECTOR_SUBJECT`）规则才能通过；只有 `EMAIL` 规则的 Layer 2 会拒绝它们。
- `ACCOUNT_ALIAS` 对应账户的**账户别名** —— 用户在 With 门户中看到并自行管理的、可轮换的句柄（例如 `quiet-meadow-7h2k-9m4p-3fnp-falcon`）。它从不向应用披露，因此适合用来锁定某个具体的人，无论其通过哪个应用 realize。
- `SECTOR_SUBJECT` 对应账户的**扇区主体**，即按（账户 × 扇区）生成的不透明标识符，也是应用在令牌中实际收到的 `sub`（例如 `sub_9SQ5535CRWNDDM2T`）。共享同一扇区的应用会为同一用户看到相同值，不同扇区则会看到不同值。它可以用于将应用已经收到的特定用户标识加入允许列表。

`ACCOUNT_ALIAS` 与 `SECTOR_SUBJECT` 都是不透明值，只进行**精确匹配**，不检查格式，也不支持通配。两者都要在账户及其扇区主体存在后才会生成，因此新注册产生的值不可能预先出现在允许列表中。Layer 2 如果只包含这两类规则，就**不会接受新注册**，只能允许已有账户。两个值都可以轮换；轮换后，引用旧值的允许列表将不再匹配。

## EMAIL 通配语义

`allowedEmails` 中的每一项以**大小写不敏感**的方式匹配；**只有 `*` 有特殊含义**（匹配零个或多个字符）。其它字符（包括 `@`、`.`、`+`）都是字面量。

| Pattern | 匹配 |
|---|---|
| `alice@example.com` | 精确匹配 |
| `*@example.com` | `example.com` 域下的任意邮箱 |
| `alice+*@example.com` | `alice@example.com` 的任意 plus 变体 |
| `*` | 任意邮箱 |

比较前会对输入做 trim 和 lowercase。

## STEAM_ID 语义

每一项要么是字面量 `"*"`（匹配任意已验证的 SteamID64），要么是十进制 SteamID64 字符串（大小写敏感精确匹配）。写入时强制 `[0-9]{1,20}` 的形状。

```json
{
  "constraintType": "STEAM_ID",
  "payload": {
    "allowedSteamIds": ["76561198000000000", "76561198000000001"]
  }
}
```

## ACCOUNT_ALIAS 语义

每一项是目标账户**账户别名**的精确字面量 —— 即可轮换、面向用户的句柄（例如 `quiet-meadow-7h2k-9m4p-3fnp-falcon`）。没有通配，也没有 glob；该值不透明，按字面比对。

```json
{
  "constraintType": "ACCOUNT_ALIAS",
  "payload": {
    "allowedAccountAliases": [
      "quiet-meadow-7h2k-9m4p-3fnp-falcon",
      "bold-harbor-2x4q-8m1p-5kna-otter"
    ]
  }
}
```

账户别名是用户在 With 门户中管理的句柄，不会向应用披露。它适合用于允许某位特定用户，而不依赖该用户通过哪个应用登录。用户轮换别名后，引用旧值的允许列表将不再匹配。

## SECTOR_SUBJECT 语义

每一项是目标账户在该应用所属扇区下**扇区主体**的精确字面量 —— 即应用可见的令牌 `sub`（例如 `sub_9SQ5535CRWNDDM2T`）。没有通配，也没有 glob；该值不透明，按字面比对。

```json
{
  "constraintType": "SECTOR_SUBJECT",
  "payload": {
    "allowedSectorSubjects": [
      "sub_9SQ5535CRWNDDM2T",
      "sub_4K2P8M1N7QRWXY3Z"
    ]
  }
}
```

扇区主体是应用及同一扇区内其他应用实际收到的用户标识符；不同扇区会为同一用户看到不同值。你可以将应用已经用于标识用户的值加入允许列表。用户轮换扇区主体后，引用旧值的规则将不再匹配。

两者都是不支持通配的精确允许列表。如果流程还需要接受新用户注册，请同时配置 `EMAIL` 或 `STEAM_ID` 规则。

## EVERYONE 语义

`EVERYONE` 是显式的"本应用对任何人开放"规则。它携带空 payload，**无条件**匹配每一个已通过鉴权的账户。

```json
{
  "constraintType": "EVERYONE",
  "payload": {}
}
```

这不会削弱“允许列表 + 默认拒绝”模型，因为 `EVERYONE` 本身就是一项显式配置。没有 Layer 2 规则的应用仍会拒绝所有人；`EVERYONE` 表示任何通过 Layer 1 的账户都可以完成准入。它是**唯一**能够允许**没有已验证邮箱**账户的 Layer 2 类型，例如仅通过 Steam 或 Battle.net 登录的用户，因此适合不按身份设限的公开应用。由于层内采用 OR 语义，它与其他规则并列时会匹配所有账户，请谨慎使用。

## 应用规则结构

每条 Layer 2 规则的外层结构相同，不同类型只有 `constraintType` 和 `payload` 的内容不同。

```json
{
  "constraintType": "EMAIL",
  "payload": { "allowedEmails": ["*@example.com"] },
  "accessTokenTtlSeconds": null,
  "refreshTokenTtlSeconds": null
}
```

应用上的多条规则之间是 OR：身份匹配任一条规则即可通过。

## 在 `/establish` 上收紧

`/establish` 中的 `realizeConstraints` 使用相同结构，用于限制单次认证请求允许的身份：

```json
{
  "applicationAnchor": "my-app",
  "realizeConstraints": [
    {
      "constraintType": "EMAIL",
      "payload": { "allowedEmails": ["admin@example.com"] }
    }
  ]
}
```

- 字段缺失 → 不收紧；只看应用规则。
- 字段存在且为空数组 → 拒绝。
- 字段存在且非空 → 与应用规则做 AND。约束内部的 payload 列表本身也必须非空。

## 示例

应用配置了一条 `EMAIL` 身份准入规则，内容为 `allowedEmails: ["*@example.com"]`。管理员入口在一次认证请求中传入 `realizeConstraints: [{ constraintType: "EMAIL", payload: { allowedEmails: ["admin@example.com"] } }]`。

| 用户认证身份 | 应用允许？ | Inquiry 允许？ | 结果 |
|---|---|---|---|
| `admin@example.com` | 是（命中 `*@example.com`） | 是 | 通过 |
| `alice@example.com` | 是（命中 `*@example.com`） | 否 | 认证后被拒绝 |
| `attacker@other.com` | 否 | 不适用 | 认证后被拒绝 |

身份准入规则在认证规则**之后**执行。此时用户已经证明自己拥有该邮箱；身份准入规则只决定应用是否接受这个身份完成当前登录。

## 相关链接

<CardGrid>
<LinkCard
    title="三层规则模型"
    description="整体模型：允许列表、默认拒绝、评估顺序，以及三层如何组合。"
    href="/zh-cn/application-rules/overview/"
/>
<LinkCard
    title="Layer 1 —— 认证规则"
    description="向用户提供哪些认证方式。"
    href="/zh-cn/application-rules/authentication-rules/"
/>
<LinkCard
    title="Layer 3 —— 返回规则"
    description="把 realize 后的会话如何回传给应用。"
    href="/zh-cn/application-rules/return-rules/"
/>
</CardGrid>