拦截器与资源归属
这些拦截器补充或重试 FetchExchange。通过对应 request、response 或 error manager 的 .use(instance) 注册。配置器安装常见组合。
实际执行流程
- CoSecRequestInterceptor 跳过被
isTrusted拒绝的请求;否则设置应用/设备/请求头,以及解析得到的真值空间 ID。 - AuthorizationRequestInterceptor 跳过不受信任的请求(不注入 Authorization、不刷新),并保留已有 Authorization;否则检查会话所有权,access 过期且 refresh 有效时刷新,再注入受管理的 Bearer token。
- ResourceAttributionRequestInterceptor 在 URL 解析前填充租户/所有者路径参数,随后核心传输发送请求。
- AuthorizationResponseInterceptor 在正常状态校验前处理受管理凭据的 401。刷新后仅删除自己注入的陈旧凭据,并完整重跑 exchange 管线,最多一次。
- 剩余失败进入错误拦截器。Unauthorized 通过通知所有权守卫处理 401/RefreshTokenError(
RefreshUnavailableError表示刷新未能到达服务端,会话保留,不通知);Forbidden 处理 403。回调都不会自动恢复失败请求。
request/response manager 按 order 排序,注册顺序本身不是执行契约。重跑管线可能生成新的 CoSec 请求 ID。调用者提供的 Authorization 不会替换或自动刷新。access 已过期而 refresh 不可用时仍可能附加 token,由服务端决定响应状态。
sequenceDiagram
autonumber
participant App
participant Fetcher
participant Tokens
participant Service
App->>Fetcher: Request
Fetcher->>Tokens: 读取受管理会话
opt access 过期且 refresh 有效
Tokens->>Service: POST refresh
Service-->>Tokens: CompositeToken
end
Fetcher->>Service: 携带受管理 Authorization 的请求
Service-->>Fetcher: Response
opt 受管理请求的 401,尚未重试
Fetcher->>Tokens: 刷新同一会话
Tokens-->>Fetcher: 当前 token 或错误
Fetcher->>Service: 重跑 exchange 一次
Service-->>Fetcher: 重试响应
end
Fetcher-->>App: 提取结果或错误拦截器参数与结果
| 导出 | 构造选项 | intercept(exchange) |
|---|---|---|
CoSecRequestInterceptor / CoSecRequestOptions | 必填 appId、deviceIdStorage;spaceIdProvider 默认 NoneSpaceIdProvider;可选 isTrusted | Promise<void>;不受信任的请求什么都不做;否则覆盖应用/设备/请求头;仅为真值 ID 写空间头;存储/provider 失败传播 |
AuthorizationRequestInterceptor / AuthorizationInterceptorOptions | 必填 tokenManager(JwtTokenManagerCapable);可选 isTrusted | Promise<void>;不受信任的请求什么都不做;保留显式 Authorization,需要时刷新受管理 token |
AuthorizationResponseInterceptor | 同 AuthorizationInterceptorOptions | Promise<void>;仅 401、匹配受管理凭据,最多 AUTHORIZATION_RESPONSE_MAX_RETRY=1 |
ResourceAttributionRequestInterceptor / ResourceAttributionOptions | 必填 tokenStorage;tenantId='tenantId'、ownerId='ownerId' 是占位符名称 | void;从解码的 access payload 取 tenantId/sub;仅匹配模板且当前路径值为假值时填充 |
UnauthorizedErrorInterceptor / options | 必填 onUnauthorized,返回 void 或 Promise<void> | Promise<void>;跳过 RefreshSessionChangedError、RefreshUnavailableError 及重复/过时通知;回调错误成为 exchange 错误(ExchangeError 的 cause) |
ForbiddenErrorInterceptor / options | 必填 onForbidden,返回 Promise<void> | Promise<void>;仅 response.status=403 时执行回调;回调错误成为 exchange 错误(ExchangeError 的 cause) |
IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY 为 Ignore-Refresh-Token。只要属性存在(即使值为 false),就禁用主动/401 自动刷新。它不阻止 Authorization 注入,也不禁用普通 HTTP 状态错误。
请求信任
RequestTrust 为 (url: string, exchange: FetchExchange) => boolean;RequestTrustCapable 为 CoSecConfig、CoSecRequestOptions 与 AuthorizationInterceptorOptions 增加可选的 isTrusted。isTrustedRequest(exchange, isTrusted?) 为两个请求拦截器做判断:没有 isTrusted 时信任所有请求;有它时,相对请求 URL 始终受信任(它相对 baseURL 解析),绝对 URL 仅在 isTrusted(url, exchange) 返回 true 时受信任。该判断对每个 exchange(每个谓词)只做一次并保存在 exchange 上:401 重试时 URL 已按 baseURL 解析为绝对 URL,仍复用首次判断,因此受信任的相对请求在重试时保留凭据,且每个请求只调用一次 isTrusted。sameOriginTrust 信任 fetcher baseURL 所在源或页面自身源上的绝对 URL;其他源以及无法解析的 URL 都不受信任。
默认信任所有源
默认情况下,任意源上的绝对请求 URL 都会收到访问 token 与 CoSec 请求头(含设备 ID)。客户端会请求不受你控制的 URL(如分页链接、下载地址、回调)时,请配置 isTrusted: sameOriginTrust(或自己的判断函数)。
设备与空间选择
DeviceIdStorage(options={}) 和 SpaceIdStorage(options={}) 继承 KeyStorage<string>。选项为部分 KeyStorageOptions,每个类都强制使用自己的 identity serializer。默认键分别是 cosec-device-id、cosec-space-id,默认广播总线的 serial delegate 名称取实际 key,destroy() 会关闭这种默认总线;通过 eventBus 传入的总线保持打开。存储选择继承自 KeyStorage,可注入事件总线/存储;清理遵循 KeyStorage。
DeviceIdStorage.generateDeviceId(): string 调用导出的 idGenerator,但不存储结果。getOrCreate(): string 返回已有真值 ID,否则生成并存储。IdGenerator.generateId(): string 由 NanoIdGenerator 使用 nanoid 实现,idGenerator 是共享实例。
SpaceIdProvider.resolveSpaceId(exchange): string | null 为同步方法。NoneSpaceIdProvider 始终返回 null。DefaultSpaceIdProvider({ spacedResourcePredicate, spaceIdStorage }) 两者必填,调用 SpacedResourcePredicate.test(exchange): boolean,仅匹配时返回已存空间值。没有你的 predicate/storage 就不会自行从 URL 或 token 推断空间。
常量与鉴权数据
| 导出族 | 值 |
|---|---|
COSEC_REQUEST_INTERCEPTOR_NAME/ORDER | CoSecRequestInterceptor / Number.MIN_SAFE_INTEGER + DEFAULT_INTERCEPTOR_ORDER_STEP |
AUTHORIZATION_REQUEST_INTERCEPTOR_NAME/ORDER | AuthorizationRequestInterceptor / COSEC_REQUEST_INTERCEPTOR_ORDER + DEFAULT_INTERCEPTOR_ORDER_STEP |
AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME/ORDER | AuthorizationResponseInterceptor / Number.MIN_SAFE_INTEGER + 1000 |
RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME/ORDER | ResourceAttributionRequestInterceptor / URL_RESOLVE_INTERCEPTOR_ORDER - DEFAULT_INTERCEPTOR_ORDER_STEP |
UNAUTHORIZED_ERROR_INTERCEPTOR_NAME/ORDER | UnauthorizedErrorInterceptor / 0 |
FORBIDDEN_ERROR_INTERCEPTOR_NAME/ORDER | ForbiddenErrorInterceptor / 0 |
DEFAULT_COSEC_DEVICE_ID_KEY、DEFAULT_COSEC_SPACE_ID_KEY | cosec-device-id、cosec-space-id |
CoSecHeaders 静态字段 | DEVICE_ID=CoSec-Device-Id、APP_ID=CoSec-App-Id、SPACE_ID=CoSec-Space-Id、AUTHORIZATION=Authorization、REQUEST_ID=CoSec-Request-Id |
ResponseCodes | UNAUTHORIZED=401、FORBIDDEN=403 |
AuthorizeResult 为 { authorized:boolean, reason:string }。AuthorizeResults 含 ALLOW(true、'Allow')、EXPLICIT_DENY('Explicit Deny')、IMPLICIT_DENY('Implicit Deny')、TOKEN_EXPIRED('Token Expired')、TOO_MANY_REQUESTS('Too Many Requests');除 ALLOW 外 authorized 均为 false。它们是结果对象,不是本地策略引擎或服务端状态码映射器。
完整空间配置
import { Fetcher } from '@ahoo-wang/fetcher';
import {
CoSecConfigurer,
DefaultSpaceIdProvider,
SpaceIdStorage,
TokenStorage,
DeviceIdStorage,
} from '@ahoo-wang/fetcher-cosec';
import { InMemoryStorage } from '@ahoo-wang/fetcher-storage';
import { SerialTypedEventBus } from '@ahoo-wang/fetcher-eventbus';
const spaces = new SpaceIdStorage({
storage: new InMemoryStorage(),
eventBus: new SerialTypedEventBus('example-space'),
});
spaces.set('workspace-1');
const provider = new DefaultSpaceIdProvider({
spaceIdStorage: spaces,
spacedResourcePredicate: {
test: exchange => exchange.request.url.startsWith('/projects'),
},
});
const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });
const cosec = new CoSecConfigurer({
appId: 'example-app',
spaceIdProvider: provider,
tokenStorage: new TokenStorage({
storage: new InMemoryStorage(),
eventBus: new SerialTypedEventBus('example-token'),
}),
deviceIdStorage: new DeviceIdStorage({
storage: new InMemoryStorage(),
eventBus: new SerialTypedEventBus('example-device'),
}),
});
cosec.applyTo(fetcher);
// 构造配置本身不发送请求。
spaces.destroy();
cosec.tokenStorage.destroy();
cosec.deviceIdStorage.destroy();AuthorizationInterceptorOptions — packages/cosec/src/authorizationRequestInterceptor.ts:30
AUTHORIZATION_REQUEST_INTERCEPTOR_NAME — packages/cosec/src/authorizationRequestInterceptor.ts:33
AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER — packages/cosec/src/authorizationRequestInterceptor.ts:35
AuthorizationRequestInterceptor — packages/cosec/src/authorizationRequestInterceptor.ts:48
AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME — packages/cosec/src/authorizationResponseInterceptor.ts:34
AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDER — packages/cosec/src/authorizationResponseInterceptor.ts:41
AUTHORIZATION_RESPONSE_MAX_RETRY — packages/cosec/src/authorizationResponseInterceptor.ts:50
AuthorizationResponseInterceptor — packages/cosec/src/authorizationResponseInterceptor.ts:69
CoSecRequestOptions — packages/cosec/src/cosecRequestInterceptor.ts:59
COSEC_REQUEST_INTERCEPTOR_NAME — packages/cosec/src/cosecRequestInterceptor.ts:85
COSEC_REQUEST_INTERCEPTOR_ORDER — packages/cosec/src/constants.ts:46
IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY — packages/cosec/src/constants.ts:73
CoSecRequestInterceptor — packages/cosec/src/cosecRequestInterceptor.ts:174
RequestTrust — packages/cosec/src/requestTrust.ts:26
RequestTrustCapable — packages/cosec/src/requestTrust.ts:28
sameOriginTrust — packages/cosec/src/requestTrust.ts:57
isTrustedRequest — packages/cosec/src/requestTrust.ts:73
DEFAULT_COSEC_DEVICE_ID_KEY — packages/cosec/src/deviceIdStorage.ts:22
DeviceIdStorageOptions — packages/cosec/src/deviceIdStorage.ts:25
DeviceIdStorage — packages/cosec/src/deviceIdStorage.ts:32
IdGenerator — packages/cosec/src/idGenerator.ts:16
NanoIdGenerator — packages/cosec/src/idGenerator.ts:24
idGenerator — packages/cosec/src/idGenerator.ts:35
ResourceAttributionOptions — packages/cosec/src/resourceAttributionRequestInterceptor.ts:27
RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME — packages/cosec/src/resourceAttributionRequestInterceptor.ts:45
RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDER — packages/cosec/src/resourceAttributionRequestInterceptor.ts:53
ResourceAttributionRequestInterceptor — packages/cosec/src/resourceAttributionRequestInterceptor.ts:61
SpaceIdProvider — packages/cosec/src/spaceIdProvider.ts:67
NoneSpaceIdProvider — packages/cosec/src/spaceIdProvider.ts:123
DEFAULT_COSEC_SPACE_ID_KEY — packages/cosec/src/spaceIdProvider.ts:134
SpaceIdStorageOptions — packages/cosec/src/spaceIdProvider.ts:169
SpaceIdStorage — packages/cosec/src/spaceIdProvider.ts:210
SpacedResourcePredicate — packages/cosec/src/spaceIdProvider.ts:289
SpaceIdProviderOptions — packages/cosec/src/spaceIdProvider.ts:318
DefaultSpaceIdProvider — packages/cosec/src/spaceIdProvider.ts:375
CoSecHeaders — packages/cosec/src/types.ts:20
ResponseCodes — packages/cosec/src/types.ts:28
AuthorizeResult — packages/cosec/src/types.ts:57
AuthorizeResults — packages/cosec/src/types.ts:65
UNAUTHORIZED_ERROR_INTERCEPTOR_NAME — packages/cosec/src/constants.ts:23
UNAUTHORIZED_ERROR_INTERCEPTOR_ORDER — packages/cosec/src/unauthorizedErrorInterceptor.ts:28
UnauthorizedErrorInterceptorOptions — packages/cosec/src/unauthorizedErrorInterceptor.ts:33
UnauthorizedErrorInterceptor — packages/cosec/src/unauthorizedErrorInterceptor.ts:73
FORBIDDEN_ERROR_INTERCEPTOR_NAME — packages/cosec/src/forbiddenErrorInterceptor.ts:21
FORBIDDEN_ERROR_INTERCEPTOR_ORDER — packages/cosec/src/forbiddenErrorInterceptor.ts:27
ForbiddenErrorInterceptorOptions — packages/cosec/src/forbiddenErrorInterceptor.ts:32
ForbiddenErrorInterceptor — packages/cosec/src/forbiddenErrorInterceptor.ts:113
