安全:增加上游数据范围灰度开关
This commit is contained in:
parent
1edfa7cd17
commit
4e599c8fc0
@ -131,6 +131,7 @@
|
||||
- 必须设置连接超时、查询超时、慢查询冷却和连接失败熔断;一次失败后进入冷却,已有旧快照时继续提供本地数据,不得连续重试。
|
||||
- 上游 API 必须保留 `X-Caiwu-Data-Status` 和 `X-Caiwu-Data-Synced-At` 响应头;验收同时检查数据状态和同步时间,不能只看 HTTP 200。
|
||||
- `JIANSUN_ENABLED` 默认关闭;只允许在现有只读访问方式完成受控连通性和单查询探测后,通过服务器私有 `runtime/.env` 开启。
|
||||
- 首次启用必须同时设置 `JIANSUN_ALLOWED_SCOPES`,先开放已完成探测的小表范围;不得用 `*` 跳过核心大表的独立验证。
|
||||
- 当前方案是近实时查询快照,不得冒充完整行级增量复制。没有可靠变更标记时,禁止定时全表扫描;历史一致性只能采用低频、小批量、可中止的核对。
|
||||
- 首次全量历史数据同步必须单独评估行数、时间窗口和上游低峰期,不得在日常发布时自动执行。
|
||||
- 上游地址、账号、密码只允许存在于服务器私有环境配置;代码、提交、PR、共享文档和日志中一律不得出现真实值。
|
||||
|
||||
@ -12,6 +12,12 @@ return [
|
||||
*/
|
||||
'enabled' => filter_var(env('JIANSUN_ENABLED', false), FILTER_VALIDATE_BOOL),
|
||||
|
||||
// 生产首次启用时从小表范围开始;支持精确 scope、prefix.* 和 *。
|
||||
'allowed_scopes' => array_values(array_filter(array_map(
|
||||
'trim',
|
||||
explode(',', (string) env('JIANSUN_ALLOWED_SCOPES', ''))
|
||||
))),
|
||||
|
||||
'connection' => [
|
||||
'host' => env('DB_JIANSUN_HOST'),
|
||||
'port' => (int) env('DB_JIANSUN_PORT', 1433),
|
||||
|
||||
@ -59,6 +59,7 @@ if [ ! -f "$REMOTE_DIR/runtime/.env" ]; then
|
||||
'SESSION_DRIVER=file' \
|
||||
'QUEUE_CONNECTION=sync' \
|
||||
'JIANSUN_ENABLED=false' \
|
||||
'JIANSUN_ALLOWED_SCOPES=' \
|
||||
'MARIADB_DATABASE=caiwu' \
|
||||
'MARIADB_USER=caiwu' \
|
||||
"MARIADB_PASSWORD=$DB_PASSWORD" \
|
||||
|
||||
@ -156,6 +156,7 @@ git pull --ff-only
|
||||
|
||||
```dotenv
|
||||
JIANSUN_ENABLED=false
|
||||
JIANSUN_ALLOWED_SCOPES=
|
||||
DB_JIANSUN_HOST=
|
||||
DB_JIANSUN_PORT=1433
|
||||
DB_JIANSUN_DATABASE=
|
||||
@ -177,6 +178,8 @@ JIANSUN_SLOW_QUERY_MS=5000
|
||||
JIANSUN_SLOW_QUERY_COOLDOWN=300
|
||||
```
|
||||
|
||||
`JIANSUN_ALLOWED_SCOPES` 支持逗号分隔的精确范围和 `prefix.*`。首次上线先开放完成探测的小表范围,例如 `invoice-deductions.*`;约 280 万行核心表对应范围必须单独验证,未验证前不得填写 `*`。
|
||||
|
||||
这套策略提供的是近实时查询快照,不等同于复制上游全库。对于没有可靠更新时间或删除标记的旧表,不能声称实现了完整行级增量;禁止为了追求秒级实时而周期性扫描数百万行。页面实时性依靠本地快照和一分钟后台刷新,历史一致性只能通过低频、小批量、可随时停止的核对补齐。
|
||||
|
||||
## 9. 禁止事项
|
||||
|
||||
@ -52,6 +52,10 @@ class JianSunQueryCache
|
||||
return $stale['value'];
|
||||
}
|
||||
|
||||
if (! $this->isScopeAllowed($scope)) {
|
||||
throw new ServiceUnavailableHttpException(30, '该数据范围仍在低压力验证中,请稍后再试。');
|
||||
}
|
||||
|
||||
// 首次访问没有任何本地数据时才允许同步获取一次;先登记,失败后后台
|
||||
// 仍可在熔断结束后继续补齐快照。
|
||||
$this->register($scope, $parameters, true);
|
||||
@ -86,6 +90,10 @@ class JianSunQueryCache
|
||||
{
|
||||
$parameters = $this->normalize($parameters);
|
||||
|
||||
if (! $this->isScopeAllowed($scope)) {
|
||||
return 'stale';
|
||||
}
|
||||
|
||||
return $this->resolve($scope, $parameters, $resolver, true)['status'];
|
||||
}
|
||||
|
||||
@ -105,6 +113,7 @@ class JianSunQueryCache
|
||||
$entries = array_filter($entries, function (mixed $entry) use ($now, $activeTtl): bool {
|
||||
return is_array($entry)
|
||||
&& isset($entry['seen_at'], $entry['scope'], $entry['parameters'])
|
||||
&& $this->isScopeAllowed((string) $entry['scope'])
|
||||
&& ($now - (int) $entry['seen_at']) <= $activeTtl
|
||||
&& $this->isDue($entry, $now);
|
||||
});
|
||||
@ -380,6 +389,25 @@ class JianSunQueryCache
|
||||
return 'jiansun:query-cooldown:' . $queryId;
|
||||
}
|
||||
|
||||
private function isScopeAllowed(string $scope): bool
|
||||
{
|
||||
$allowed = (array) config('jiansun.allowed_scopes', []);
|
||||
|
||||
foreach ($allowed as $pattern) {
|
||||
$pattern = trim((string) $pattern);
|
||||
|
||||
if ($pattern === '*' || $pattern === $scope) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_ends_with($pattern, '.*') && str_starts_with($scope, substr($pattern, 0, -1))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function entryKey(string $scope, array $parameters): string
|
||||
{
|
||||
return hash('sha256', $scope . '|' . json_encode($parameters, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
@ -31,6 +31,7 @@ class JianSunQueryCacheTest extends TestCase
|
||||
config()->set('jiansun.snapshots.slot_wait_ms', 0);
|
||||
config()->set('jiansun.snapshots.lock_wait_seconds', 1);
|
||||
config()->set('jiansun.snapshots.slow_query_ms', 5000);
|
||||
config()->set('jiansun.allowed_scopes', ['*']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
@ -131,4 +132,22 @@ class JianSunQueryCacheTest extends TestCase
|
||||
$slot1->release();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_disallowed_cold_scope_never_calls_upstream(): void
|
||||
{
|
||||
config()->set('jiansun.allowed_scopes', ['invoice-deductions.*']);
|
||||
$cache = app(JianSunQueryCache::class);
|
||||
$calls = 0;
|
||||
|
||||
try {
|
||||
$cache->remember('goods.list', [], function () use (&$calls): array {
|
||||
$calls++;
|
||||
|
||||
return [];
|
||||
});
|
||||
$this->fail('未开放的数据范围不应进入冷查询。');
|
||||
} catch (ServiceUnavailableHttpException) {
|
||||
$this->assertSame(0, $calls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user