Skip to content

@ahoo-wang/fetcher-cosec

@ahoo-wang/fetcher-cosec 包将 CoSec(企业安全)认证与 Fetcher HTTP 客户端集成。它提供了带自动刷新的 JWT 令牌管理、通过 Storage 实现的令牌和设备 ID 持久化存储、多租户请求归属,以及处理授权、401 重试和 403 错误传播的完整拦截器链。

安装

bash
pnpm add @ahoo-wang/fetcher-cosec

架构概览

mermaid
graph TB
    subgraph sg_1 ["CoSec Interceptor Chain"]
        direction TB
        REQ1["CoSecRequestInterceptor<br>(order: MIN_SAFE_INTEGER + 1000)"]
        REQ2["ResourceAttributionRequestInterceptor<br>(order: URL_RESOLVE - 1000)"]
        REQ3["AuthorizationRequestInterceptor<br>(order: MIN_SAFE_INTEGER + 2000)"]
        RES1["AuthorizationResponseInterceptor<br>(order: MIN_SAFE_INTEGER + 1000)"]
        ERR1["UnauthorizedErrorInterceptor<br>401 handler"]
        ERR2["ForbiddenErrorInterceptor<br>403 handler"]
    end

    subgraph sg_2 ["Storage Layer"]
        direction LR
        TS["TokenStorage<br>extends KeyStorage&lt;JwtCompositeToken&gt;"]
        DS["DeviceIdStorage<br>extends KeyStorage&lt;string&gt;"]
    end

    subgraph sg_3 ["Token Management"]
        direction LR
        TM["JwtTokenManager<br>refresh orchestration"]
        TR["TokenRefresher<br>refresh strategy"]
        JT["JwtCompositeToken<br>access + refresh tokens"]
    end

    REQ1 --> |"CoSec-App-Id<br>CoSec-Device-Id<br>CoSec-Request-Id<br>CoSec-Space-Id"| DS
    REQ2 --> |"tenantId, ownerId<br>path params"| TS
    REQ3 --> |"Authorization: Bearer"| TM
    RES1 --> |"401 -> refresh -> retry"| TM
    TM --> TS
    TM --> TR
    TS --> JT

    style REQ1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style REQ2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style REQ3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RES1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ERR1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ERR2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style TS fill:#161b22,stroke:#30363d,color:#e6edf3
    style DS fill:#161b22,stroke:#30363d,color:#e6edf3
    style TM fill:#161b22,stroke:#30363d,color:#e6edf3
    style TR fill:#161b22,stroke:#30363d,color:#e6edf3
    style JT fill:#161b22,stroke:#30363d,color:#e6edf3

快速开始

typescript
import { Fetcher } from '@ahoo-wang/fetcher';
import { CoSecConfigurer, CoSecTokenRefresher } from '@ahoo-wang/fetcher-cosec';

const fetcher = new Fetcher({ baseUrl: 'https://api.example.com' });

const configurer = new CoSecConfigurer({
  appId: 'my-web-app',
  tokenRefresher: new CoSecTokenRefresher({
    fetcher,
    endpoint: '/auth/refresh',
  }),
  onUnauthorized: () => (window.location.href = '/login'),
  onForbidden: () => alert('Access Denied'),
});

configurer.applyTo(fetcher);

// All requests now include CoSec headers and Bearer token
const users = await fetcher.get('/api/users');

配置

CoSecConfig

CoSecConfigurer 接受一个 CoSecConfig 对象,包含三个配置级别:

级别必需属性功能
最小配置appId仅安全头(设备 ID、请求 ID、应用 ID)
标准配置appId + tokenRefresher完整的 JWT 认证,含自动刷新
企业配置所有选项多租户、自定义存储、自定义错误处理器
属性类型必填默认值描述
appIdstring--应用标识符,用于 CoSec-App-Id 请求头
tokenRefresherTokenRefresher--令牌刷新策略。提供时启用 JWT 认证。
tokenStorageTokenStoragenew TokenStorage()令牌持久化后端
deviceIdStorageDeviceIdStoragenew DeviceIdStorage()设备 ID 持久化
spaceIdProviderSpaceIdProviderNoneSpaceIdProvider多租户空间解析器
onUnauthorized(exchange) => Promise<void>--自定义 401 错误处理器
onForbidden(exchange) => Promise<void>--自定义 403 错误处理器

来源: packages/cosec/src/cosecConfigurer.ts:86-298

拦截器

CoSecRequestInterceptor

向每个发出的请求注入安全头:

