提示信息
# 同伴 App · API 认证与签名规范 v1.1
> 本文档定义了前端(Flutter)与后端(PHP/Slim)之间进行 API 请求时的安全验证机制,包括公共 Header、请求签名、Token 认证及 OSS 流程。
---
## 1. 签名层 (Request Signature)
**所有请求(包含白名单接口)必须携带签名 Header。**
### 1.1 公共请求头 (Common Headers)
| Header | 类型 | 示例 | 说明 |
| :--- | :--- | :--- | :--- |
| `X-App-Key` | String | `tongban_ios_dev` | 平台标识 |
| `X-Timestamp` | Int | `1773239030` | Unix 秒级时间戳(正负 5 分钟校验) |
| `X-Nonce` | String | `3a045...` | 随机字符串(32位 UUID),防重放校验 |
| `X-Sign` | String | `f0350...` | **签名字符串**(MD5 摘要,小写) |
| `X-Device-Id` | String | `uuid-xxx` | 设备唯一硬件 ID |
| `X-OS` | String | `ios` | 操作系统:`ios` 或 `android` |
| `X-App-Version` | String | `1.0.0` | 客户端版本号 |
| `X-Language` | String | `zh-CN` | 客户端当前语言 |
| `X-Device-Risk-Flag` | String | `["safe"]` | (可选) 设备风险状态/标识符数组,JSON 格式 |
### 1.2 签名计算 (X-Sign)
1. **公式**: `X-Sign = MD5(appKey + timestamp + nonce + sortedParams + appSecret).toLowerCase()`
2. **sortedParams 规则**:
* **排序**: 将 URL 参数与 Body 参数(Json/Form)合并后按 Key 字母序排列。
* **拼接**: `key1=value1&key2=value2`(**必须使用 `&` 分隔**)。
* **嵌套对象**: value 为对象或数组时,必须先 `json_encode`(PHP)/ `jsonEncode`(Dart)序列化为 JSON 字符串后再参与拼接,**key 名使用 JSON 序列化后的字段名(下划线命名)**,不得使用语言原生的字段名(如 Dart 驼峰)。
* **例外**: `multipart/form-data` 请求的 Body 不参与签名,仅计入 URL 参数。
3. **示例** — `POST /auth/login/phone`:
```
原始 body 参数(3个 key):
phone = "17621252106"
code = "8868"
device_info = { "device_id": "383...", "platform": "ios", ... }
按 key 字母序排列后:code → device_info → phone
sortedParams =
code=8868
&device_info={"device_id":"383...","platform":"ios","model":"iPhone","os_version":"26.0.1","app_version":"1.0.0","push_token":"1d4..."}
&phone=17621252106
raw = appKey + timestamp + nonce + sortedParams + appSecret
X-Sign = MD5(raw).toLowerCase()
```
---
## 2. 认证层 (Token Authentication)
### 2.1 登录状态判断 (Whitelist)
* **白名单接口 (No Token)**: 无需携带 `Authorization` Header(如 `/app/start`, `/auth/login/phone`)。
* **认证接口 (Need Token)**: 必须携带 `Authorization: Bearer <Access_Token>`。
### 2.2 Token 错误码定义
| Code | 含义 | 处理建议 |
| :--- | :--- | :--- |
| `1001` | 安全校验失败 (签名解析失败 或 Token 缺失) | 检查 Header 是否遗漏,签名算法是否正确 |
| `1005` | Access Token 过期 | **静默刷新**: 使用 Refresh Token 调用 `/auth/token/refresh` |
| `1006` | Refresh Token 过期 / 无效 | 强制登出,跳转至登录页 |
### 2.3 Token 自动刷新流程 (Frontend)
1. 客户端请求返回 `code: 1005`。
2. Dio 拦截器捕获该错误,暂停当前请求队列。
3. 调用刷新接口获取新 Token 并保存。
4. 使用新 Token 重新发起原请求。
## 3. 风控降权 (X-Device-Risk-Flag)
### 3.1 状态枚举与触发条件
`X-Device-Risk-Flag` 的值始终为一个 JSON 编码的字符串数组(如 `["safe"]`)。
#### A. 系统状态 (Lifecycle States)
这类标识符反映了安全检测本身的运行状况:
| 标识符 | 含义 | 触发场景 |
| :--- | :--- | :--- |
| `unverified` | **未审核** | **默认初始值**。发生于:<br>1. 启动初期:SDK 尚未完成异步扫描。<br>2. 系统限制:非生产环境(Debug/Profile)或 Web/Desktop 平台。<br>3. 超时:安全检测初始化超过 5 秒未响应。 |
| `safe` | **安全** | **正常用户的理想状态**。安全扫描已完整运行且未发现下述任何风险项。 |
#### B. 风险标识符 (Risk Identifiers)
当检测到攻击行为时,数组将包含以下一项或多项(`unverified` 和 `safe` 会被移除):
| 标识符 | 含义 | 详细触发条件 |
| :--- | :--- | :--- |
| `app_integrity` | 签名/完整性异常 | 检测到 App 被二次打包、重签名或资源文件被篡改。 |
| `obfuscation` | 混淆异常 | 检测到代码混淆层级不足或被逆向工具还原。 |
| `debug` | 调试器附加 | 检测到系统层级的 Debugger 挂载(如 LLDB/GDB)或开启了非法调试开关。 |
| `device_binding` | 设备绑定异常 | 检测到 App 运行环境与其应有的设备指纹不匹配。 |
| `device_id` | Device ID 异常 | 检测到设备 ID 被劫持、伪造或通过 Hook 修改。 |
| `hooks` | Hook 框架检测 | 检测到内存中加载了 Xposed, Frida, Cydia Substrate 或相关 Hook 框架。 |
| `privileged_access` | 系统提权 | **检测到 Root (Android) 或越狱 (iOS)**。系统分区权限被篡改。 |
| `simulator` | 模拟器运行 | 检测到运行在虚拟化环境(Android Emulator, iOS Simulator, Genymotion 等)。 |
### 3.2 典型场景示例 (Scenarios)
| 场景 | Header 示例值 | 说明 |
| :--- | :--- | :--- |
| **正常用户 (App Store)** | `["safe"]` | 检测通过,环境纯净。 |
| **开发者 (调试运行)** | `["unverified"]` | 非生产环境,跳过检测。 |
| **安卓模拟器用户** | `["simulator"]` | 环境受限,后端可能限制其发布内容。 |
| **深度攻击者** | `["privileged_access", "hooks", "debug"]` | 设备已越狱,且正在使用 Hook 工具附加调试。建议后端直接封禁。 |
### 3.3 后端处理逻辑
后端接收到非 `["safe"]` 的请求时,应记录风控日志,同时对该账号执行“静默降权”,禁止在客户端弹出打断式拦截(避免告知攻击者防守细节)。
---
## 4. OSS 特殊流程
* **下载逻辑**: 使用 OSS 预签名 URL 下载时,必须使用 **“纯净”的 HTTP 客户端实例**。
* **原则**: **禁止携带 `Authorization` 头**,否则可能导致 OSS 校验 Signature 冲突返回 403。
---
## 5. 变更说明
* **2026.03.12 (v1.2)**: 修复 sortedParams 嵌套对象序列化规范。明确嵌套对象必须 `json_encode` 后参与签名,key 名使用 JSON 字段名(下划线),补充完整示例。前端对应修复:`auth_interceptor.dart` 中对 Freezed/JsonSerializable 对象调用 `toJson()` 后再 `jsonEncode`,不再依赖 `toString()`。
* **2026.03.11 (v1.1)**: 纠正”白名单”定义(白名单仅指免登录,仍需签名)。整合 Token 刷新、风控标记及 OSS 流规范。