接入 BOSS
初始化 Control 和测试台后,登记 BOSS 的证书、内部产品和登录客户端,开通平台租户与员工,填好部署配置,再接入登录、账户设置和事件接收。
| 环境 | 登录地址 | 带证书调用的接口地址 |
|---|---|---|
local | https://id.local.globalbes.sg | https://id.local.globalbes.sg:19443 |
test | https://id.test.globalbes.sg | https://id.test.globalbes.sg:9443 |
staging | 待定 | 待定 |
prod | 待定 | 待定 |
Sign in to send requests to see which of these steps are done. Sign in
确认环境就绪
在哪里做:测试台
确认 Control 和测试台初始化完成,找到 active 的平台租户,为登记 BOSS 和开通员工准备环境。
- Control 按
bes-control-service/docs/runbooks/operations.md第 1 节做完迁移、密钥、同步目录和首位平台管理员初始化(bes-control seed-person)。 - 首位管理员初始化同时建好平台租户,code 为
BES_CONTROL_PLATFORM_TENANT_CODE,状态为active。 - 测试台按
bes-api-console/docs/setup.md登记,并能用平台管理员登录。 - 在
tenant.list返回的entries中确认平台租户的 code 和active状态;有next_cursor时填入cursor继续查。
本环境(test)
- Control 与测试台的部署配置位置:待定。
- 平台租户代码取本环境的
BES_CONTROL_PLATFORM_TENANT_CODE。
其他环境
local
bes-infra 仓的 ./envs/local/dev stack up 与 ./envs/local/dev stack seed-demo 已做完初始化,平台租户代码为 bes。
staging
- Control 与测试台的部署配置位置:待定。
- 平台租户代码取本环境的
BES_CONTROL_PLATFORM_TENANT_CODE。
prod
- Control 与测试台的部署配置位置:待定。
- 平台租户代码取本环境的
BES_CONTROL_PLATFORM_TENANT_CODE。
做完这一部分,手里有:可用的 Control 和测试台、active 的平台租户;测试台可用平台管理员登录。
生成私钥和证书请求
在哪里做:BOSS 服务器
跑一条 openssl 命令,得到私钥 boss-service-key.pem 和证书请求 boss-service.csr。证书请求拿去签发;私钥留在 BOSS 服务器上,不发给任何人。
SERVICE_ACCOUNT_ID每次新生成,写进证书唯一的 URI SANurn:bes:service:<uuid>;登记平台账号后它就是账号的service_account_id。- 私钥
boss-service-key.pem放进 BOSS 部署的密钥存储。
本环境(test)
在 BOSS 服务器上执行:
CERT_DIR="$HOME/bes-certs/boss" mkdir -p "$CERT_DIR" SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z) openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \ -subj "/CN=BOSS" \ -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature" \ -addext "extendedKeyUsage=clientAuth" \ -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"
其他环境
local
bes-infra 仓的 ./envs/local/dev stack up 已做完这一步:私钥在 ~/.local/share/bes/local/stack/secrets/control/boss-service-key.pem,本地后面的步骤用它。要另做一套,在本机执行:
CERT_DIR="$HOME/bes-certs/boss" mkdir -p "$CERT_DIR" SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z) openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \ -subj "/CN=BOSS" \ -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature" \ -addext "extendedKeyUsage=clientAuth" \ -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"
staging
在 BOSS 服务器上执行:
CERT_DIR="$HOME/bes-certs/boss" mkdir -p "$CERT_DIR" SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z) openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \ -subj "/CN=BOSS" \ -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature" \ -addext "extendedKeyUsage=clientAuth" \ -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"
prod
在 BOSS 服务器上执行:
CERT_DIR="$HOME/bes-certs/boss" mkdir -p "$CERT_DIR" SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z) openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \ -subj "/CN=BOSS" \ -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature" \ -addext "extendedKeyUsage=clientAuth" \ -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"
签发证书
在哪里做:客户端 CA 所在的机器
用客户端 CA 给证书请求签名,得到证书 boss-service.pem。证书拷回 BOSS 服务器,和私钥一起调 Control;登记平台账号时也要用它。
前提:OpenSSL 3.0 及以上,openssl version 显示 OpenSSL 3.x。Mac 自带的 /usr/bin/openssl 是 LibreSSL,不认 -copy_extensions:先执行 brew install openssl@3,再执行 export PATH="$(brew --prefix openssl@3)/bin:$PATH"。
本环境(test)
CA_DIR:待定,test 环境的客户端 CA 位置未定。- 签发前把
boss-service.csr拷到客户端 CA 所在机器的~/bes-certs/boss/;签发后把boss-service.pem拷回 BOSS 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/boss" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/boss-service.pem"
其他环境
local
bes-infra 仓的 ./envs/local/dev stack up 已做完这一步:证书在 ~/.local/share/bes/local/stack/secrets/control/boss-service.pem,客户端 CA(client-ca.pem、client-ca-key.pem)在同一目录,本地后面的步骤用这张证书。给上一步另做的证书请求签发,在本机执行(证书请求不用拷):
CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
CA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \
-CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
-CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \
-out "$CERT_DIR/boss-service.pem"
staging
CA_DIR:待定,staging 环境的客户端 CA 位置未定。- 签发前把
boss-service.csr拷到客户端 CA 所在机器的~/bes-certs/boss/;签发后把boss-service.pem拷回 BOSS 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/boss" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/boss-service.pem"
prod
CA_DIR:待定,prod 环境的客户端 CA 位置未定。- 签发前把
boss-service.csr拷到客户端 CA 所在机器的~/bes-certs/boss/;签发后把boss-service.pem拷回 BOSS 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/boss" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/boss-service.pem"
登记平台账号
在哪里做:Control 服务器
执行 bes-control seed-trusted-caller,得到 BOSS 的平台账号,状态 active。之后 BOSS 带这张证书调管理接口,Control 按这个账号放行。
- 账号带 Control 的
platform-administrator角色(role),在service-account.list里product_id为null,能管所有产品(product)。 --display-name写BOSS:测试台按这个名字找 BOSS 的平台账号。- 重复执行保留同一个账号。
本环境(test)
- 在 Control 服务器上执行
bes-control的方式:待定,test 环境的 Control 部署方式未定。 boss-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --certificate "待定/boss-service.pem" --display-name BOSS --role platform-administrator
其他环境
local
bes-infra 仓的 ./envs/local/dev stack seed-demo 已做完这一步:执行的就是下面这条命令,登记的证书是 ~/.local/share/bes/local/stack/secrets/control/boss-service.pem。它还做完了:创建产品 boss、登记并启用 boss-web(不保存 client_secret,要用时调 client.rotate-credential 换一个)、创建并启用平台租户 bes 和开通记录、创建员工 yang.staff(Control 的 platform-administrator 角色)并发首次登录链接。
BES_INFRA="$HOME/Projects/bes-infra" # bes-infra 仓的路径 "$BES_INFRA/envs/local/dev" stack compose exec -T control-api /bin/sh /opt/bes/stack-entrypoint.sh \ bes-control seed-trusted-caller --certificate /run/bes/control/boss-service.pem --display-name BOSS --role platform-administrator
- 本地 Control 在容器
control-api里;~/.local/share/bes/local/stack/secrets/control/下的文件,每次执行时复制成容器里的/run/bes/control/<文件名>。
staging
- 在 Control 服务器上执行
bes-control的方式:待定,staging 环境的 Control 部署方式未定。 boss-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --certificate "待定/boss-service.pem" --display-name BOSS --role platform-administrator
prod
- 在 Control 服务器上执行
bes-control的方式:待定,prod 环境的 Control 部署方式未定。 boss-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --certificate "待定/boss-service.pem" --display-name BOSS --role platform-administrator --confirm-production
用证书调第一个接口
在哪里做:BOSS 代码
用证书和私钥调 tenant.list,得到租户列表。证明证书、地址和平台账号都对上,之后的管理接口照这样调。
程序里发这个请求(id 用前面步骤返回的):
带:证书
请求:
GET https://id.test.globalbes.sg:9443/internal/control/v1/tenants?limit=100
返回:
HTTP/1.1 200 OK
{
"entries": [
{
"tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
"code": "bes",
"display_name": "BES",
"status": "active",
"created_at": "2026-01-01T08:00:00+00:00",
"mfa_required": false
}
],
"next_cursor": null
}
程序发的请求和下面各环境的 curl 一样,可以先用它试。
- 返回
200和租户列表:证书、地址和平台账号都对上。 - 地址:带证书的接口发到
https://id.test.globalbes.sg:9443,只带用户令牌的接口发到https://id.test.globalbes.sg。 - 证书:TLS 客户端证书
boss-service.pem、私钥boss-service-key.pem,用服务端 CA 验证 Control 的证书。 - 写操作:请求体带
request_ref,同一个request_ref重发返回第一次的结果。 X-BES-Operator:可选,写代表哪个人操作的person_id。- 出错时返回
{"error": {"code": …, "reference": …, "message": …}},程序按code分支。
本环境(test)
SERVER_CA:待定,test 环境验证 Control 服务端证书的 CA 文件未定。
在 BOSS 服务器上执行:
CERT_DIR="$HOME/bes-certs/boss" SERVER_CA="待定" curl -i --cert "$CERT_DIR/boss-service.pem" --key "$CERT_DIR/boss-service-key.pem" \ --cacert "$SERVER_CA" "https://id.test.globalbes.sg:9443/internal/control/v1/tenants?limit=100"
其他环境
local
用 ./envs/local/dev stack up 签发、./envs/local/dev stack seed-demo 登记的证书,在本机执行:
CA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control"
SERVER_CA="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/stack/server-ca.pem"
curl -i --cert "$CA_DIR/boss-service.pem" --key "$CA_DIR/boss-service-key.pem" \
--cacert "$SERVER_CA" "https://id.local.globalbes.sg:19443/internal/control/v1/tenants?limit=100"
staging
SERVER_CA:待定,staging 环境验证 Control 服务端证书的 CA 文件未定。
在 BOSS 服务器上执行:
CERT_DIR="$HOME/bes-certs/boss" SERVER_CA="待定" curl -i --cert "$CERT_DIR/boss-service.pem" --key "$CERT_DIR/boss-service-key.pem" \ --cacert "$SERVER_CA" "待定/internal/control/v1/tenants?limit=100"
prod
SERVER_CA:待定,prod 环境验证 Control 服务端证书的 CA 文件未定。
在 BOSS 服务器上执行:
CERT_DIR="$HOME/bes-certs/boss" SERVER_CA="待定" curl -i --cert "$CERT_DIR/boss-service.pem" --key "$CERT_DIR/boss-service-key.pem" \ --cacert "$SERVER_CA" "待定/internal/control/v1/tenants?limit=100"
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 401 | CALLER_AUTHENTICATION_FAILED |
证书不是登记的那张,或账号没启用。 |
| 403 | ACCESS_DENIED |
账号的 Control 角色不含这个操作。 |
做完这一部分,手里有:BOSS 的证书、私钥和 active 平台账号;用证书调通 tenant.list。
创建产品 boss
在哪里做:测试台
创建产品 boss,得到它的 product_id。登记登录客户端、功能、角色和开通记录时都要填这个 product_id。
在测试台点本步的 Product Create 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"request_ref": "create-boss",
"code": "boss",
"display_name": "BOSS",
"scope_level": "tenant",
"sellable": false,
"status": "active"
}
返回:
{
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"code": "boss",
"scope_level": "tenant",
"sellable": false,
"display_name": "BOSS",
"status": "active",
"version": 0
}
- 产品
boss按租户(tenant)开通,scope_level写tenant。 - BOSS 是内部系统,不对客户售卖。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 409 | PRODUCT_CODE_CONFLICT |
boss 已登记:用 product.list 查出 product_id。 |
登记登录客户端
在哪里做:测试台
登记员工登录用的客户端 boss-web,得到 client_secret。BOSS 用 code 换令牌时带 client_id 和 client_secret。
在测试台点本步的 Client Register 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"request_ref": "register-boss-web",
"client_id": "boss-web",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"deployment_version": "1",
"display_name": "BOSS",
"kind": "confidential_web",
"auth_method": "client_secret_basic",
"redirect_uris": [
"https://boss.example.com/callback"
],
"post_logout_redirect_uris": [
"https://boss.example.com/"
],
"scopes": [
"openid",
"profile",
"email"
],
"token_profile": "web",
"dpop_required": false
}
返回:
{
"client_id": "boss-web",
"display_name": "BOSS",
"kind": "confidential_web",
"auth_method": "client_secret_basic",
"redirect_uris": [
"https://boss.example.com/callback"
],
"post_logout_redirect_uris": [
"https://boss.example.com/"
],
"backchannel_logout_uri": null,
"scopes": [
"openid",
"profile",
"email"
],
"dpop_required": false,
"token_profile": "web",
"service_account_id": null,
"status": "pending",
"version": 0,
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"public_key_jwk": null,
"deployment_version": "1",
"client_secret": "<client_secret>"
}
product_id写boss产品的:登录后可选的租户和权限按这个产品的开通记录(entitlement)和角色算。redirect_uris写 BOSS 实际的回调地址,逐字符一致。client_secret只在这次返回里出现,放进 BOSS 部署的密钥存储;丢了就调client.rotate-credential换一个。- 退出后回到 BOSS 的地址在这里登记,
end-session用它。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 409 | CLIENT_ID_CONFLICT |
boss-web 已登记:用 client.get 查看,跳过这一步。 |
启用登录客户端
在哪里做:测试台
把 boss-web 改成 active,得到可用的登录客户端。员工从此能通过它登录 BOSS。
在测试台点本步的 Client Set Status 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"client_id": "boss-web",
"request_ref": "activate-boss-web",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"status": "active"
}
返回:
{
"client_id": "boss-web",
"display_name": "BOSS",
"kind": "confidential_web",
"auth_method": "client_secret_basic",
"redirect_uris": [
"https://boss.example.com/callback"
],
"post_logout_redirect_uris": [],
"backchannel_logout_uri": null,
"scopes": [
"openid",
"profile",
"email"
],
"dpop_required": false,
"token_profile": "web",
"service_account_id": null,
"status": "active",
"version": 1,
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"public_key_jwk": null,
"deployment_version": "1"
}
做完这一部分,手里有:内部产品 boss 的 product_id、启用的 boss-web 登录客户端及 client_secret;登录回调与退出地址已登记。
登记功能
在哪里做:测试台
把 BOSS 的全部路由登记成功能,得到功能清单。验证员工的请求时,Control 按它找到功能码。
PUTFeature Register在测试台点本步的 Feature Register 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"deployment_version": "1",
"request_ref": "register-boss-features-1",
"features": [
{
"code": "document.read",
"route_template": "/documents/{id}",
"method": "GET",
"risk": "low",
"display_name": "Read documents"
},
{
"code": "document.write",
"route_template": "/documents",
"method": "POST",
"risk": "low",
"display_name": "Write documents"
},
{
"code": "payment.approve",
"route_template": "/payments/{id}/approve",
"method": "POST",
"risk": "high",
"display_name": "Approve payments"
}
]
}
返回:
[
{
"feature_id": "3d8a1b6f-c247-42a5-99f4-16eaf5c38c7b",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"code": "document.read",
"route_template": "/documents/{id}",
"method": "GET",
"risk": "low",
"display_name": "Read documents",
"deployment_version": "1",
"status": "active"
},
{
"feature_id": "4e9b2c70-d358-43b6-8a05-27fb06d49d8c",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"code": "document.write",
"route_template": "/documents",
"method": "POST",
"risk": "low",
"display_name": "Write documents",
"deployment_version": "1",
"status": "active"
},
{
"feature_id": "5fac3d81-e469-44c7-9b16-380c17e5ae9d",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"code": "payment.approve",
"route_template": "/payments/{id}/approve",
"method": "POST",
"risk": "high",
"display_name": "Approve payments",
"deployment_version": "1",
"status": "active"
}
]
features写 BOSS 自己的全部路由;BOSS 的功能清单:待定。- 每次调用替换这个
deployment_version的整份清单,所以每次都发全部功能。 deployment_version是 BOSS 验证请求时发的版本。
定义角色
在哪里做:测试台
把几个功能码组成一个角色,得到 role_id。创建员工时填它,员工就有这些功能。
在测试台点本步的 Role Define 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"code": "document-controller",
"request_ref": "define-boss-document-controller",
"display_name": "Document controller",
"feature_codes": [
"document.read",
"document.write"
]
}
返回:
{
"role_id": "6abd4e92-f57a-45d8-8c27-491d28f6bfae",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"code": "document-controller",
"display_name": "Document controller",
"scope_level": "tenant",
"feature_codes": [
"document.read",
"document.write"
],
"status": "active",
"tenant_id": null,
"project_id": null,
"version": 0
}
- 角色按
code创建或整体替换;feature_codes只能写 BOSS 已登记且active的功能码。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 400 | ROLE_CONFIGURATION_INVALID |
feature_codes 有没登记的功能码,先登记功能。 |
做完这一部分,手里有:BOSS 的功能清单和角色的 role_id;创建员工时填这个 role_id。
查平台租户
在哪里做:测试台
用 tenant.list 找到平台租户代码对应的那一行,得到 tenant_id,创建开通记录和员工时填它。
在测试台点本步的 Tenant List 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"limit": 100
}
返回:
{
"entries": [
{
"tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
"code": "bes",
"display_name": "BES",
"status": "active",
"created_at": "2026-01-01T08:00:00+00:00",
"mfa_required": false
}
],
"next_cursor": null
}
- 用本环境的
BES_CONTROL_PLATFORM_TENANT_CODE在entries中找到平台租户,保存这一行的tenant_id;本地代码为bes。 - 有
next_cursor时填入cursor继续查,找到后确认状态为active。 - 创建开通记录和员工时填写这个
tenant_id。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 401 | CALLER_AUTHENTICATION_FAILED |
核对测试台登记的证书和账号状态。 |
| 403 | ACCESS_DENIED |
测试台账号使用 Control 的 platform-administrator 角色。 |
创建开通记录
在哪里做:测试台
给平台租户开通 BOSS,得到待启用的 entitlement_id。下一步启用时填它。
在测试台点本步的 Entitlement Create 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"request_ref": "create-bes-boss-entitlement",
"tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
"entitlement": {
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"starts_at": "2026-01-01T00:00:00Z",
"ends_at": "2036-01-01T00:00:00Z",
"source": "internal",
"source_ref": "seed-demo-boss",
"source_version": 1
}
}
返回:
{
"entitlement_id": "c3a7e1f9-5b26-4d48-9a0c-e8f2b4d6a135",
"tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"project_id": null,
"starts_at": "2026-01-01T00:00:00+00:00",
"ends_at": "2036-01-01T00:00:00+00:00",
"source": "internal",
"source_ref": "seed-demo-boss",
"source_version": 1,
"seat_limit": null,
"status": "pending",
"readiness": "pending",
"readiness_ref": null,
"revoked_at": null,
"version": 0
}
- 时段:同一租户、同一产品的时段不能重叠;
ends_at必填。 source、source_ref、source_version写这段时间出自哪份订单或合同。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 409 | ENTITLEMENT_PERIOD_OVERLAP |
已有重叠的开通记录:用 entitlement.list 查出并启用它。 |
启用开通记录
在哪里做:测试台
启用开通记录,得到生效的开通。员工登录 BOSS 后能选平台租户。
POSTEntitlement Activate在测试台点本步的 Entitlement Activate 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"entitlement_id": "c3a7e1f9-5b26-4d48-9a0c-e8f2b4d6a135",
"request_ref": "activate-bes-boss-entitlement",
"tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"runtime_ready": true
}
返回:
{
"entitlement_id": "c3a7e1f9-5b26-4d48-9a0c-e8f2b4d6a135",
"tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"project_id": null,
"starts_at": "2026-01-01T00:00:00+00:00",
"ends_at": "2036-01-01T00:00:00+00:00",
"source": "internal",
"source_ref": "seed-demo-boss",
"source_version": 1,
"seat_limit": null,
"status": "active",
"readiness": "ready",
"readiness_ref": "4bf92f3577b34da6a3ce929d0e0e4736",
"revoked_at": null,
"version": 1
}
runtime_ready: true表示产品的运行环境已就绪。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 409 | ENTITLEMENT_NOT_READY |
runtime_ready 写 true。 |
创建员工
在哪里做:测试台
在平台租户里创建员工,得到 person_id。发首次登录链接时填它。
在测试台点本步的 Person Create 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"request_ref": "create-staff-weiling-tan",
"tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
"display_name": "Tan Wei Ling",
"channels": [
{
"channel_type": "email",
"channel_identifier": "weiling.tan@bes.example.com"
}
],
"role_id": "6abd4e92-f57a-45d8-8c27-491d28f6bfae",
"person_type": "staff"
}
返回:
{
"person_id": "2c7e9b41-5d08-4a3f-b6e2-91f4a8c3d057",
"membership_id": "6e1a4c9d-3f57-4b28-8d06-a2c7e5b1f938",
"person_role_id": "f2c6ea04-8d57-4b93-ae2f-d5b9c3a7f480",
"channel_identity_ids": [
"7d3b9f15-2a68-4c04-b1e7-e8a2d6c4f591"
]
}
person_type写staff,tenant_id写平台租户的;员工只能建进平台租户,只有平台级服务账号能建。role_id写 BOSS 的角色。管平台的员工另外要 Control 的platform-administrator角色,在平台租户里用role-assignment.assign分配;这个角色对所有租户生效。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 409 | CHANNEL_IDENTITY_BOUND |
邮箱已属于某人:用 membership.create 把这个人加进平台租户。 |
发首次登录链接
在哪里做:测试台
给员工发首次登录链接,得到 action_id。员工用链接设好密码后登录 BOSS;用 action_id 查链接状态。
在测试台点本步的 Credential Issue 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。
填表的值:
{
"request_ref": "first-login-weiling-tan",
"person_id": "2c7e9b41-5d08-4a3f-b6e2-91f4a8c3d057",
"purpose": "initial_login",
"reason": "BOSS staff onboarding"
}
返回:
{
"action_id": "4d9f2b6e-8a13-4c75-b2e0-f6a1c9d3e587",
"purpose": "initial_login",
"expires_at": "2026-01-03T08:00:00+00:00",
"delivery": "queued"
}
- 返回只说明链接已排队发送;用
credential-action.get和返回的action_id查是pending、completed还是expired。 - 首次登录链接最长 48 小时有效。
- 员工首次登录要在手机验证器 App 上登记两步验证。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 409 | CREDENTIAL_CONFLICT |
此人已设过密码,不用再发。 |
做完这一部分,手里有:平台租户的 tenant_id 和启用的 BOSS 开通记录、员工的 person_id;员工收到首次登录链接并登记两步验证。
写入 BOSS 配置
在哪里做:BOSS 服务器
把地址、证书、产品和登录客户端等值写入 BOSS 部署配置,得到调用 Control 和员工登录所需的配置。
| 配置值 | 来源与填写内容 |
|---|---|
control_issuer |
本环境的登录地址,见下面环境配置 |
control_gateway |
本环境带证书调用的接口地址,见下面环境配置 |
| 服务端 CA 文件 | 本环境验证 Control 服务端证书的 CA 文件,first-call 的 SERVER_CA |
| 证书与私钥 | sign-certificate 的 boss-service.pem、generate-key 的 boss-service-key.pem |
product_id |
create-product 返回的 product_id |
client_id 与 client_secret |
register-client 返回的 client_id(boss-web)和 client_secret |
deployment_version |
register-features 登记的版本,示例为 1 |
平台租户 tenant_id |
find-platform-tenant 找到的 tenant_id |
client_secret与私钥放密钥存储,BOSS 部署配置引用它们。
本环境(test)
control_issuer:待定。control_gateway:待定。- 服务端 CA 文件:待定。
- 证书与私钥:
~/bes-certs/boss/boss-service.pem、~/bes-certs/boss/boss-service-key.pem。
其他环境
local
control_issuer:https://id.local.globalbes.sg。control_gateway:https://id.local.globalbes.sg:19443。- 服务端 CA 文件:
~/.local/share/bes/local/stack/secrets/stack/server-ca.pem。 - 证书与私钥:
~/.local/share/bes/local/stack/secrets/control/boss-service.pem、~/.local/share/bes/local/stack/secrets/control/boss-service-key.pem。
staging
control_issuer:待定。control_gateway:待定。- 服务端 CA 文件:待定。
- 证书与私钥:
~/bes-certs/boss/boss-service.pem、~/bes-certs/boss/boss-service-key.pem。
prod
control_issuer:待定。control_gateway:待定。- 服务端 CA 文件:待定。
- 证书与私钥:
~/bes-certs/boss/boss-service.pem、~/bes-certs/boss/boss-service-key.pem。
做完这一部分,手里有:BOSS 部署配置已填入地址、证书、产品、登录客户端、功能版本和平台租户,秘密存进密钥存储。
把用户跳到登录页
在哪里做:BOSS 代码
员工没登录时,BOSS 把浏览器跳到 BES 登录页。登录完,浏览器带着 code 回到 BOSS,下一步用它换令牌。
GET https://id.test.globalbes.sg/authorize?response_type=code&client_id=boss-web&redirect_uri=https%3A%2F%2Fboss.example.com%2Fcallback&scope=openid%20profile%20email&state=<state>&nonce=<nonce>&code_challenge=<code_challenge>&code_challenge_method=S256
- 端点地址从
GET https://id.test.globalbes.sg/.well-known/openid-configuration读。 state、nonce每次随机生成,存在用户会话里。code_challenge= BASE64URL(SHA256(code_verifier)),只支持S256;以上参数缺一个就被拒。- 回调收到
?code=…&state=…;code60 秒内有效,只能用一次。 - Java 示例见「接入新产品」的「把用户跳到登录页」。
用 code 换令牌
在哪里做:BOSS 代码
用 code 和 client_secret 换令牌,得到 access_token 和 refresh_token。存在服务端会话里,之后每个请求和刷新都要用。
POST https://id.test.globalbes.sg/oauth/token Authorization: Basic <base64(client_id:client_secret)> Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=<code>&code_verifier=<code_verifier>&redirect_uri=https%3A%2F%2Fboss.example.com%2Fcallback
HTTP/1.1 200 OK
{
"access_token": "<access_token>",
"id_token": "<id_token>",
"refresh_token": "<refresh_token>",
"token_type": "Bearer",
"expires_in": 300,
"scope": "openid profile email"
}
access_token、refresh_token:存在服务端的用户会话里,浏览器只拿会话 cookie。access_token有效 5 分钟。id_token里的sub是这个人的person_id,长期不变。
让用户选租户
在哪里做:BOSS 代码
带员工的 access_token 发起选租户,员工选好平台租户后,BOSS 得到 context_id。存进会话,每次验证请求时带上。
程序里发这个请求(id 用前面步骤返回的):
带:证书 + 用户的 access_token
请求:
POST https://id.test.globalbes.sg:9443/api/sessions/tenant-selection
Authorization: Bearer <access_token>
Content-Type: application/json
{
"continue_url": "https://boss.example.com/callback"
}
返回:
HTTP/1.1 201 Created
{
"selection_id": "9c4e1b7a-2d58-4f3c-b6a0-e81d5f2c7a94",
"selection_url": "/tenants?selection=9c4e1b7a-2d58-4f3c-b6a0-e81d5f2c7a94",
"expires_at": "2026-01-01T08:05:00+00:00"
}
continue_url必须与boss-web登记的redirect_uris或post_logout_redirect_uris之一完全一致。- 把浏览器跳到
https://id.test.globalbes.sg+selection_url;员工选好后回到continue_url?context_id=0f6b8d2e-4a17-4c93-9e5b-a1d27c4f3b68,把context_id存进服务端会话。 - 员工只能选平台租户
bes。 - 选择 5 分钟内有效;同一会话再发起一次会取消上一次。
- Java 示例见「接入新产品」的「让用户选租户」。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 401 | LOGIN_REQUIRED |
BES 登录已结束,重新登录后再发起。 |
验证每个请求
在哪里做:BOSS 代码
之后员工每发一个请求,BOSS 带证书、access_token、context_id 问 Control,返回这个人的权限列表。BOSS 按它放行或拒绝。
程序里发这个请求(id 用前面步骤返回的):
带:证书 + 用户的 access_token
请求:
POST https://id.test.globalbes.sg:9443/api/sessions/validate
Authorization: Bearer <access_token>
Content-Type: application/json
{
"product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
"context_id": "0f6b8d2e-4a17-4c93-9e5b-a1d27c4f3b68",
"target": {
"method": "GET",
"path": "/documents/42",
"deployment_version": "1"
}
}
返回:
HTTP/1.1 200 OK
{
"context": "<signed bes-context+jwt>",
"checked_at": "2026-01-01T08:00:00.123456+00:00",
"hard_expiry": "2026-01-01T08:00:30.123456+00:00",
"action": "document.read",
"risk": "low"
}
- 请求必须带
product_id,写boss的;BOSS 是平台级账号,不带会被拒绝。 target.method、target.path:BOSS 收到的请求,不含查询串;deployment_version:登记功能清单时用的版本。context用https://id.test.globalbes.sg/.well-known/jwks.json验证(ES256,类型bes-context+jwt),aud是boss的product_id;claimfeatures是此人的全部功能码。- Control 不可达、超时或返回
5xx时拒绝请求。 - Java 示例见「接入新产品」的「验证每个请求」。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 401 | INVALID_TOKEN |
刷新令牌后重试;刷新失败就重新登录。 |
| 401 | LOGIN_REQUIRED |
重新登录。 |
| 403 | ACCESS_DENIED |
返回 403。 |
| 403 | TARGET_UNKNOWN |
把这条路由登记进这个版本的功能清单。 |
刷新令牌
在哪里做:BOSS 代码
access_token 过期前用 refresh_token 换一对新令牌。两个都存下,下次请求和刷新用新的。
POST https://id.test.globalbes.sg/oauth/token Authorization: Basic <base64(client_id:client_secret)> Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=<refresh_token>
- 返回新的
access_token和refresh_token,两个都存下。 - 每个
refresh_token只能用一次;旧的再用一次,整个会话被撤销。同一会话的刷新要串行。 - 网页会话空闲 2 小时或总共 12 小时后结束,刷新也不延长。
结束登录
在哪里做:BOSS 代码
员工退出时,BOSS 带 access_token 结束在 Control 里的登录,手里的令牌失效。接着结束浏览器里的登录。
程序里发这个请求(id 用前面步骤返回的):
带:用户的 access_token
请求:
POST https://id.test.globalbes.sg/api/sessions/logout
Authorization: Bearer <access_token>
Content-Type: application/json
{
"login_scope": "product"
}
返回:
HTTP/1.1 204 No Content
login_scope: product(默认)登出 BOSS 所有登录客户端,client只登出这一个。- 先调这个接口,再清掉自己的会话。
- Java 示例见「接入新产品」的「结束登录」。
结束浏览器登录
在哪里做:BOSS 代码
把浏览器跳到 BES 的退出地址,结束 BES 在浏览器里的登录。员工下次进 BOSS 要重新登录。
GETEnd sessionGET https://id.test.globalbes.sg/end_session?client_id=boss-web&state=<state>
- BES 显示确认页。要回到 BOSS,先给
boss-web登记post_logout_redirect_uris,再在这里带上post_logout_redirect_uri。
加「Account settings」入口
在哪里做:BOSS 代码
在 BOSS 用户菜单里加「Account settings」链接,得到打开 BES 账户页并管理密码、两步验证等设置的入口。
- 在 BOSS 的用户菜单里加「Account settings」链接,在新标签页或约 560×760 的小窗口打开
https://id.test.globalbes.sg/account。 - 密码、两步验证、恢复码、已登录的浏览器、手机和邮箱、语言都在那里管理,BOSS 使用这个入口管理账户设置。
- 用户菜单只留:当前用户、切换公司(
session.tenant-selection-start)、Account settings、登出(end_session)。
做完这一部分,手里有:员工登录后的令牌和 context_id、每个请求的权限列表、账户设置入口;退出时两边的登录都结束。
登记接收地址
在哪里做:Control 服务器
在 Control 部署配置里登记 BOSS 的接收地址、audience 和 12 种事件,得到 Control 投递事件的接收方配置。
在 Control 部署配置的 event_consumers(环境变量 BES_CONTROL_EVENT_CONSUMERS)JSON 对象里配置接收方 boss:
{
"boss": {
"endpoint": "https://boss.local.globalbes.sg/internal/events/control",
"audience": "boss-api",
"event_types": [
"entitlement.changed",
"membership.joined",
"membership.revoked",
"role_assignment.changed",
"channel_access.revoked",
"invitation.created",
"tenant.policy_changed",
"person.status_changed",
"role.defined",
"role.changed",
"support_grant.issued",
"support_grant.closed"
]
}
}
audience写boss-api,event_types与bes-control-service/.env.example中boss的 12 种事件一致,事件内容见契约仓events/control/。
本环境(test)
- 接收地址:待定。
- Control 部署配置的位置和改完后怎样生效:待定。
其他环境
local
- 接收地址:
https://boss.local.globalbes.sg/internal/events/control。 - 本地 Control 的配置在 bes-infra 仓
envs/local/compose.yaml,把boss写进BES_CONTROL_EVENT_CONSUMERS的同一个 JSON 对象;更新部署配置后重新启动栈使配置生效:
BES_INFRA="$HOME/Projects/bes-infra" "$BES_INFRA/envs/local/dev" stack up
- 本地 Control 信任栈的服务端 CA(
~/.local/share/bes/local/stack/secrets/stack/server-ca.pem),接收地址要能从栈的容器里访问,证书由这张 CA 签发。
staging
- 接收地址:待定。
- Control 部署配置的位置和改完后怎样生效:待定。
prod
- 接收地址:待定。
- Control 部署配置的位置和改完后怎样生效:待定。
验证收到的事件
在哪里做:BOSS 代码
BOSS 收到事件后验证签名并按 event_id 存下再回 2xx,得到 BOSS 表里的事件供后续处理。
Control 每个事件发一个请求:
POST https://boss.local.globalbes.sg/internal/events/control Content-Type: application/jwt Idempotency-Key: 3e9a7c51-0b46-4d2f-9e83-a1c5f7d2b064 <event_jws>
- 请求体是 ES256 签名的 JWT,类型
bes-event+jwt,用https://id.test.globalbes.sg/.well-known/jwks.json验证;aud是登记的boss-api,5 分钟后过期。 - claim 是
common/event-envelope.json的信封:event_id、event_type、aggregate_type、aggregate_id、tenant_id、product_id、sequence、occurred_at、trace_id、payload。 - 先按
event_id(主键)存下再回2xx,之后从自己的表里处理;重复投递按event_id去重。 - 同一接收方的事件严格按顺序到达。没回
2xx时 Control 从 30 秒起重试到 24 小时、共 10 次;之后这次投递保持失败,后面的事件都停住。
本环境(test)
- 接收地址:待定。
其他环境
local
- 接收地址:
https://boss.local.globalbes.sg/internal/events/control。
staging
- 接收地址:待定。
prod
- 接收地址:待定。
Java(Spring Boot)
@PostMapping(path = "/internal/events/control", consumes = "application/jwt")
ResponseEntity<Void> receive(@RequestBody String jws) throws Exception {
JWTClaimsSet event = events.process(jws, null); // same setup as the context processor,
// type "bes-event+jwt", audience "boss-api"
try {
jdbc.update("insert into bes_event_inbox(event_id, event_type, body) values (?, ?, ?::jsonb)",
event.getStringClaim("event_id"), event.getStringClaim("event_type"), event.toString());
} catch (DuplicateKeyException alreadyStored) {
// delivered before: acknowledge again
}
return ResponseEntity.noContent().build();
}
做完这一部分,手里有:接收方 boss 已登记 12 种事件;BOSS 验证签名并按 event_id 去重存储后处理。