用途
本技能用于在 iOS 和 Android 的 React Native(Expo)应用中,把登录做得安全且完整:令牌只存放在 Keychain 或 Keystore 中,由单一会话管理器负责恢复、刷新和结束会话且不产生竞态,路由分组随会话开闭,OAuth 走带 PKCE 的授权码流程,Apple 登录使用 nonce,登出后不残留上一位用户的数据。技能附带零依赖的结构检查器验证结构,用 Jest 测试验证行为。
主要能力与必须遵守的规则
- 令牌只存放在 expo-secure-store,且位于 SecureStoragePort 之后。一个适配器导入 SDK,并向每个异步调用传入同一个冻结的选项对象(keychainAccessible: AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY);不使用 getItem/setItem,不使用 ALWAYS,会话令牌不启用 requireAuthentication。
- 凭据不出现在其他任何地方:不在 SQLite、kv-store、AsyncStorage、MMKV 或文件中,不在 Zustand store(哪怕只是一个裸 token 字段),不在日志、崩溃报告或分析事件中,不在 TanStack mutation 中,不在 URL 中,绝不在 EXPO_PUBLIC_* 中。
- 每个密钥在 SECURE_KEYS 中声明一次;清除时遍历 ALL_SECURE_KEYS。
- 全新安装会清除 Keychain 保留的内容:在恢复时检查应用沙盒中的安装标记(kv-store),缺失则清除所有密钥再标记;不可读则永不清除。
- 恢复不猜测、不无限等待:后台读取失败发布 unavailable 并在前台重试;前台失败(读取两次)以 storage-unavailable 登出并保留值;不可读的会话登出但保留。恢复是 app-startup-and-bootstrap 的阻塞启动门任务 restoreSessionTask,在其 deadlineMs 内完成;未命中只打开“检查会话”路由,绝不出现空帧。
- 先持久化再发布;登出总是优先。登录先写刷新令牌,再写其余,最后才发布已登录;所有写入和清除经过同一队列,代际号会丢弃登出前开始的工作。
- 单一令牌签发方,同一时间只有一次刷新,由会话管理器持有。每个登录方法最终都得到签发方的令牌;有应用服务器时(Apple 需要)各方法在服务器交换,刷新器为 createServerTokenRefresher;仅当 OAuth 是唯一方法且无服务器时才用 OAuth 刷新器。调用方共享进行中的刷新;只有会话管理器调用刷新器。
- 只有服务器能结束会话。invalid_grant 或其他 OAuth 错误结束会话;无连接、超时或 5xx 保留会话,未过期令牌离线仍可发出;401 续期一次(sendWithSession),第二次 401 登出。
- 受保护分组是守卫而非导航。每个已登录分组有自己的 _layout,位于 <Stack.Protected guard={isSignedIn}> 内;未登录路由位于 guard={isSignedOut} 内;流程通过改变会话来切换,绝不用 router.replace;SDK 58 之前不用 redirectTo。
- 登出分两阶段清除一切。首先 signOut('user') 时,serverSignOutSteps(推送注销、服务器登出)在 5 秒期限内用仍有效的令牌执行;然后清除所有密钥,发布已登出(受保护屏幕离开),执行每个 signOutSteps 条目(查询缓存、账户 store、行、提醒)即使某个失败,并尽力撤销刷新令牌。两份列表在 contracts/auth.json 中命名。
- OAuth 是在临时系统浏览器中带 PKCE(S256)的授权码流程。不用隐式流程、不用 WebView、不用客户端密钥,code_verifier 传给 exchangeCodeAsync。
- Apple 登录仅限 iOS、需配置、带 nonce。usesAppleSignIn(或插件),新 nonce 用 SHA-256 哈希,ERR_REQUEST_CANCELED 静默,凭据由应用服务器验证,首次登录保存姓名和邮箱,凭据被撤销则登出,使用 Apple 自己的按钮。
- Android 备份排除安全存储:在 app.config.ts 中配置 ['expo-secure-store', { faceIDPermission: false }],或使用契约中命名的应用自有规则。
- 创建账户的应用需在应用内删除账户:已登录分组中的路由,服务器删除(并撤销 Apple 令牌),然后 signOut('account-deleted')。
使用方式
- 阅读 references/session-lifecycle.md(状态、竞态和登出顺序)和 references/secure-storage.md(平台行为)。提供方读 references/oauth-and-apple.md;路由读 references/protected-routes.md。
- 安装包并编辑 app.config.ts(references/adopting-in-an-app.md 第 1-2 节):expo-secure-store 插件带 faceIDPermission: false、Apple 插件、每个变体的 scheme。
- 把 templates/ 下每个文件复制到其首行指定的路径,包括测试;契约和 schema 落到 contracts/。
- 把四个端口加入组合根及其测试,并在 contracts/architecture.json 中记录 SDK(examples/create-device-services.ts、examples/services-context.tsx、adopting-in-an-app.md 第 4-5 节)。
- 读取 src/config/oauth-config.ts 中的提供方设置(examples/oauth-config.ts);为每个变体向提供方注册 <scheme>://oauth-redirect(Google 在 Android 上拒绝自定义 scheme)。停下来向开发者询问签发方、客户端 id 和 Apple 团队设置。
- 选定唯一的令牌签发方(规则 7):有服务器时每个方法都在那里交换,会话获得 examples/server-refresher.ts。在 AppRoot 中创建一次会话(examples/create-app-session.ts),在 RootStack 中渲染 SessionProvider(examples/root-stack.tsx),并守卫 Stack(examples/app-stack.tsx);给每个受保护分组自己的 _layout.tsx。
- 把登录构建为功能:examples/use-sign-in-model.ts、examples/sign-in-screen.tsx、examples/sign-in-route.tsx、Android 重定向路由(examples/oauth-redirect-route.tsx、examples/use-oauth-redirect.ts),iOS 上 examples/use-apple-sign-in.ts 基于 examples/auth-mutations.ts。
- 通过 examples/authorized-api-client.ts(sessionHeaders 和 sendWithSession)向应用服务器发送请求,把每个按账户的缓存作为登出步骤,把每个需要会话的服务器调用(推送注销)作为服务器登出步骤;两者都列在 contracts/auth.json 中。
- 当应用创建账户时,构建删除(examples/use-delete-account-model.ts)并在契约中命名路由。仅针对 staging 服务器测试,绝不针对生产。
- 为每个采用的示例写测试(createSessionTestKit 让它们保持简短),并为应用自己的 AppStack 写路由测试(来自 session-guards.test.tsx:未登录深链落到登录;检查器看不到绑定到错误布尔值的守卫),运行 npx jest --ci,然后运行检查器:node ${CLAUDE_SKILL_DIR}/scripts/check-auth.mjs .;修复并重跑直到 RESULT: PASS。在 references/checker-rules.md 中查规则。
- 发布前把设备清单(references/oauth-and-apple.md 第 9 节)交给人工,针对一台指定的 iOS 设备和一台指定的 Android 设备。
适用场景
适用于需要在 Expo/React Native 应用中添加登录、令牌管理、401 重试、登出、OAuth、Apple 登录、受保护路由、账户删除等认证与安全存储能力的开发场景。
限制
- Apple 登录仅限 iOS。
- 账户删除测试仅针对 staging 服务器,绝不针对生产。
- 检查器有 17 条规则;存储规则仅在 "signIn": false 时适用。
- 部分配置(签发方、客户端 id、Apple 团队设置)需向开发者询问,属于对外账户。
- 发布前需人工在指定设备上完成设备清单。
- 文档中未提供具体收费、价格或平台支持范围等信息。