Expo API 审计
Expo API 集成审计
概览
这项技能审计 Expo(React Native)TypeScript 应用的 API 集成层,以识别差距、硬编码数据、认证问题和离线行为问题。适用于使用 Expo Router、expo-secure-store 和 expo-constants 的应用。
输入
开始前,从用户那里收集:
- 已知问题(可选):怀疑存在问题的特定屏幕或流程
- 输出格式偏好:markdown 报告、JSON 查找、修复 PR 或任务列表
- 范围:全面审计或特定焦点(仅认证、仅离线等)
工具说明:ripgrep
如果 rg(ripgrep)可用,使用它而不是 grep —— 它的速度明显更快,并且自动忽略 node_modules/.git。所有 grep 命令在这项技能中都有 rg 等效命令:
# 检查 ripgrep 是否可用
which rg && echo "Use rg commands" || echo "Falling back to grep"
# 等效命令:
# grep -rn "pattern" --include="*.ts" | grep -v node_modules
# rg "pattern" -t ts
# grep -rln "pattern" --include="*.ts" | grep -v node_modules
# rg -l "pattern" -t ts
第一阶段:发现
在审计之前构建一个心理模型。运行这些命令来定位关键文件:
# Orval 配置和生成的钩子
find . -name "orval.config.*" -o -name "*.orval.ts" 2>/dev/null | head -5
find . -type d -name "generated" | xargs -I{} ls {} 2>/dev/null | head -20
# API 客户端/变异器
rg -l "customInstance|axios\.create|baseURL" -t ts | head -10
# Zustand 存储
rg -l "create\(" -t ts | xargs rg -l "zustand|devtools" 2>/dev/null | head -20
# OpenAPI 规范
find . -name "openapi.json" -o -name "openapi.yaml" -o -name "swagger.json" 2>/dev/null
# 认证基础设施
rg -l "TokenManager|refreshToken|Bearer|interceptor" -t ts
# Expo 配置(环境变量,API URL)
cat app.config.js 2>/dev/null || cat app.config.ts 2>/dev/null || cat app.json
rg "expoConfig|Constants\.manifest" -t ts
关键文件映射
| 组件 | 典型位置 | 检查内容 |
|---|---|---|
| Orval 配置 | orval.config.ts |
客户端类型、变异器路径、输出目录 |
| 生成的钩子 | /api/generated/ 或 /src/api/ |
与 OpenAPI 规范的完整性对比 |
| Axios 客户端 | /api/ 或 /services/ |
拦截器、基础配置 |
| 令牌管理器 | /services/auth/ |
必须使用 expo-secure-store |
| Zustand 存储 | /stores/ |
持有服务器状态(违规?) |
| OpenAPI 规范 | /docs/api/ 或根目录 |
最后修改、版本 |
| Expo 配置 | app.config.js 或 app.json |
extra 中的 API URL,环境变量 |
| 屏幕 | /app/(Expo Router) |
基于文件的路由 |
第二阶段:API 层审计
2.1 Orval 生成健康
# 检查生成的代码是否符合规范
npx orval --dry-run 2>&1 | head -50
# 比较端点计数
jq '.paths | keys | length' docs/api/openapi.json # 规范中的端点
find ./api/generated -name "*.ts" -exec grep -l "useQuery\|useMutation" {} \; | wc -l
验证:
- [ ] 生成的类型与 OpenAPI 模式匹配
- [ ] 所有规范端点都有相应的钩子
- [ ] 自定义变异器注入认证头
- [ ] 查询默认设置合理(staleTime,gcTime,retry)
2.2 认证令牌处理
检查 axios 客户端是否有这些模式:
// 必需:请求拦截器注入令牌
config.headers.Authorization = `Bearer ${token}`
// 必需:令牌到期前预刷新
if (tokenExpiresWithin(600)) await refreshToken()
// 必需:401 响应触发刷新
if (error.response?.status === 401) { /* 刷新逻辑 */ }
// 必需:刷新去重
if (isRefreshing) return pendingRefreshPromise
// 必需:刷新失败触发注销
clearTokens(); navigate('/auth')
Expo 特定检查:
# 令牌存储 - 必须使用 expo-secure-store,而不是 AsyncStorage
rg "AsyncStorage.*token|token.*AsyncStorage" -t ts -i # BAD if found
rg "SecureStore|expo-secure-store" -t ts # GOOD - should exist
# 环境变量 - 应使用 expo-constants 或 app.config.js
rg "process\.env\." -t ts # BAD for Expo (won't work in production)
rg "Constants\.expoConfig|Constants\.manifest" -t ts # GOOD - Expo way
grep -l "extra:" app.config.* 2>/dev/null # Config-based env vars
红旗:
- 源代码中的硬编码令牌或 API 密钥
- 使用 AsyncStorage 的令牌(必须使用
expo-secure-store) - 使用
process.env的 API URL(使用expo-constants替代) - 没有刷新去重(并行刷新调用)
- 无限刷新循环(刷新端点返回 401)
- 令牌检查和请求之间的竞态条件
2.3 直接 API 违规
查找绕过 Orval 生成钩子的调用:
# 原始 fetch(应使用生成的钩子)
rg "fetch\(" -t ts -t tsx --glob '!*.d.ts'
# 直接 axios(应使用 orval 变异器)
rg "axios\.|axios\(" -t ts -t tsx --glob '!*orval*'
# 手动 useQuery(应使用生成的)
rg "useQuery\(|useMutation\(" -t ts -t tsx --glob '!*generated*'
# 硬编码 URL
rg "https?://[^\"']*api" -t ts -t tsx
# Expo 特定:process.env 使用(在 Expo 生产构建中不起作用)
rg "process\.env\." -t ts -t tsx # 应使用 Constants.expoConfig.extra 替代
分类每个发现:
- 合法:第三方 API、文件上传、WebSocket
- 违规:不使用生成的钩子的后端调用
- 硬编码:应使用
expo-constants的 URL - 环境错误:
process.env使用(在 Expo 生产中破坏)
第三阶段:屏幕数据审计
对于 /app/(Expo Router)或 /screens/ 中的每个屏幕:
3.1 数据源分类
| 类别 | 模式 | 状态 |
|---|---|---|
| API 数据 | useGet*(), use*Query() |
✓ 正确 |
| 缓存 | React Query 提供过时数据 | ✓ 预期 |
| Zustand | 商业数据在存储中 | ⚠️ 应该是服务器状态? |
| 硬编码 | Mock 数组、占位符对象 | ❌ 标记 |
| 派生 | 来自 API 数据的计算 | ⚠️ 检查后端是否应该计算 |
# 查找没有 API 钩子的屏幕(可疑)
for f in $(find ./app -name "*.tsx" | grep -v "_layout"); do
if ! grep -q "use.*Query\|use.*Mutation\|useGet\|usePost\|usePut\|useDelete" "$f"; then
echo "NO API HOOKS: $f"
fi
done
# 查找硬编码数组/对象(rg 版本)
rg "useState\(\[" -t tsx
rg "const.*=.*\[\{" -t tsx
3.2 用户交互审计
每个修改数据的用户操作都必须触发变异:
| 操作类型 | 必需模式 |
|---|---|
| 表单提交 | useMutation + onSuccess 使无效 |
| 切换/开关 | 变异或防抖变异 |
| 删除 | 变异带有乐观更新或确认 |
| 拖拽/重新排序 | 放下时变异 |
| 设置更改 | 变异(不仅仅是 Zustand) |
# 表单没有变异(可疑)
for f in $(rg -l "onSubmit|handleSubmit" -t tsx); do
if ! rg -q "useMutation|usePost|usePut|usePatch" "$f"; then
echo "FORM WITHOUT MUTATION: $f"
fi
done
# 按钮处理程序审计
rg "onPress=|onClick=" -t tsx | head -50
3.3 Zustand 存储审计
Zustand 应该持有 客户端唯一 状态。如果存储包含:
- 应该来自 API 的数据(用户、项目、记录)
- 商业逻辑计算(应在服务器端)
- React Query 缓存的副本
# 列出所有 Zustand 存储及其状态形状
rg "interface.*State|type.*State" stores/ -t ts
# 检查持久性中间件(可能会复制 RQ 缓存)
rg "persist\(" stores/ -t ts
有效的 Zustand 使用:认证状态、UI 偏好、导航状态、草稿表单 无效:获取的实体、计算的商业数据、任何带有 API 端点的内容
第四阶段:离线行为审计
4.1 网络模式配置
# 检查查询客户端默认值
rg "networkMode" -t ts
# 检查离线检测(Expo 支持两者)
rg "NetInfo|@react-native-community/netinfo" -t ts # 社区包
rg "expo-network|Network\.getNetworkStateAsync" -t ts # Expo 本地包
rg "isConnected|isInternetReachable" -t ts
预期模式:
- 查询:
networkMode: 'offlineFirst'(离线时提供过时数据) - 变异:
networkMode: 'online'或队列实现
4.2 离线场景测试
| 场景 | 预期行为 | 检查 |
|---|---|---|
| 离线加载屏幕 | 显示缓存数据或空状态 | isLoading vs isFetching |
| 离线提交表单 | 队列或清除错误消息 | 变异错误处理 |
| 离线刷新令牌 | 优雅失败,重新连接时重试 | 拦截器错误路径 |
| 应用后台然后离线 | 缓存持久 | AsyncStorage/MMKV 检查 |
| 离线后重新连接 | 自动重新获取 | refetchOnReconnect |
# 检查离线队列实现
rg "offlineQueue|pendingMutations|syncQueue" -t ts
# 检查缓存持久性
rg "persistQueryClient|createAsyncStoragePersister|MMKV" -t ts
4.3 错误边界覆盖
# 查找错误边界
rg "ErrorBoundary|errorElement|onError" -t tsx
# 检查查询错误处理
rg "isError|error:" -t tsx | head -30
第五阶段:报告生成
按严重程度组织发现:
严重(认证/安全)
- 硬编码凭证
- 使用 AsyncStorage 的令牌(必须使用
expo-secure-store) process.env用于秘密(在 Expo 构建中不起作用)- 认证绕过可能性
- 缺少 401 处理
重大(数据完整性)
- 表单未同步到 API
- 用户操作未持久化
- 前端业务逻辑
- 提供过时数据作为新鲜数据
- API URL 未使用
expo-constants
中等(可靠性)
- 缺少错误处理
- 没有离线回退
- 竞态条件
- 缺少加载状态
轻微(代码质量)
- 直接 API 调用(应使用生成的)
- Zustand 持有服务器状态
- 不一致的模式
输出模板
Markdown 报告
# API 集成审计报告
## 执行摘要
- X 严重问题,Y 大问题,Z 中等
## 发现
### [严重] 令牌存储在 AsyncStorage 中
**文件**:`services/auth/tokenStorage.ts:15`
**问题**:JWT 令牌存储在 AsyncStorage 而不是 expo-secure-store
**修复**:迁移到 `import * as SecureStore from 'expo-secure-store'`
### [严重] 生产代码中的 process.env
**文件**:`api/client.ts:8`
**问题**:`process.env.API_URL` 在 Expo 生产构建中不起作用
**修复**:使用 expo-constants 中的 `Constants.expoConfig?.extra?.apiUrl`
### [重大] 个人资料表单未同步
**文件**:`app/profile/edit.tsx`
**问题**:表单仅保存到 Zustand,没有 API 调用
**修复**:提交时添加 `useUpdateProfile` 变异
JSON 查找
{
"summary": { "critical": 2, "major": 2, "medium": 5 },
"findings": [
{
"severity": "critical",
"category": "auth",
"file": "services/auth/tokenStorage.ts",
"line": 15,
"issue": "Tokens in AsyncStorage instead of expo-secure-store",
"fix": "Migrate to SecureStore.setItemAsync/getItemAsync"
},
{
"severity": "critical",
"category": "config",
"file": "api/client.ts",
"line": 8,
"issue": "process.env.API_URL won't work in Expo builds",
"fix": "Use Constants.expoConfig.extra.apiUrl"
}
]
}
快速参考命令
# 全面审计(grep 版本)
echo "=== 直接 fetch ===" && grep -rn "fetch(" --include="*.ts" --include="*.tsx" | grep -v node_modules | grep -v ".d.ts"
echo "=== 直接 axios ===" && grep -rn "axios\." --include="*.ts" --include="*.tsx" | grep -v node_modules | grep -v orval
echo "=== 硬编码 URL ===" && grep -rn "http://\|https://" --include="*.ts" --include="*.tsx" | grep -v node_modules
echo "=== useState 数组 ===" && grep -rn "useState\(\[" --include="*.tsx" | grep -v node_modules
echo "=== 表单 ===" && grep -rn "onSubmit" --include="*.tsx" | grep -v node_modules
# 全面审计(ripgrep 版本 - 更快)
echo "=== 直接 fetch ===" && rg "fetch\(" -t ts -t tsx --glob '!*.d.ts'
echo "=== 直接 axios ===" && rg "axios\." -t ts -t tsx --glob '!*orval*'
echo "=== 硬编码 URL ===" && rg "https?://" -t ts -t tsx
echo "=== useState 数组 ===" && rg "useState\(\[" -t tsx
echo "=== 表单 ===" && rg "onSubmit" -t tsx
依赖项
如果命令失败,安装:
# ripgrep(强烈推荐 - 比 grep 快 10 倍)
brew install ripgrep # 或 apt-get install ripgrep, cargo install ripgrep
# jq 用于 JSON 解析
brew install jq # 或 apt-get install jq
# 对于 Orval 干运行
npm install -g orval # 或使用 npx
安装
要使用这项技能与 Claude Code,将其添加到项目的 skills/ 目录:
my-expo-app/
├── app/ # Expo Router 屏幕
├── stores/
├── api/
├── skills/
│ └── expo-api-audit/
│ └── SKILL.md
├── app.config.js # Expo 配置
├── package.json
└── ...
Claude Code 自动发现此目录中的技能。安装后,可以使用提示触发审计,例如:
- “运行 API 集成审计”
- “检查我的屏幕中的硬编码数据”
- “审计我的认证令牌处理”
- “查找不同步到 API 的表单”
- “检查我是否正确使用 expo-secure-store”