返回網誌
保安2025年9月18日安安科技團隊閱讀時間 15 分鐘

FLUTTER 應用程式保安
最佳實踐

全面的 Flutter 應用程式保安指南。學習數據加密、安全儲存、API 保安、身份驗證及漏洞防範技術。

Flutter 應用程式保安最佳實踐
// 簡報

在流動應用程式開發中,保安絕不應是事後才考慮的問題。隨著網絡威脅日益增加,加上 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/越獄偵測
  • 應用程式完整性驗證
  • Debug 模式偵測

常見漏洞與防範

了解常見保安漏洞,有助你主動保護應用程式。以下是最關鍵的威脅及相應的防範方法。

常見威脅

  • 中間人攻擊
  • 透過 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 }
CHK.01審計

FLUTTER 保安 清單

客戶端保安

使用 Flutter Secure Storage 儲存 token
實施生物識別驗證
加入 Root/越獄偵測
混淆發佈版本
驗證所有用戶輸入

網絡保安

實施證書綁定
所有通訊使用 HTTPS
以 HMAC 簽署 API 請求
實施速率限制
加密敏感請求數據
// 加固防線

立即保護你的 FLUTTER 應用程式

保安絕不應是事後補救。我們的團隊可以幫助你實施全面的保安措施,保護你的用戶和業務。

聯絡我們
// 訊號放大

分享本指南