请求头描述
CoSec-App-Id配置的 appId应用标识符
CoSec-Device-Id来自 DeviceIdStorage持久化设备标识符(nanoid)
CoSec-Request-Id每次请求生成唯一的请求关联 ID
CoSec-Space-Id来自 SpaceIdProvider空间标识符(如已解析)

来源: packages/cosec/src/cosecRequestInterceptor.ts:214-382

AuthorizationRequestInterceptor

添加 Authorization: Bearer <token> 请求头。在注入之前,它会检查访问令牌是否需要刷新,并在可能的情况下进行主动刷新。遵循 Ignore-Refresh-Token 属性以防止在刷新请求本身中触发递归刷新。

来源: packages/cosec/src/authorizationRequestInterceptor.ts:41-92

AuthorizationResponseInterceptor

处理 401 响应:

  1. 检查响应状态是否为 401
  2. 验证刷新令牌是否仍然有效
  3. 刷新访问令牌
  4. 使用新令牌重试原始请求
  5. 刷新失败时清除令牌

来源: packages/cosec/src/authorizationResponseInterceptor.ts:42-81

ResourceAttributionRequestInterceptor

当 URL 模板包含 {tenantId}{ownerId} 占位符时,自动从当前 JWT 载荷中注入 tenantIdownerId 路径参数。在 URL 解析之前运行。

来源: packages/cosec/src/resourceAttributionRequestInterceptor.ts:58-127

错误拦截器

拦截器状态码行为
UnauthorizedErrorInterceptor401调用配置中的 onUnauthorized 回调
ForbiddenErrorInterceptor403调用配置中的 onForbidden 回调

来源: packages/cosec/src/unauthorizedErrorInterceptor.ts, packages/cosec/src/forbiddenErrorInterceptor.ts

认证流程

mermaid
sequenceDiagram
autonumber

    participant App as Application
    participant F as Fetcher
    participant CRI as CoSecRequestInterceptor
    participant ARI as AuthorizationRequestInterceptor
    participant API as Remote API
    participant ARS as AuthorizationResponseInterceptor
    participant TM as JwtTokenManager
    participant TS as TokenStorage
    participant REF as TokenRefresher

    App->>F: fetcher.get('/api/data')
    F->>CRI: intercept(exchange)
    CRI->>CRI: Add CoSec-App-Id, CoSec-Device-Id, CoSec-Request-Id
    CRI->>ARI: intercept(exchange)
    ARI->>TS: get current token
    TS-->>ARI: JwtCompositeToken
    alt Token expired but refreshable
        ARI->>TM: refresh()
        TM->>REF: refresh(currentToken)
        Note over REF: POST /auth/refresh<br>with Ignore-Refresh-Token
        REF-->>TM: new CompositeToken
        TM->>TS: setCompositeToken(newToken)
    end
    ARI->>ARI: Set Authorization: Bearer <token>
    F->>API: HTTP Request with headers
    alt 401 Unauthorized
        API-->>ARS: 401 Response
        ARS->>TM: refresh()
        TM->>REF: refresh(currentToken)
        REF-->>TM: new CompositeToken
        TM->>TS: setCompositeToken(newToken)
        ARS->>F: retry with new token
        F->>API: HTTP Request (retried)
    end
    API-->>App: Response

JWT 令牌管理

JwtToken

解析 JWT 字符串并提供类型化的载荷访问和过期检查。支持 earlyPeriod 以在实际过期前触发主动刷新。

typescript
import { JwtToken } from '@ahoo-wang/fetcher-cosec';

const token = new JwtToken<CoSecJwtPayload>('eyJ...', 300000); // 5 min early period
console.log(token.isExpired);     // false if not yet expired
console.log(token.payload?.sub);  // user ID from payload

JwtCompositeToken

一起管理访问令牌和刷新令牌对:

typescript
import { JwtCompositeToken } from '@ahoo-wang/fetcher-cosec';

const composite = new JwtCompositeToken({
  accessToken: 'access.jwt.token',
  refreshToken: 'refresh.jwt.token',
}, 300000);

console.log(composite.authenticated);    // true if access token valid
console.log(composite.isRefreshNeeded);  // true if access token expired
console.log(composite.isRefreshable);    // true if refresh token still valid

TokenStorage

扩展 KeyStorage<JwtCompositeToken>,增加了认证特定的方法和跨标签页同步:

方法描述
signIn(compositeToken)存储新的复合令牌
signOut()移除已存储的令牌
authenticated检查是否存在有效令牌
currentUser如已认证则获取 JWT 载荷

