feat:日志敏感信息过滤

This commit is contained in:
dzc committed 2026-10-29 15:46:54 +08:00
1 parent 82800373ca
commit ede0f9bc4c
10 files changed
+517 -18

No files matched your search

+1 -1
View File
@@ -143,7 +143,7 @@ abstract class AppNetworkTool {
final response = await downloadClient.send(request).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) return await response.stream.toBytes().timeout(const Duration(seconds: 30));
await response.stream.drain<void>();
if (response.statusCode != 403 || attempt > 0 || !FileAccess.client.isProtected(url)) return null;
if (response.statusCode != 403 || attempt > 0 || !FileAccess.signingEnabled || !FileAccess.client.isProtected(url)) return null;
await FileAccess.refreshClock(force: true);
}
return null;
+151 -11
View File
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import '../../values/constants.dart';
@@ -13,6 +15,45 @@ const _filePaths = {
'/api/admin/sys-file/oss/file'
};
enum FileAccessMode { signed, legacy }
/// Missing Debug credentials can simulate old clients. Invalid credentials and
/// release/profile builds still require a valid signing configuration.
FileAccessMode resolveFileAccessMode({
required String mode,
required bool debugBuild,
required bool hasConfiguration,
required String appId,
required String kid,
required String key,
}) {
if (!{'auto', 'signed', 'legacy'}.contains(mode)) {
throw StateError('Invalid FILE_ACCESS_MODE: $mode');
}
if (mode == 'legacy') {
if (!debugBuild) {
throw StateError('Legacy file access testing requires a Debug build');
}
return FileAccessMode.legacy;
}
if (mode == 'auto' && debugBuild && !hasConfiguration) {
return FileAccessMode.legacy;
}
var validKey = false;
try {
validKey = base64Decode(key).length >= 32;
} on FormatException {
// A broken configuration must not silently produce unsigned requests.
}
if (appId != 'flutter' ||
!RegExp(r'^[a-zA-Z0-9_-]{1,32}$').hasMatch(kid) ||
!validKey) {
throw StateError(
'Build with --dart-define-from-file=.file-access/config.json');
}
return FileAccessMode.signed;
}
String fileAccessCanonical(String file, String app, String kid, int ts) => [
'v1',
'oss-file-read',
@@ -32,6 +73,24 @@ String fileAccessSignature(
.toString();
}
String fileUploadCanonical(
String target, String app, String kid, int ts, String nonce) {
if (!{'upload', 'inner-upload', 'qn-upload'}.contains(target)) {
throw ArgumentError('Invalid file upload target');
}
return ['v1', 'oss-file-upload', 'POST', target, app, kid, '$ts', nonce]
.join('\n');
}
String fileUploadSignature(
String target, String app, String kid, int ts, String nonce, String key) {
final bytes = base64Decode(key);
if (bytes.length < 32) throw StateError('Invalid file upload key');
return Hmac(sha256, bytes)
.convert(utf8.encode(fileUploadCanonical(target, app, kid, ts, nonce)))
.toString();
}
/// Pure URL/signature logic. No GetX, database, device or main-engine dependency.
class FileAccessClient {
FileAccessClient(
@@ -39,14 +98,17 @@ class FileAccessClient {
required this.appId,
required this.kid,
required this.key,
this.signingEnabled = true,
this.allowedOrigins = const [],
DateTime Function()? now})
: now = now ?? DateTime.now;
final String baseUrl, appId, kid, key;
final bool signingEnabled;
final List<String> allowedOrigins;
final DateTime Function() now;
int offsetMs = 0, ttlSeconds = 1800;
final _cache = <String, ({int ts, String url})>{};
final _uploadRandom = Random.secure();
String resolve(String source) {
final value = source.trim();
@@ -102,6 +164,66 @@ class FileAccessClient {
}
bool isProtected(String source) => _owned(source) != null;
String? uploadTarget(String source) {
var uri = Uri.tryParse(resolve(source));
if (uri != null && !uri.hasScheme) uri = Uri.parse(baseUrl).resolveUri(uri);
if (uri == null ||
!{'http', 'https'}.contains(uri.scheme) ||
uri.host.isEmpty ||
uri.userInfo.isNotEmpty) {
return null;
}
final origins = [
Uri.parse(baseUrl).origin,
...allowedOrigins
.where((e) => e.isNotEmpty)
.map((e) => Uri.parse(e).origin)
];
if (!origins.contains(uri.origin)) return null;
final prefix = Uri.parse(baseUrl).path.replaceFirst(RegExp(r'/+$'), '');
final uploadPath = uri.path;
for (final entry in {
'upload': 'upload',
'inner-upload': 'inner-upload',
'qn-upload': 'qn/upload'
}.entries) {
if ([
'/sys-file/',
'/admin/sys-file/',
'/api/sys-file/',
'/api/admin/sys-file/',
'$prefix/admin/sys-file/',
'$prefix/sys-file/'
].any((start) => uploadPath == '$start${entry.value}')) {
return entry.key;
}
}
return null;
}
Map<String, String> uploadHeaders(String source, {String? nonce}) {
if (!signingEnabled) return {};
final target = uploadTarget(source);
if (target == null) return {};
final issued = (now().millisecondsSinceEpoch + offsetMs) ~/ 1000;
final requestNonce = nonce ??
List.generate(
16,
(_) => _uploadRandom
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
return {
'X-File-App-Id': appId,
'X-File-Key-Id': kid,
'X-File-Timestamp': '$issued',
'X-File-Nonce': requestNonce,
'X-File-Signature':
fileUploadSignature(target, appId, kid, issued, requestNonce, key),
};
}
String original(String source) {
final uri = _owned(source);
if (uri == null) return resolve(source);
@@ -116,6 +238,7 @@ class FileAccessClient {
}
String url(String source, {bool force = false}) {
if (!signingEnabled) return original(source);
final uri = _owned(source);
if (uri == null) return resolve(source);
final names = uri.queryParametersAll['fileName'];
@@ -156,15 +279,33 @@ class FileAccessClient {
_cache.clear();
}
bool synchronizeInfo(Map data, int roundTripMs) {
// The Java project's Long serializer returns numeric JSON strings.
final serverTime = int.tryParse('${data['serverTime']}');
final ttl = int.tryParse('${data['ttlSeconds']}');
if (serverTime == null || serverTime <= 0) return false;
synchronize(serverTime, ttl ?? ttlSeconds, roundTripMs);
return true;
}
void clear() => _cache.clear();
}
abstract class FileAccess {
static final mode = resolveFileAccessMode(
mode: Constants.fileAccessMode,
debugBuild: kDebugMode,
hasConfiguration: Constants.hasFileAccessConfiguration,
appId: Constants.fileAccessAppId,
kid: Constants.fileAccessKeyId,
key: Constants.fileAccessKey);
static bool get signingEnabled => mode == FileAccessMode.signed;
static final client = FileAccessClient(
baseUrl: Constants.ossUrl,
appId: Constants.fileAccessAppId,
kid: Constants.fileAccessKeyId,
key: Constants.fileAccessKey,
signingEnabled: signingEnabled,
allowedOrigins: Constants.fileAccessAllowedOrigins.split(','));
static final revision = ValueNotifier<int>(0);
static Timer? _timer;
@@ -177,11 +318,10 @@ abstract class FileAccess {
static void initialize() {
if (_initialized) return;
if (Constants.fileAccessAppId != 'flutter' ||
!RegExp(r'^[a-zA-Z0-9_-]{1,32}$').hasMatch(Constants.fileAccessKeyId) ||
base64Decode(Constants.fileAccessKey).length < 32) {
throw StateError(
'Build with --dart-define-from-file=.file-access/config.json');
if (!signingEnabled) {
_initialized = true;
debugPrint('[file-access] 旧版兼容测试模式:图片和上传不携带应用签名');
return;
}
_initialized = true;
WidgetsBinding.instance.addObserver(_lifecycle);
@@ -195,6 +335,7 @@ abstract class FileAccess {
}
static void resume() {
if (!signingEnabled) return;
_timer ??=
Timer.periodic(const Duration(minutes: 1), (_) => revision.value++);
unawaited(refreshClock());
@@ -206,6 +347,7 @@ abstract class FileAccess {
}
static Future<void> refreshClock({bool force = false}) {
if (!signingEnabled) return Future.value();
if (_pending != null) return _pending!;
if (!force &&
_lastSync != null &&
@@ -227,11 +369,9 @@ abstract class FileAccess {
.timeout(const Duration(seconds: 5));
if (response.statusCode != 200) return;
final data = jsonDecode(response.body)['data'];
if (data is Map && data['serverTime'] is num) {
client.synchronize(
(data['serverTime'] as num).toInt(),
(data['ttlSeconds'] as num?)?.toInt() ?? 1800,
DateTime.now().millisecondsSinceEpoch - started);
if (data is Map &&
client.synchronizeInfo(
data, DateTime.now().millisecondsSinceEpoch - started)) {
_lastSync = DateTime.now();
}
} catch (_) {
@@ -242,7 +382,7 @@ abstract class FileAccess {
}
static Future<bool> retryImage(String source) async {
if (!client.isProtected(source)) return false;
if (!signingEnabled || !client.isProtected(source)) return false;
final raw = client.original(source),
previous = _retries[client.original(source)];
if (previous != null &&
+6
View File
@@ -9,6 +9,7 @@ import 'package:get/get.dart' hide FormData;
import 'package:pj_supermarket_cloud/common/index.dart';
import 'package:pj_supermarket_cloud/common/utils/order/payment_timing_trace.dart';
import 'package:pj_supermarket_cloud/common/utils/app/file_access.dart';
/// 无需向操作员重复提示的服务端业务消息。
const ignoreMsg = ["账号未登录"];
@@ -71,6 +72,8 @@ class HttpManage {
'imagehash',
'localfilepath',
'evidencefilepath',
'xfilesignature',
'xfilenonce',
};
/// 进程内唯一的 HTTP 管理器实例,确保拦截器和连接池不被重复创建。
@@ -99,6 +102,9 @@ class HttpManage {
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (RequestOptions options, handler) async {
if (options.method.toUpperCase() == 'POST') {
options.headers.addAll(FileAccess.client.uploadHeaders(options.uri.toString()));
}
options.extra[_requestStartAtKey] = DateTime.now();
_startPaymentTiming(options);
options.headers["CLIENT-TOC"] = "Y";