# 车辆数据中台客户 Scope API 覆盖矩阵 更新时间:2026-07-14 状态:安全审计与实施设计;尚未开放客户主体。 ## 1. 当前结论 车辆中台当前的 Bearer Token 只表达 `viewer/operator/admin` 功能角色,不表达客户或车辆数据范围。 因此,即使 OneOS 已能登录客户,也不能直接把客户 token 映射成 `viewer`:这会让客户读取全量车辆、轨迹、 告警、统计及其他人的导出文件。 2026-07-14 已只读核验 ECS 当前运行状态: - `AUTH_MODE=enforce`; - 使用 `AUTH_TOKENS_JSON`,未使用单一 `AUTH_TOKEN`; - `/opt/lingniu-vehicle-platform/env/platform.env` 权限为 `0600 root:root`; - `lingniu-vehicle-platform` 服务已启用且正在运行。 该配置能阻止匿名访问普通平台 API,但不能提供客户级隔离。 ## 2. 独立发现:高德服务端接口绕过平台鉴权 `app.NewServer` 当前先把平台 API 包在 `withAPIAuth` 中,之后又在外层注册 `withAMapReverseGeocodeAPI`。所以 `/api/map/reverse-geocode` 在进入鉴权中间件前就会被处理;只要配置了 `AMAP_API_KEY`,匿名请求即可消耗服务端高德额度。 整改要求: - 将逆地理编码路由放回鉴权之后; - 对客户和内部主体分别限流,并限制经纬度格式和并发; - 不记录完整 access token; - `app-config.js` 可继续公开,但只能暴露高德 Web JS 公钥和代理路径,不暴露服务端 API Key; - 高德安全代理因浏览器 SDK 需要可访问,应在边缘层配置域名来源、速率和响应缓存,不能把它视为客户身份接口。 ## 3. 主体与权限模型 `Principal` 至少扩展为: ```text name role # 仅内部功能角色 subject_type # employee | service | customer subject_id # OneOS sys_user.user_id customer_id # 仅 customer 必填 tenant_id identity_version capabilities # vehicle:read, history:read, export:create ... ``` 约束: - 内部静态 token 只能生成 `employee/service` 主体; - 客户主体只能来自 OneOS 签名、短时且 `aud=vehicle-data-platform` 的声明; - 浏览器 Query、Body、Cookie 或普通转发头中的 `customerId` 均无效; - 客户权限不能通过 `roleRank(viewer)` 复用内部 viewer 权限,需要独立 capability 判断; - 未知 `subject_type`、缺失 customer ID、签名异常或身份版本失效全部拒绝。 ## 4. QueryScope 机制 所有 Store 查询显式接收 `QueryScope`,避免新增方法时忘记从 `context.Context` 读取 Scope: ```text mode: unrestricted | customer_current | customer_period customer_id active_version valid_from / valid_to # 历史持有期查询 ``` `unrestricted` 只能由已验证的内部 employee/service 主体创建。空客户 Scope 表示“零车辆”,绝不能解释为不限制。 MySQL 当前数据查询应 JOIN: ```text business_scope_state(id=1) business_customer_vehicle_scope( source_version = active_version, customer_id = principal.customer_id, vin = 业务表.vin ) ``` 同时要求: - `last_success_at` 距当前时间不超过 10 分钟; - active version 存在且快照计数完整; - 快照缺失或过期返回 `503 BUSINESS_SCOPE_UNAVAILABLE`,而不是空列表或全量数据; - 快照新鲜但该客户确实没有车辆时返回空列表; - 单车越权统一返回 404,避免泄露 VIN 是否存在。 TDengine 无法直接 JOIN MySQL Scope:单车查询先在 MySQL 做授权判断;多车查询从同一 active version 读取授权 VIN 集合,设置数量上限后再生成参数化过滤。不能先从 TDengine 取全量再由前端过滤。 ## 5. API 覆盖矩阵 ### 5.1 客户首期允许,但必须按当前 Scope 过滤 | API 组 | 代表路由 | 必须实施的控制 | | --- | --- | --- | | 全局监控 | `/api/v2/monitor/summary`、`/map` | 车辆、点位、聚合、告警数和所有汇总均从同一客户 Scope 计算;禁止混用全局 DashboardSummary | | 车辆列表与解析 | `/api/vehicles`、`/resolve`、`/detail` | VIN、车牌、终端号解析必须先限定 Scope;越权关键字表现为不存在 | | 最新状态 | `/api/realtime/vehicles`、`/locations` | 列表、total、分页和筛选均在 SQL 层过滤 | | 车辆只读档案 | `GET /api/v2/vehicles/{vin}/profile` | 先校验 VIN;只返回门户需要的非敏感字段 | | 最新遥测 | `/api/v2/vehicles/{vin}/telemetry/latest` | 路径 VIN 校验 Scope;原始协议帧字段按白名单输出 | | 轨迹 | `/api/v2/tracks` | 车牌/VIN/终端解析、轨迹点、停留点和统计都使用同一授权 VIN | | 历史查询 | `/api/history/locations`、`/api/v2/history/query`、`/series` | 首期只允许当前车辆且 eventAt 不早于 `scope_start_at`;不能读取交付前数据 | | 里程 | `/api/mileage/summary`、`/daily`、`/api/v2/statistics/mileage` | 列表与汇总按 VIN 和授权时间过滤,total 不能泄露全局数量 | | 在线统计 | `/api/statistics/online-summary`、`/online-vehicles` | 分母、在线数、离线数、趋势和明细均限定客户车辆 | | 告警只读 | `/api/v2/alerts/summary`、`/events`、`/events/{id}` | 列表 JOIN event.vin Scope;详情按 ID+Scope 查询,越权返回 404 | | 指标元数据 | `/api/v2/metrics`、`/api/v2/history/metrics` | 可返回全局字段定义,但过滤内部专用/敏感指标 | | 逆地理编码 | `/api/map/reverse-geocode` | 必须鉴权和限流;不依赖客户参数,不返回其他车辆信息 | ### 5.2 客户首期明确禁止 | API 组 | 路由 | 原因 | | --- | --- | --- | | 平台总览 | `/api/dashboard/summary` | 包含全局车辆、帧量、Kafka/服务健康信息 | | 接入诊断 | `/api/vehicles/coverage*`、`/api/vehicle-service*` | 包含协议覆盖、终端、原始帧和数据质量诊断 | | 原始帧 | `/api/history/raw-frames*` | 包含协议证据和内部字段;首期使用白名单遥测/历史 API 代替 | | 质量中心 | `/api/quality/*`、旧 `/api/alert-events/*` 别名 | 平台级质量问题,不是客户业务告警 | | 运维 | `/api/ops/*` | 数据源、容量、Kafka、Redis 和服务健康仅内部可见 | | 接入管理 | `/api/v2/access/*` | 身份绑定、阈值和未解析终端均为内部运维能力 | | 车辆档案写入 | `PUT /api/v2/vehicles/{vin}/profile`、`/vehicle-profiles/sync` | 只允许内部管理员/同步服务 | | 告警规则/处置 | `/api/v2/alerts/rules*`、`events/{id}/actions` | 首期客户只读,不能修改平台规则或全局事件状态 | | 平台通知 | `/api/v2/alerts/notifications*` | 当前通知没有客户 owner,读取与已读操作均可能跨客户 | 禁止应在服务端 capability 层返回 403,不能只隐藏前端菜单。 ### 5.3 导出必须单独重构后才能开放 当前导出实现有四个 P0 问题: 1. `CreateHistoryExport` 没有 `context`,创建时无法固化主体; 2. `HistoryExportJob` 没有 owner/customer/scope version; 3. `ListHistoryExports` 返回全局任务; 4. `HistoryExportFile(id)` 只校验 ID 和完成状态,知道 ID 即可下载。 整改要求: - `Create/List/Download` 全部接收 context 和 Principal; - Job 固化 `owner_subject_id`、`customer_id`、`scope_version`、已解析 VIN、时间范围和审计 request ID; - 创建任务时校验每个 VIN,异步执行前再次校验; - 列表只返回当前 owner;下载同时校验 owner 和当前授权,越权返回 404; - 客户停用、车辆授权撤销或 Scope 失效时停止未完成任务并拒绝下载; - 文件名和 CSV 元数据不得包含其他客户信息;文件按 owner 隔离目录并设置 `0640`; - 定期清理过期文件与 jobs 记录。 在上述改造完成前,客户主体不授予 `export:create` 或 `export:download`。 ## 6. 当前 Scope 与历史 Scope 的边界 已实现的 `business_customer_vehicle_scope` 只保存当前有效车辆。它足以保护实时监控,但不能完整实现“客户还车后 仍可查看持有期间历史”。 首期安全策略: - 只允许查询当前仍在 Scope 中的车辆; - 历史起点不得早于当前记录的 `scope_start_at`; - 车辆还车并从当前 Scope 移除后,历史访问也随即关闭。 若产品确认需要持有期历史,再新增版本化区间投影: ```text customer_id, vin, contract_id, valid_from, valid_to, source_version ``` 历史查询必须满足 VIN 和 `valid_from <= event_at < valid_to`。不能仅凭“客户曾经拥有过这辆车”开放该 VIN 的全部历史。 ## 7. 必须覆盖的越权测试 准备客户 A、客户 B、内部 viewer、内部 operator、内部 admin 和 service 六类主体,至少覆盖: 1. A 在列表、关键字解析、路径 VIN、POST body、分页游标中请求 B 的车辆; 2. A 直接访问 B 的告警事件 ID、导出 ID 和下载 URL; 3. A 使用车牌或终端号绕过 VIN 校验; 4. A 的空 Scope 不返回全量;Scope 缺失/过期返回 503; 5. 快照切换期间所有查询只使用一个 active version,不混合新旧结果; 6. total、summary、地图聚合数量和在线率不泄露全局或 B 的数量; 7. A 只能读取交付后的历史点,不能读取交付前记录; 8. 车辆从 A 还车并交付给 B 后,A 的实时访问立即 404,B 只能读取自身授权时间段; 9. 客户主体访问运维、接入、质量、写档案、规则和处置接口均为 403; 10. 匿名逆地理编码失败;认证请求达到限额后返回 429; 11. 客户 token 的 customer ID 不能被 Query/Header/Body 覆盖; 12. 服务重启后导出 owner、Scope 和下载授权仍保持,不能退化成全局任务。 ## 8. 推荐实施顺序 1. 扩展 Principal 和 OneOS JWT 校验,但暂不开放客户路由; 2. 引入强类型 QueryScope,先改车辆解析与单车详情; 3. 改所有列表、汇总、地图、统计和 TDengine 查询; 4. 改告警详情 IDOR、逆地理编码鉴权限流; 5. 重构导出 owner 与异步 Scope; 6. 完成双客户自动化测试和生产影子查询; 7. 最后为测试客户启用 capabilities,默认保持客户入口关闭。