来源: packages/cosec/src/tokenStorage.ts:43-121

JwtTokenManager

编排令牌刷新操作,支持去重(防止并发刷新请求):

属性 / 方法描述
currentToken从存储中获取当前复合令牌
refresh()刷新令牌。对并发调用进行去重。
isRefreshNeeded检查访问令牌是否需要刷新
isRefreshable检查刷新令牌是否仍然有效

来源: packages/cosec/src/jwtTokenManager.ts:33-105

CoSecTokenRefresher

内置的 TokenRefresher 实现,向配置的端点发送 POST 请求:

typescript
import { CoSecTokenRefresher } from '@ahoo-wang/fetcher-cosec';

const refresher = new CoSecTokenRefresher({
  fetcher: myFetcher,
  endpoint: '/auth/refresh',
});

// The refresher automatically sets IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY
// to prevent infinite refresh loops

重要:刷新请求包含 IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY 属性,以防止 AuthorizationRequestInterceptor 触发另一个刷新循环。

来源: packages/cosec/src/tokenRefresher.ts:141-207

JWT 载荷类型

mermaid
classDiagram
    class JwtPayload {
        +jti: string
        +sub: string
        +iss: string
        +aud: string | string[]
        +exp: number
        +nbf: number
        +iat: number
    }

    class CoSecJwtPayload {
        +tenantId: string
        +policies: string[]
        +roles: string[]
        +attributes: Record~string, any~
    }

    JwtPayload <|-- CoSecJwtPayload

    style JwtPayload fill:#161b22,stroke:#30363d,color:#e6edf3
    style CoSecJwtPayload fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
接口关键字段
JwtPayloadjtisubissaudexpnbfiat
CoSecJwtPayload继承所有字段 + tenantIdpoliciesrolesattributes

来源: packages/cosec/src/jwts.ts:17-80

设备追踪

DeviceIdStorage 扩展了 KeyStorage<string>,增加了 getOrCreate() 方法,该方法在首次使用时生成唯一的设备 ID(通过 nanoid)并持久化存储:

typescript
import { DeviceIdStorage } from '@ahoo-wang/fetcher-cosec';

const deviceStorage = new DeviceIdStorage();
const deviceId = deviceStorage.getOrCreate();
// First call: generates and stores a new nanoid
// Subsequent calls: returns the stored ID

来源: packages/cosec/src/deviceIdStorage.ts:35-71

主要导出

导出描述
CoSecConfigurer注册所有 CoSec 拦截器的 FetcherConfigurer
CoSecConfigCoSec 配置接口
CoSecHeaders请求头名称常量(DEVICE_IDAPP_IDSPACE_IDAUTHORIZATIONREQUEST_ID
CoSecRequestInterceptor向请求添加安全头
AuthorizationRequestInterceptor添加 Bearer 令牌并支持主动刷新
AuthorizationResponseInterceptor处理 401,刷新令牌并重试
ResourceAttributionRequestInterceptor从 JWT 注入 tenantId/ownerId
UnauthorizedErrorInterceptor自定义 401 处理器
ForbiddenErrorInterceptor自定义 403 处理器
TokenStorage带认证方法的 JWT 令牌持久化
DeviceIdStoragegetOrCreate() 的设备 ID 持久化
JwtTokenManager带去重的令牌刷新编排
JwtToken<Payload>带类型化载荷的已解析 JWT
JwtCompositeToken访问令牌 + 刷新令牌对
JwtCompositeTokenSerializer复合令牌序列化器
CoSecTokenRefresher内置的基于 POST 的令牌刷新器
TokenRefresher自定义刷新策略的接口
JwtPayload标准 JWT 载荷接口
CoSecJwtPayloadCoSec 扩展的 JWT 载荷,包含 tenantId、roles、policies
AuthorizeResults授权结果常量(ALLOWEXPLICIT_DENYIMPLICIT_DENY 等)
SpaceIdProvider多租户空间解析接口
NoneSpaceIdProvider空操作的空间提供者

交叉引用

  • Fetcher -- CoSecConfigurer 实现 FetcherConfigurer 以与 Fetcher 的拦截器链集成
  • Storage -- TokenStorageDeviceIdStorage 扩展 KeyStorage 用于持久化
  • React -- useSecurityRouteGuardRefreshableRouteGuard 提供 React 集成
  • EventBus -- 令牌和设备存储使用 BroadcastTypedEventBus 实现跨标签页同步

基于 Apache License 2.0 发布。