Skip to content

接入 CoSec 身份认证 ​

将浏览器应用连接到现有 CoSec 服务,得到一个处理登录、受保护请求和退出登录的会话对象。

安装时还需要解析该包声明的全部 peer 依赖;完整清单见包接入前提。以下服务路由和身份由应用提供,不由安装过程创建。

1. 确认认证契约 ​

安装 @ahoo-wang/fetcher 和 @ahoo-wang/fetcher-cosec。应用须已有登录流程,返回 JWT 字符串 accessToken 和 refreshToken。本例的 POST /auth/refresh 接收该令牌对的 JSON 并返回新令牌对,GET /profile 返回 { id, name }。这些路由和 app ID 必须与后端一致。仅对该可信来源使用此客户端。下面的 isTrusted: sameOriginTrust 让令牌和设备 ID 只发往 baseURL 所在源与页面自身的源;不设它时,发往其他任意源绝对 URL 的请求(分页链接、下载地址)会收到访问令牌。

2. 为应用所有者创建一个会话 ​

ts
import { Fetcher, ResultExtractors } from '@ahoo-wang/fetcher';
import {
  CoSecConfigurer,
  CoSecTokenRefresher,
  sameOriginTrust,
  type CompositeToken,
} from '@ahoo-wang/fetcher-cosec';

export function createSession(baseURL: string) {
  const api = new Fetcher({ baseURL });
  const refreshApi = new Fetcher({ baseURL });
  const cosec = new CoSecConfigurer({
    appId: 'developer-console',
    isTrusted: sameOriginTrust,
    tokenRefresher: new CoSecTokenRefresher({
      fetcher: refreshApi,
      endpoint: '/auth/refresh',
    }),
    onUnauthorized: () => {
      console.error('Sign in again');
    },
    onForbidden: async () => {
      console.error('Access denied');
    },
  });
  cosec.applyTo(api);

  return {
    signIn(tokens: CompositeToken) {
      cosec.tokenStorage.signIn(tokens);
    },
    signOut() {
      cosec.tokenStorage.signOut();
    },
    async loadProfile() {
      return api.get<{ id: string; name: string }>(
        '/profile',
        {},
        { resultExtractor: ResultExtractors.Json },
      );
    },
    dispose() {
      cosec.tokenStorage.destroy();
      cosec.deviceIdStorage.destroy();
    },
  };
}

独立刷新客户端让刷新请求不经过受保护请求的管线。CoSecTokenRefresher 还会设置跳过递归刷新的标记。提供 tokenRefresher 才会启用 bearer 认证;仅传 appId 只安装基础请求头和资源归属逻辑。

3. 连接登录与退出流程 ​

应用启动时创建 const session = createSession(yourApiOrigin)。登录成功后调用 session.signIn(loginResponse),再在 UI 错误边界内等待 session.loadProfile()。退出时调用 session.signOut() 并清除敏感 UI 状态。不要将真实令牌写入源码或日志。

令牌解析不是签名验证,授权责任仍在后端。请求中显式提供的 Authorization 不会被覆盖。添加空间或租户行为前先阅读请求头与资源归属规则。

4. 验证刷新与失败行为 ​

在测试中使用内存令牌/设备存储和模拟 fetch,覆盖有效令牌、访问令牌过期但刷新令牌有效、并发请求、刷新失败及 401/403 响应。刷新并发在单个 token manager 内共享,不是跨标签页的分布式锁。刷新因另一个标签页已用掉一次性刷新令牌而失败时,本标签页会重新读取存储,并用那个标签页存入的令牌继续;服务端拒绝的刷新(刷新端点返回 4xx,或响应不是 composite token)会清除当前会话并抛 RefreshTokenError,而未能到达服务端的刷新(网络错误、超时、中止、5xx)保留会话并抛 RefreshUnavailableError,不调用 onUnauthorized;回调不会将失败请求变成成功结果。除了显示登录或错误界面,也要捕获请求拒绝。

5. 清理所有者 ​

当前 TokenStorage 默认使用按存储 key 命名的广播总线;其他上下文需要匹配通道和支持的 messenger。这与默认只使用本地总线的普通 KeyStorage 不同。广播是变更通知,不是跨标签页的事务性刷新协调。

应用关闭时停止尚未完成的应用任务,并调用 session.dispose()。每个存储的 destroy() 释放自己的处理器并关闭自己创建的广播总线,不删除令牌;退出登录需另外调用 signOut()。 存储通知是异步的:退出登录后通知仍在投递时,不要立即销毁会话。投递中关闭总线可能中断广播并记录错误。不要销毁其他所有者仍在共享的总线。配置器没有 dispose 方法;本例随会话一起停止使用私有 Fetcher。

查阅配置和令牌刷新与存储。

tokenStorage.ts:94 创建默认广播总线。

评估集成边界;返回本组任务。

基于 Apache License 2.0 发布。