feat:日志敏感信息过滤
This commit is contained in:
1 parent
82800373ca
commit
ede0f9bc4c
10 files changed
+517
-18
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user