
// 简报
在移动应用开发中,安全绝不应是事后才考虑的问题。随着网络威胁日益增加,加上 GDPR 和 CCPA 等严格的隐私法规,保护用户数据既是法律要求,也是建立信任的关键。
本指南全面涵盖 Flutter 应用的必备安全实践,从基础数据保护到高级威胁缓解。无论你正在开发简单的工具应用,还是复杂的金融科技方案,这些实践都能帮助你打造安全、值得信赖的应用。
数据加密与安全存储
无论是静态存储还是传输中的敏感数据,都必须妥善保护。切勿以明文存储敏感信息,传输前务必先加密。
secure_storage.dart
// Secure Storage Implementation
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';
class SecureStorageService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
sharedPreferencesName: 'secure_prefs',
preferencesKeyPrefix: 'app_',
),
iOptions: IOSOptions(
groupId: 'com.yourapp.security',
accessibility: IOSAccessibility.first_unlock_this_device,
synchronizable: false,
),
);
// Store encrypted sensitive data
static Future<void> storeSecureData(String key, String value) async {
try {
final encryptedValue = _encryptData(value);
await _storage.write(key: key, value: encryptedValue);
} catch (e) {
throw SecurityException('Failed to store secure data: $e');
}
}
// Retrieve and decrypt sensitive data
static Future<String?> getSecureData(String key) async {
try {
final encryptedValue = await _storage.read(key: key);
if (encryptedValue == null) return null;
return _decryptData(encryptedValue);
} catch (e) {
throw SecurityException('Failed to retrieve secure data: $e');
}
}
// Store authentication tokens securely
static Future<void> storeAuthToken(String token) async {
await storeSecureData('auth_token', token);
await storeSecureData('token_timestamp', DateTime.now().toIso8601String());
}
// Check if token is valid and not expired
static Future<bool> isTokenValid() async {
final token = await getSecureData('auth_token');
final timestampStr = await getSecureData('token_timestamp');
if (token == null || timestampStr == null) return false;
final timestamp = DateTime.parse(timestampStr);
final now = DateTime.now();
const tokenLifetime = Duration(hours: 24);
return now.difference(timestamp) < tokenLifetime;
}
// Clear all sensitive data
static Future<void> clearAllSecureData() async {
await _storage.deleteAll();
}
// Basic encryption for additional security layer
static String _encryptData(String data) {
final bytes = utf8.encode(data);
final digest = sha256.convert(bytes);
return base64.encode(bytes); // In production, use proper encryption
}
static String _decryptData(String encryptedData) {
final bytes = base64.decode(encryptedData);
return utf8.decode(bytes);
}
}
// Advanced Encryption Service
import 'package:pointycastle/export.dart';
class AdvancedEncryptionService {
static final _key = _generateKey();
static final _iv = _generateIV();
static Uint8List encrypt(String plainText) {
final plainBytes = Uint8List.fromList(utf8.encode(plainText));
final cipher = AESFastEngine();
final cbcCipher = CBCBlockCipher(cipher);
final paddedCipher = PaddedBlockCipher('AES/CBC/PKCS7');
paddedCipher.init(true, PaddedBlockCipherParameters(
ParametersWithIV(KeyParameter(_key), _iv),
null,
));
return paddedCipher.process(plainBytes);
}
static String decrypt(Uint8List cipherBytes) {
final cipher = AESFastEngine();
final cbcCipher = CBCBlockCipher(cipher);
final paddedCipher = PaddedBlockCipher('AES/CBC/PKCS7');
paddedCipher.init(false, PaddedBlockCipherParameters(
ParametersWithIV(KeyParameter(_key), _iv),
null,
));
final decrypted = paddedCipher.process(cipherBytes);
return utf8.decode(decrypted);
}
static Uint8List _generateKey() {
// In production, derive from secure random or user password
final keyBytes = Uint8List(32);
final secureRandom = SecureRandom('Fortuna');
secureRandom.seed(KeyParameter(Uint8List.fromList('your-secret-key'.codeUnits)));
for (int i = 0; i < keyBytes.length; i++) {
keyBytes[i] = secureRandom.nextUint8();
}
return keyBytes;
}
static Uint8List _generateIV() {
final ivBytes = Uint8List(16);
final secureRandom = SecureRandom('Fortuna');
for (int i = 0; i < ivBytes.length; i++) {
ivBytes[i] = secureRandom.nextUint8();
}
return ivBytes;
}
}存储应做事项
- 使用 Flutter Secure Storage 存储 token
- 敏感数据先加密后存储
- 实施密钥轮换策略
- 为数据设置过期时间戳
存储禁忌
- 切勿以明文存储密码
- 不要用 SharedPreferences 存储机密数据
- 避免硬编码加密密钥
- 个人身份信息(PII)必须加密存储
API 安全与身份验证
安全的 API 通信对保护传输中的数据至关重要。实施妥善的身份验证、授权及请求签名,防止未经授权的访问。
secure_api_client.dart
// Secure API Client Implementation
import 'package:dio/dio.dart';
import 'package:dio_certificate_pinning/dio_certificate_pinning.dart';
import 'package:crypto/crypto.dart';
class SecureApiClient {
late final Dio _dio;
final String _baseUrl;
final String _apiKey;
SecureApiClient({
required String baseUrl,
required String apiKey,
}) : _baseUrl = baseUrl, _apiKey = apiKey {
_setupDio();
}
void _setupDio() {
_dio = Dio();
// Certificate pinning for enhanced security
_dio.interceptors.add(
CertificatePinningInterceptor(
allowedSHAFingerprints: [
'YOUR_SERVER_CERTIFICATE_SHA256_FINGERPRINT',
],
),
);
// Request/Response interceptor for security
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Add security headers
options.headers['Content-Type'] = 'application/json';
options.headers['User-Agent'] = 'YourApp/1.0.0';
options.headers['X-Requested-With'] = 'XMLHttpRequest';
// Add timestamp and nonce for replay attack prevention
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final nonce = _generateNonce();
options.headers['X-Timestamp'] = timestamp;
options.headers['X-Nonce'] = nonce;
// Add request signature
final signature = _signRequest(
method: options.method,
path: options.path,
timestamp: timestamp,
nonce: nonce,
body: options.data?.toString() ?? '',
);
options.headers['X-Signature'] = signature;
handler.next(options);
},
onResponse: (response, handler) {
// Verify response signature if provided
final responseSignature = response.headers['x-response-signature']?.first;
if (responseSignature != null) {
if (!_verifyResponseSignature(response.data, responseSignature)) {
throw DioError(
requestOptions: response.requestOptions,
error: 'Invalid response signature',
type: DioErrorType.other,
);
}
}
handler.next(response);
},
onError: (error, handler) {
_handleApiError(error);
handler.next(error);
},
),
);
}
// Authenticated GET request
Future<Map<String, dynamic>> get(String endpoint) async {
try {
final token = await SecureStorageService.getSecureData('auth_token');
if (token == null) throw UnauthorizedException('No auth token found');
final response = await _dio.get(
'$_baseUrl$endpoint',
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
return response.data;
} catch (e) {
throw _handleApiException(e);
}
}
// Authenticated POST request with body encryption
Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
try {
final token = await SecureStorageService.getSecureData('auth_token');
if (token == null) throw UnauthorizedException('No auth token found');
// Encrypt sensitive data in request body
final encryptedData = _encryptRequestBody(data);
final response = await _dio.post(
'$_baseUrl$endpoint',
data: encryptedData,
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Content-Encoding': 'encrypted',
},
),
);
return response.data;
} catch (e) {
throw _handleApiException(e);
}
}
// OAuth 2.0 / JWT Authentication
Future<void> authenticate(String username, String password) async {
try {
// Never send passwords in plain text
final hashedPassword = _hashPassword(password);
final response = await _dio.post(
'$_baseUrl/auth/login',
data: {
'username': username,
'password': hashedPassword,
'client_id': 'your_client_id',
'grant_type': 'password',
},
);
final accessToken = response.data['access_token'];
final refreshToken = response.data['refresh_token'];
await SecureStorageService.storeAuthToken(accessToken);
await SecureStorageService.storeSecureData('refresh_token', refreshToken);
} catch (e) {
throw AuthenticationException('Authentication failed: $e');
}
}
// Token refresh mechanism
Future<void> refreshToken() async {
final refreshToken = await SecureStorageService.getSecureData('refresh_token');
if (refreshToken == null) throw UnauthorizedException('No refresh token');
try {
final response = await _dio.post(
'$_baseUrl/auth/refresh',
data: {
'refresh_token': refreshToken,
'grant_type': 'refresh_token',
},
);
final newAccessToken = response.data['access_token'];
await SecureStorageService.storeAuthToken(newAccessToken);
} catch (e) {
// Clear tokens if refresh fails
await SecureStorageService.clearAllSecureData();
throw AuthenticationException('Token refresh failed: $e');
}
}
String _signRequest({
required String method,
required String path,
required String timestamp,
required String nonce,
required String body,
}) {
final message = '$method$path$timestamp$nonce$body';
final key = utf8.encode(_apiKey);
final messageBytes = utf8.encode(message);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
return digest.toString();
}
String _generateNonce() {
final random = Random.secure();
final values = List<int>.generate(16, (i) => random.nextInt(256));
return base64.encode(values);
}
Map<String, dynamic> _encryptRequestBody(Map<String, dynamic> data) {
// Encrypt sensitive fields
final sensitiveFields = ['password', 'ssn', 'credit_card', 'personal_data'];
final encryptedData = Map<String, dynamic>.from(data);
for (final field in sensitiveFields) {
if (encryptedData.containsKey(field)) {
final plainText = encryptedData[field].toString();
encryptedData[field] = base64.encode(
AdvancedEncryptionService.encrypt(plainText),
);
}
}
return encryptedData;
}
String _hashPassword(String password) {
// Use bcrypt or Argon2 in production
final salt = 'your_app_salt'; // Use proper salt generation
final combined = password + salt;
final digest = sha256.convert(utf8.encode(combined));
return digest.toString();
}
}API 安全清单:
- 实施证书绑定(certificate pinning)
- 使用 HMAC 签名验证请求
- 加入时间戳及 nonce 防止重放攻击
- 加密请求及响应中的敏感数据
- 实施妥善的 token 刷新机制
生物识别验证与设备安全
实施生物识别验证及设备安全检查,确保应用只在可信任的设备上运行,并提供安全的用户验证。
biometric_auth_service.dart
// Biometric Authentication Service
import 'package:local_auth/local_auth.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart';
class BiometricAuthService {
static final LocalAuthentication _localAuth = LocalAuthentication();
// Check if biometric authentication is available
static Future<bool> isBiometricAvailable() async {
try {
final isAvailable = await _localAuth.canCheckBiometrics;
final isDeviceSupported = await _localAuth.isDeviceSupported();
return isAvailable && isDeviceSupported;
} catch (e) {
return false;
}
}
// Get available biometric types
static Future<List<BiometricType>> getAvailableBiometrics() async {
try {
return await _localAuth.getAvailableBiometrics();
} catch (e) {
return [];
}
}
// Authenticate with biometrics
static Future<bool> authenticateWithBiometrics({
required String reason,
bool stickyAuth = true,
}) async {
try {
final isAvailable = await isBiometricAvailable();
if (!isAvailable) return false;
return await _localAuth.authenticate(
localizedReason: reason,
authMessages: const [
AndroidAuthMessages(
signInTitle: 'Biometric Authentication',
biometricHint: 'Verify your identity',
biometricNotRecognized: 'Biometric not recognized',
biometricRequiredTitle: 'Biometric Required',
biometricSuccess: 'Authentication successful',
cancelButton: 'Cancel',
deviceCredentialsRequiredTitle: 'Device credentials required',
deviceCredentialsSetupDescription: 'Please set up device credentials',
goToSettingsButton: 'Go to Settings',
goToSettingsDescription: 'Please set up biometric authentication',
),
IOSAuthMessages(
lockOut: 'Biometric authentication is disabled',
goToSettingsButton: 'Go to Settings',
goToSettingsDescription: 'Please set up biometric authentication',
cancelButton: 'Cancel',
),
],
options: AuthenticationOptions(
stickyAuth: stickyAuth,
biometricOnly: false, // Allow fallback to device credentials
),
);
} catch (e) {
return false;
}
}
// Enhanced authentication with retry logic
static Future<AuthResult> authenticateWithRetry({
required String reason,
int maxRetries = 3,
}) async {
int attempts = 0;
while (attempts < maxRetries) {
try {
final success = await authenticateWithBiometrics(reason: reason);
if (success) {
return AuthResult.success();
}
attempts++;
} catch (e) {
if (e is PlatformException) {
switch (e.code) {
case 'UserCancel':
return AuthResult.cancelled();
case 'BiometricOnlyNotSupported':
return AuthResult.fallbackRequired();
case 'DeviceNotSupported':
return AuthResult.notSupported();
default:
attempts++;
}
}
}
}
return AuthResult.failed('Maximum authentication attempts exceeded');
}
}
// Device Security Checks
class DeviceSecurityService {
static final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();
// Check if device is rooted/jailbroken
static Future<bool> isDeviceCompromised() async {
try {
if (Platform.isAndroid) {
return await _checkAndroidRootedDevice();
} else if (Platform.isIOS) {
return await _checkIOSJailbrokenDevice();
}
return false;
} catch (e) {
return true; // Assume compromised if check fails
}
}
static Future<bool> _checkAndroidRootedDevice() async {
final androidInfo = await _deviceInfo.androidInfo;
// Check for common root indicators
final rootIndicators = [
'/system/app/Superuser.apk',
'/sbin/su',
'/system/bin/su',
'/system/xbin/su',
'/data/local/xbin/su',
'/data/local/bin/su',
'/system/sd/xbin/su',
'/system/bin/failsafe/su',
'/data/local/su',
];
for (final path in rootIndicators) {
if (await File(path).exists()) {
return true;
}
}
// Check for root apps
try {
await Process.run('which', ['su']);
return true;
} catch (e) {
// su command not found, likely not rooted
}
return false;
}
static Future<bool> _checkIOSJailbrokenDevice() async {
final iosInfo = await _deviceInfo.iosInfo;
// Check for jailbreak indicators
final jailbreakPaths = [
'/Applications/Cydia.app',
'/Library/MobileSubstrate/MobileSubstrate.dylib',
'/bin/bash',
'/usr/sbin/sshd',
'/etc/apt',
'/private/var/lib/apt/',
];
for (final path in jailbreakPaths) {
if (await File(path).exists()) {
return true;
}
}
return false;
}
// Check if app is running in debug mode
static bool isDebugMode() {
return kDebugMode;
}
// Verify app integrity
static Future<bool> verifyAppIntegrity() async {
try {
// Check app signature (platform-specific implementation required)
// This is a simplified check
final packageInfo = await PackageInfo.fromPlatform();
// Verify package name and version
const expectedPackageName = 'com.yourapp.package';
if (packageInfo.packageName != expectedPackageName) {
return false;
}
return true;
} catch (e) {
return false;
}
}
// Complete security check
static Future<SecurityCheckResult> performSecurityCheck() async {
final results = await Future.wait([
isDeviceCompromised(),
BiometricAuthService.isBiometricAvailable(),
verifyAppIntegrity(),
]);
final isCompromised = results[0] as bool;
final biometricAvailable = results[1] as bool;
final integrityValid = results[2] as bool;
return SecurityCheckResult(
isDeviceCompromised: isCompromised,
isBiometricAvailable: biometricAvailable,
isAppIntegrityValid: integrityValid,
isDebugMode: isDebugMode(),
);
}
}
// Result classes
class AuthResult {
final bool isSuccess;
final String? error;
final AuthResultType type;
AuthResult._(this.isSuccess, this.error, this.type);
factory AuthResult.success() => AuthResult._(true, null, AuthResultType.success);
factory AuthResult.failed(String error) => AuthResult._(false, error, AuthResultType.failed);
factory AuthResult.cancelled() => AuthResult._(false, 'User cancelled', AuthResultType.cancelled);
factory AuthResult.notSupported() => AuthResult._(false, 'Not supported', AuthResultType.notSupported);
factory AuthResult.fallbackRequired() => AuthResult._(false, 'Fallback required', AuthResultType.fallbackRequired);
}
enum AuthResultType { success, failed, cancelled, notSupported, fallbackRequired }
class SecurityCheckResult {
final bool isDeviceCompromised;
final bool isBiometricAvailable;
final bool isAppIntegrityValid;
final bool isDebugMode;
SecurityCheckResult({
required this.isDeviceCompromised,
required this.isBiometricAvailable,
required this.isAppIntegrityValid,
required this.isDebugMode,
});
bool get isSecure => !isDeviceCompromised && isAppIntegrityValid && !isDebugMode;
}设备安全功能:
- 支持后备方案的生物识别验证
- Root/越狱检测
- 应用完整性验证
- 调试模式检测
常见漏洞与防范
了解常见安全漏洞,有助你主动保护应用。以下是最关键的威胁及相应的防范方法。
常见威胁
- 中间人攻击
- 通过 API 的 SQL 注入
- 逆向工程
- 日志泄露数据
- 不安全的数据传输
- 代码篡改
防范措施
- 证书绑定(certificate pinning)
- 输入验证与清理
- 代码混淆(obfuscation)
- 安全的日志记录实践
- 端到端加密
- 应用完整性检查
security_utils.dart
// Security Utilities and Best Practices
class SecurityUtils {
// Secure logging that prevents sensitive data leakage
static void secureLog(String message, {LogLevel level = LogLevel.info}) {
if (kReleaseMode) {
// In production, only log non-sensitive information
final sanitizedMessage = _sanitizeLogMessage(message);
_logToSecureEndpoint(sanitizedMessage, level);
} else {
// In debug mode, allow detailed logging
developer.log(message, level: level.value);
}
}
static String _sanitizeLogMessage(String message) {
// Remove sensitive patterns
final sensitivePatterns = [
RegExp(r'password[=:]s*S+', caseSensitive: false),
RegExp(r'token[=:]s*S+', caseSensitive: false),
RegExp(r'api[_-]?key[=:]s*S+', caseSensitive: false),
RegExp(r'd{4}[s-]?d{4}[s-]?d{4}[s-]?d{4}'), // Credit card numbers
RegExp(r'd{3}-d{2}-d{4}'), // SSN
];
String sanitized = message;
for (final pattern in sensitivePatterns) {
sanitized = sanitized.replaceAll(pattern, '[REDACTED]');
}
return sanitized;
}
// Input validation and sanitization
static String sanitizeInput(String input) {
// Remove potentially dangerous characters
return input
.replaceAll(RegExp(r'[<>"']'), '')
.replaceAll(RegExp(r'script', caseSensitive: false), '')
.trim();
}
// Email validation with security considerations
static bool isValidEmail(String email) {
final emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$');
return emailRegex.hasMatch(email) && email.length <= 254;
}
// Password strength validation
static PasswordStrength checkPasswordStrength(String password) {
int score = 0;
final feedback = <String>[];
if (password.length >= 8) score += 1;
else feedback.add('Use at least 8 characters');
if (RegExp(r'[a-z]').hasMatch(password)) score += 1;
else feedback.add('Include lowercase letters');
if (RegExp(r'[A-Z]').hasMatch(password)) score += 1;
else feedback.add('Include uppercase letters');
if (RegExp(r'd').hasMatch(password)) score += 1;
else feedback.add('Include numbers');
if (RegExp(r'[!@#$&*~]').hasMatch(password)) score += 1;
else feedback.add('Include special characters');
// Check against common passwords
if (_isCommonPassword(password)) {
score = 0;
feedback.add('Password is too common');
}
return PasswordStrength(score: score, feedback: feedback);
}
static bool _isCommonPassword(String password) {
final commonPasswords = [
'password', '123456', 'password123', 'admin', 'qwerty',
'letmein', 'welcome', 'monkey', '1234567890', 'dragon'
];
return commonPasswords.contains(password.toLowerCase());
}
// Generate secure random strings
static String generateSecureToken(int length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
final random = Random.secure();
return List.generate(length, (index) => chars[random.nextInt(chars.length)]).join();
}
// Rate limiting for API calls
static final Map<String, List<DateTime>> _requestHistory = {};
static bool isRateLimited(String identifier, {int maxRequests = 10, Duration window = const Duration(minutes: 1)}) {
final now = DateTime.now();
final windowStart = now.subtract(window);
_requestHistory[identifier] ??= [];
final requests = _requestHistory[identifier]!;
// Remove old requests outside the window
requests.removeWhere((time) => time.isBefore(windowStart));
if (requests.length >= maxRequests) {
return true; // Rate limited
}
requests.add(now);
return false;
}
}
// Security exceptions
class SecurityException implements Exception {
final String message;
SecurityException(this.message);
@override
String toString() => 'SecurityException: $message';
}
class AuthenticationException extends SecurityException {
AuthenticationException(String message) : super(message);
}
class UnauthorizedException extends SecurityException {
UnauthorizedException(String message) : super(message);
}
// Password strength result
class PasswordStrength {
final int score;
final List<String> feedback;
PasswordStrength({required this.score, required this.feedback});
PasswordStrengthLevel get level {
if (score < 2) return PasswordStrengthLevel.weak;
if (score < 4) return PasswordStrengthLevel.medium;
return PasswordStrengthLevel.strong;
}
}
enum PasswordStrengthLevel { weak, medium, strong }
enum LogLevel { debug, info, warning, error }

