---
url: /zh/concepts/identity.md
description: 聚合怎样寻址、归谁所有、调用方是谁、访问怎样判定，以及部署的信任边界在哪里。
---

# 身份、租户与访问

每条命令、每次读都要回答同样的问题：哪个聚合、哪个租户、归谁所有、谁在问、允不允许？Wow 对每个问题只在一处回答一次，所以答案不会互相矛盾。本页说明每个答案从哪里来。

## 聚合由一个值寻址 {#an-aggregate-is-addressed-by-one-value}

聚合的身份是一个值，即它的**引用**：上下文、聚合类型、租户和 ID，写作 `context.aggregate/tenant/id`。引用只在准入时决定一次，此后没有任何东西能改变它。

* **租户是身份的一部分。** 两个租户可以使用相同的 ID 而互不影响：引用不同，哈希到的分区也不同，而且每个唯一键（版本、请求 ID 和预留键）都包含租户。
* **ID** 来自路由或消息，新聚合的 ID 在准入时生成。
* **命令体不包含身份。** 它只承载业务数据。需要所在聚合引用的命令函数，从准入后的命令里读取。

## 聚合归谁所有 {#who-an-aggregate-belongs-to}

聚合总是属于一个租户，还可以属于一个**所有者**和一个**空间**；两者合起来是它的**作用域**。

* 新聚合从创建它的命令那里得到作用域。
* 此后，只有转移所有者或空间的生命周期命令能改变它。
* 每次提交都记录提交后聚合的作用域，快照也带着它。投影、查询策略或快照订阅不用重放历史就能读到它。

聚合类型声明它的**归属**：

| 归属 | 属于 | 适用于 |
| --- | --- | --- |
| 所有者（默认） | 它的租户，以及创建它的命令的所有者和空间 | 属于某个人或某个账户的数据：订单、购物车、保存的视图 |
| 租户 | 它的租户，以及它所在的空间（如果有） | 租户的每个调用方都要读的数据：商品、价目表 |

## 调用方是谁 {#who-the-caller-is}

对每个请求，宿主的调用方解析器把已认证的主体变成一个**调用方**：

* 它的**租户**；没有租户的主体属于默认租户；
* 它的**作用域**：它所代表的所有者和空间；
* 它的**主体标识（subject）**，即主体的 ID；匿名调用方没有；
* 它能否在**另一个租户或作用域**中行事，若能，就在 `Wow-Scope` 请求头里指明；
* 其他**属性**，例如用于访问控制的角色或属性；
* 它是不是系统里的一个**服务**，而不是用户。

调用方只存活于请求期间。提交保留下来的是**操作人**：主体标识、操作人自己的租户，以及它是否在另一个租户或作用域中行事。角色、属性和个人信息从不进入日志。

调用方分两级。**所有者级**调用方的作用域有所有者，只读写自己的聚合。**租户级**调用方的作用域没有所有者，读写它租户的所有聚合，例如负责发货的员工。各自能执行哪些动作，由下文的授权决定。

## 身份来自主体 {#identity-comes-from-the-principal}

租户、所有者和空间来自已认证的调用方，从不来自请求的路径或请求体。路由不随租户变化，客户端也不能把另一个租户写进 URL 来冒充它。在别处行事的唯一办法，是成为被允许这样做的调用方，并在 `Wow-Scope` 里说明。

Saga 发出的命令、补偿和定时命令不是由人发出的。它们**继承**根命令的作用域和操作人，所以 Saga 创建的聚合属于发出第一条命令的人，日志也写明是以谁的名义做的。每次提交的 `causation` 说明是什么触发了它。根命令的权限在准入时已经检查过；内核不会拿继承来的作用域去对照已存在的聚合再检查一次。

## 请求事实 {#request-facts}

准入把关于请求的几项事实复制进命令，提交时记录下来：

* 请求来自的**应用** `app_id`：只有当调用方解析器认证了它时才记录，例如 CoSec 用那个应用的角色授权了这个请求。客户端自己设置的请求头永远不被信任；
* 在公开端口上，客户端的**远程地址**和 **User-Agent**；
* 宿主列出的其他请求头。

Saga 发出的命令只继承那些本该随行的事实，例如应用；第一个请求的远程地址和 User-Agent 不继承。

## 访问 {#access}

访问在准入时判定，依据的是命令和查询共用的策略：

* **路由级授权**，例如公开路由前的 CoSec，决定调用方究竟能不能调用某个路由。
* **动作级授权**决定调用方能否对这个聚合执行这条命令。用 CoSec 时，它可以以调用方的身份为每条命令授权。
* **作用域。** 内核拒绝作用于调用方作用域之外的聚合的命令；查询准入把调用方的租户、所有者和空间加进每个查询（[读模型](./read-models#who-sees-what)）。
* **基于属性的访问**，默认关闭，用调用方的属性匹配每个聚合的标签。聚合的标签由生命周期命令打上，或从它的状态推出。

内核本身不做授权：命令到达内核时，准入已经做出了判定。

## 内部端口 {#the-internal-port}

每个节点监听两个端口。**公开端口**提供 HTTP API。**内部端口**承载：

* 一个服务的节点之间的集群通道，用于转发命令；
* 完成等待的信号，可能来自系统里的任何服务；
* 来自其他服务的同步调用，以 `Wow-Call` 请求头标记；
* 来自其他服务的读：与公开端口相同的查询和状态路由，供以自己的服务身份读本服务的服务使用。

信号完成一次等待并把结果交给调用方，所以谁能发信号，谁就能伪造任何命令的结果。因此内部端口绝不暴露到集群之外，也没有公开网关转发到它。在集群内部，发往它的每个请求都要认证：

| 请求 | 接受来自 |
| --- | --- |
| 转发的命令 | 同一服务的节点，带它的集群令牌 |
| 等待信号 | 同一服务的节点，带它的集群令牌；或另一个服务，带它的服务身份 |
| 同步调用或读 | 另一个服务，带它的服务身份；集群令牌不能发起调用 |

除非服务配置为允许更多，同步调用只能调用领域命令。生命周期命令和系统命令（例如删除、重建快照）必须为调用方服务显式列出。一次调用带着它所代表的根命令的作用域、操作人和请求事实，目标只在调用来自服务身份时才信任这些声明。
