在設計 Crew Up 的儲存層時,我們面臨的核心挑戰不僅是「如何儲存」,更重要的是「如何建立清晰的安全邊界」。作為一個涉及使用者認證、社交互動和敏感資料的應用,我們需要在效能、安全性和可維護性之間找到最佳平衡點。
本文將深入探討我們的儲存架構設計決策,包括:
在評估專案需求後,我們採用基於資料敏感度的分層儲存策略:
┌─────────────────────────────────────┐
│ Application Layer │
├─────────────────────────────────────┤
│ Repository Layer (Business Logic) │
├─────────────────────────────────────┤
│ Storage Abstraction Layer │
├──────────────────┬──────────────────┤
│ LocalStorage │ SecureStorage │
│ (Non-sensitive) │ (Sensitive) │
├──────────────────┼──────────────────┤
│ SharedPreferences│flutter_secure_ │
│ │ storage │
└──────────────────┴──────────────────┘
設計原則:
LocalStorage(一般儲存)
使用場景:快取資料、UI 狀態、非敏感業務資料
底層實作:SharedPreferences
資料範圍:使用者去識別化資料、活動列表、計數器
特性:讀寫效能優先、支援多種資料型別
SecureStorage(安全儲存)
使用場景:認證憑證、私密金鑰、使用者隱私資料
底層實作:flutter_secure_storage
資料範圍:Firebase ID Token(Firebase Auth 自動管理 token 生命週期)
特性:加密儲存、系統級保護、跨平台安全一致性
在儲存架構設計中,我們面臨的核心權衡是安全性 vs 效能:
SharedPreferences (LocalStorage) 的效能特性:
flutter_secure_storage (SecureStorage) 的效能成本:
┌─────────────────┬─────────────────┬─────────────────┐
│ 操作類型 │ LocalStorage │ SecureStorage │
├─────────────────┼─────────────────┼─────────────────┤
│ 讀取速度 │ ⚡ 快速 │ 🐢 較慢(需加解密)│
│ 寫入速度 │ ⚡ 快速 │ 🐢 較慢(需加解密)│
│ 批次存取效率 │ 👍 適合 │ 👎 不建議(開銷大)│
└─────────────────┴─────────────────┴─────────────────┘
為什麼這個權衡至關重要?
正因為 SecureStorage 的效能成本較高,我們才更需要將其使用範圍限制在絕對必要的敏感資料上(如 Firebase ID Token)。如果將 SecureStorage 濫用於頻繁讀寫的業務快取,會對 App 流暢度造成可觀測的影響,特別是在:
在決定儲存架構時,我們必須理解不同儲存方式面臨的安全威脅。
SharedPreferences 的安全限制:
SharedPreferences 和 iOS 的 UserDefaults 本質上都是明文儲存,存在多種安全風險:
Android 平台風險:
/data/data/com.example.crew_up/shared_prefs/
├── FlutterSharedPreferences.xml ← 明文可讀
├── [其他設定檔案].xml
iOS 平台風險:
~/Library/Preferences/com.example.crew-up.plist ← 明文 plist 檔案
具體威脅場景:
Root/Jailbreak 設備風險:
除錯工具風險:
adb shell
可以直接讀取應用資料設備備份洩露:
實際的安全邊界劃分:
在 Crew Up 專案中,我們的劃分策略基於資料的敏感度:
// 一般儲存(LocalStorage)- 可接受的風險
final chatCache = await localStorage.getStringList('recent_chats');
final activityCount = await localStorage.getInt('activities_count');
// 即使被讀取,也不會造成嚴重的安全問題
// 安全儲存(SecureStorage)- 必須保護的資料
final firebaseIdToken = await secureStorage.getString(SecureStorageKeys.firebaseIdToken);
// Firebase ID Token 如果被竊取,攻擊者可以冒充使用者身份
風險評估的實務考量:
這就是為什麼我們採用分層儲存策略的核心原因:不同敏感度的資料需要不同層級的保護。
// lib/app/core/storage/local_storage.dart
// (imports omitted)
/// 本地儲存抽象介面
///
/// 提供類型安全的儲存操作,支援 SharedPreferences 的所有基本資料型別
/// 設計原則:
/// - 介面隔離:只暴露必要的操作
/// - 異常透明:讓呼叫方決定錯誤處理策略
/// - 類型安全:避免運行時類型錯誤
abstract class LocalStorage {
// 基本儲存操作
Future<void> saveString(String key, String value);
Future<String?> getString(String key);
Future<void> saveBool(String key, bool value);
Future<bool?> getBool(String key);
Future<void> saveInt(String key, int value);
Future<int?> getInt(String key);
Future<void> saveDouble(String key, double value);
Future<double?> getDouble(String key);
// 複合資料型別
Future<void> saveStringList(String key, List<String> value);
Future<List<String>?> getStringList(String key);
// 元資料操作
Future<bool> containsKey(String key);
Future<void> remove(String key);
Future<void> clear();
Future<Set<String>> getKeys();
}
🎯 設計考量:
async
// lib/app/core/storage/local_storage.dart
// (imports omitted)
/// LocalStorage 的生產環境實作
///
/// 基於 SharedPreferences,增強了錯誤處理和可觀測性
class LocalStorageImpl implements LocalStorage {
SharedPreferences? _preferences;
/// 延遲初始化的 SharedPreferences 實例
/// 使用單例模式避免重複初始化開銷
Future<SharedPreferences> get _prefs async {
_preferences ??= await SharedPreferences.getInstance();
return _preferences!;
}
@override
Future<void> saveString(String key, String value) async {
try {
final prefs = await _prefs;
await prefs.setString(key, value);
developer.log('✅ 字串儲存成功: $key', name: 'LocalStorageImpl');
} on Exception catch (e) {
developer.log('❌ 字串儲存失敗: $key - $e', name: 'LocalStorageImpl');
rethrow; // 讓呼叫方決定降級策略
}
}
@override
Future<String?> getString(String key) async {
try {
final prefs = await _prefs;
final value = prefs.getString(key);
developer.log('📖 字串讀取: $key = $value', name: 'LocalStorageImpl');
return value;
} on Exception catch (e) {
developer.log('❌ 字串讀取失敗: $key - $e', name: 'LocalStorageImpl');
throw LocalStorageReadException(
'Failed to read string value',
originalException: e,
key: key,
);
}
}
// 其他方法遵循相同模式...
}
💡 實作亮點:
developer.log
提供豐富的除錯資訊// lib/app/core/storage/secure_storage.dart
// (imports omitted)
/// 安全儲存抽象介面
///
/// 專門處理敏感資料,提供系統級加密保護
/// 設計目標:最小化攻擊面、跨平台一致性
abstract class SecureStorage {
Future<void> saveString(String key, String value);
Future<String?> getString(String key);
Future<bool> containsKey(String key);
Future<void> remove(String key);
Future<void> clear();
}
/// 基於 flutter_secure_storage 的實作
///
/// 針對不同平台優化安全參數,提供最佳的安全性與可用性平衡
class SecureStorageImpl implements SecureStorage {
final FlutterSecureStorage _storage;
SecureStorageImpl({FlutterSecureStorage? storage})
: _storage = storage ?? const FlutterSecureStorage();
// 平台特定的安全配置
IOSOptions get _defaultIOSOptions =>
const IOSOptions(accessibility: KeychainAccessibility.unlocked);
AndroidOptions get _defaultAndroidOptions =>
const AndroidOptions(encryptedSharedPreferences: true);
// 其他平台配置...
@override
Future<void> saveString(String key, String value) async {
try {
await _storage.write(
key: key,
value: value,
iOptions: _defaultIOSOptions,
aOptions: _defaultAndroidOptions,
// 其他平台參數...
);
developer.log('🔐 Saved secure key: $key', name: 'SecureStorageImpl');
} on Exception catch (e) {
developer.log(
'❌ Failed to save secure key: $key - $e',
name: 'SecureStorageImpl',
);
rethrow;
}
}
// 其他方法實作...
}
/// 安全金鑰常數定義
///
/// 集中管理避免魔法字串,降低金鑰衝突風險
class SecureStorageKeys {
static const String firebaseIdToken = 'auth:firebase_id_token';
// 注意:使用 Firebase Auth,只需儲存 Firebase ID Token,
// 不需要傳統的 access token / refresh token 機制
// 私有建構函數防止實例化
SecureStorageKeys._();
}
🔒 安全性考量:
iOS Keychain 配置選擇:
IOSOptions get _defaultIOSOptions =>
const IOSOptions(accessibility: KeychainAccessibility.unlocked);
為什麼選擇 KeychainAccessibility.unlocked
?這是基於專案實際需求的選擇:
Android EncryptedSharedPreferences 配置:
AndroidOptions get _defaultAndroidOptions =>
const AndroidOptions(encryptedSharedPreferences: true);
encryptedSharedPreferences: true
在專案中的實際效果:
跨平台一致性設計:
// 統一的安全配置介面
await _storage.write(
key: key,
value: value,
iOptions: _defaultIOSOptions, // iOS 專屬配置
aOptions: _defaultAndroidOptions, // Android 專屬配置
// ... 其他平台
);
這種設計確保了:
// lib/features/auth/data/datasources/auth_remote_datasource.dart
// (imports omitted)
// Token 儲存:優先級和降級策略
final String? idToken = await firebaseUser.getIdToken();
if (idToken != null && idToken.isNotEmpty) {
developer.log(
'🔐 保存 Firebase ID Token 至 SecureStorage',
name: 'AuthRemoteDataSourceFirebaseOnly',
);
// 安全儲存 Token
await _secureStorage.saveString(
SecureStorageKeys.firebaseIdToken,
idToken,
);
developer.log(
'✅ Firebase ID Token 已安全儲存',
name: 'AuthRemoteDataSourceFirebaseOnly',
);
}
// lib/features/auth/data/datasources/auth_remote_datasource.dart
// (imports omitted)
// 清除敏感資料:確保無殘留
developer.log(
'🧽 準備清除 SecureStorage Token',
name: 'AuthRemoteDataSourceFirebaseOnly',
);
// 清除 Firebase ID Token
await _secureStorage.remove(SecureStorageKeys.firebaseIdToken);
🎯 生命週期管理重點:
在 Crew Up 的 Clean Architecture 實作中,儲存層作為基礎設施層,透過 Repository 模式為業務邏輯提供服務:
// Repository 中的儲存層使用範例
// (imports omitted)
class ActivityRepositoryImpl extends BaseRepository
with RepositoryErrorHandling
implements ActivityRepository {
// 注入儲存抽象,不直接依賴具體實作
final ActivityLocalDataSource _localDataSource; // 內部使用 LocalStorage
final ActivityDataSource _remoteDataSource;
@override
Future<Result<List<Activity>>> getActivities() async => executeRemoteFirst(
'獲取活動列表',
RepositoryOperation.read,
() async {
// 遠端優先策略
final remoteActivities = await _remoteDataSource.getAllActivities();
// 同步到本地儲存,使用 safeSyncOperation 確保錯誤隔離
await safeSyncOperation(() async {
for (final activity in remoteActivities) {
await _localDataSource.saveActivity(activity); // 內部使用 LocalStorage
}
}, '同步活動列表到本地');
return remoteActivities;
},
() async => await _localDataSource.getAllActivities(), // 本地降級策略
);
}
🏗️ 架構整合優勢:
明天,我們將深入儲存系統的進階議題:資料快取、TTL 過期控制與版本管理。我們會學習如何設計可過期的快取策略、在本地與遠端之間做出正確取捨,並在資料格式演進時安全完成遷移與回滾。
期待與您在 Day 9 相見!