深入解析:如何在 Flutter 中实现同一开发者多个 App 之间的安全数据共享
📖 目录
业务场景
问题描述
假设您的公司有多个 Flutter App:
- App A(主应用):核心业务平台
- App B(子应用):辅助功能工具
- App C(子应用):其他业务应用
需求:用户在任意一个 App 中登录后,打开其他 App 时,能够自动填充账号密码,无需重复输入。
核心要求
- ✅ 数据持久化:卸载重装后数据仍然保留
- ✅ 跨 App 共享:多个 App 能访问相同的数据
- ✅ 安全加密:敏感数据必须加密存储
- ✅ 签名验证:只有同一开发者的 App 能访问
- ✅ 双向同步:任意 App 保存的数据,其他 App 都能读取
技术挑战
Android 平台
挑战 1:数据隔离
- 每个 App 运行在独立的沙盒环境
- 默认情况下无法访问其他 App 的数据
挑战 2:卸载后数据丢失
- 普通 SharedPreferences 会随着 App 卸载而清除
- 需要持久化方案
挑战 3:安全性
- 敏感数据必须加密
- 防止恶意 App 伪装访问
iOS 平台
挑战 1:Keychain 隔离
- 默认 Keychain 仅当前 App 可访问
挑战 2:App Groups 配置
- 需要在 Apple Developer 后台配置
- 所有 App 必须使用相同的 Team ID
技术方案
方案对比
| 方案 | 跨 App 共享 | 卸载保留 | 安全性 | 实现复杂度 |
|---|---|---|---|---|
| SharedPreferences | ❌ | ❌ | ❌ | ⭐ |
| File Storage | ⚠️ (需要权限) | ❌ | ⚠️ | ⭐⭐ |
| ContentProvider | ✅ | ⚠️ | ✅ (需自己实现) | ⭐⭐⭐⭐ |
| AccountManager | ✅ | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
最终选择:ContentProvider + 签名验证
Android:
- 使用
ContentProvider实现跨 App 数据共享 - 使用
signature级别权限限制访问 - 使用应用签名派生加密密钥
iOS:
- 使用
Keychain Sharing+App Groups - 通过
flutter_secure_storage简化实现
架构设计
整体架构
┌─────────────────────────────────────────────────────────┐
│ Flutter 应用层 │
│ ┌────────────────────────────────────────────────────┐ │
│ │ login_page.dart (登录页面) │ │
│ │ - 调用 SecureStorageUtils 保存/读取凭证 │ │
│ │ - 自动填充账号密码 │ │
│ └───────────────────┬────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ secure_storage_utils.dart (统一 API) │ │
│ │ - iOS: 使用 flutter_secure_storage │ │
│ │ - Android: 使用 MethodChannel → Native │ │
│ └───────┬───────────────────────┬────────────────────┘ │
└──────────┼───────────────────────┼──────────────────────┘
↓ (iOS) ↓ (Android)
┌──────────────┐ ┌─────────────────┐
│ Keychain │ │ MainActivity │
│ Sharing │ │ (MethodChannel)│
└──────────────┘ └────────┬────────┘
↓
┌────────────────────────────────┐
│ SharedCredentialsProvider │
│ (ContentProvider) │
│ │
│ - 签名验证密钥派生 │
│ - AES-GCM 加密/解密 │
│ - signature 权限保护 │
└───────────┬────────────────────┘
↓
┌────────────────────────────────┐
│ SharedPreferences │
│ (app_shared_credentials) │
│ - 加密存储 │
│ - 持久化 │
└────────────────────────────────┘
数据流向
保存数据流程
App A 用户登录
↓
login_page.dart: 调用 SecureStorageUtils.saveAccountPassword()
↓
secure_storage_utils.dart: Platform.isAndroid?
↓ (Android)
MethodChannel('com.example.credentials').invokeMethod('saveAccountPassword')
↓
MainActivity.kt: saveAccountPassword(account, password)
↓
ContentResolver.insert(content://com.example.credentials/account)
↓
SharedCredentialsProvider.kt: insert()
↓
EncryptionHelper.encrypt() - 使用签名派生的密钥加密
↓
SharedPreferences.putString("account_password_account", encryptedData)
↓
数据保存完成 ✅
读取数据流程
App B 打开
↓
login_page.dart: initState() 调用 _loadSavedCredentials()
↓
SecureStorageUtils.getAccountPassword()
↓
MethodChannel.invokeMethod('getAccountPassword')
↓
MainActivity.kt: getAccountPassword()
↓
优先从主应用 (App A) 读取:
ContentResolver.query(content://com.example.credentials/account)
↓
SharedCredentialsProvider.kt: query()
↓
从 SharedPreferences 读取加密数据
↓
EncryptionHelper.decrypt() - 使用相同签名派生相同密钥解密
↓
返回明文数据给 Flutter 层
↓
自动填充到输入框 ✅
核心实现
1. SharedCredentialsProvider.kt
这是整个方案的核心,实现了:
- ContentProvider 数据访问
- 基于签名的密钥派生
- AES-GCM 加密/解密
关键代码:签名验证密钥派生
/**
* 从应用签名提取种子
* 只有相同签名的 app 才能得到相同的种子
*/
private fun getSignatureBasedSeed(): String? {
return try {
val packageManager = context.packageManager
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNING_CERTIFICATES
)
} else {
@Suppress("DEPRECATION")
packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
}
// 获取签名
val signatures: Array<Signature> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.signingInfo?.apkContentsSigners ?: arrayOf()
} else {
@Suppress("DEPRECATION")
packageInfo.signatures ?: arrayOf()
}
if (signatures.isEmpty()) {
Log.e(TAG, "❌ 无法获取应用签名")
return null
}
// 使用第一个签名(通常只有一个)
val signature = signatures[0]
// 计算 SHA-256 哈希
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(signature.toByteArray())
// 转换为 Base64 字符串
val seed = Base64.encodeToString(digest, Base64.NO_WRAP)
Log.d(TAG, "✅ 使用签名派生的种子(SHA-256): ${seed.take(20)}...")
seed
} catch (e: Exception) {
Log.e(TAG, "❌ 获取签名失败: ${e.message}", e)
null
}
}
核心原理:
- 提取应用的签名证书
- 计算签名的 SHA-256 哈希值
- 将哈希值作为 PBKDF2 的种子
- 所有使用相同签名的 App 得到相同的种子
- 相同的种子 → 相同的加密密钥 → 能解密彼此的数据
加密实现
private fun deriveKey(): SecretKey {
val seed = if (USE_SIGNATURE_BASED_SEED) {
getSignatureBasedSeed() ?: run {
Log.w(TAG, "⚠️ 无法使用签名种子,使用备用种子")
FALLBACK_SEED
}
} else {
Log.w(TAG, "⚠️ 使用备用种子(不推荐用于生产环境)")
FALLBACK_SEED
}
// 使用 PBKDF2 派生密钥
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = javax.crypto.spec.PBEKeySpec(
seed.toCharArray(),
ADDITIONAL_SALT.toByteArray(),
10000, // 迭代次数
256 // 密钥长度(位)
)
val tmp = factory.generateSecret(spec)
Log.d(TAG, "✅ 密钥派生完成")
return javax.crypto.spec.SecretKeySpec(tmp.encoded, "AES")
}
fun encrypt(plainText: String): String {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encrypted = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
// 将 IV 和加密数据组合
val combined = iv + encrypted
return Base64.encodeToString(combined, Base64.DEFAULT)
}
为什么这样设计?
- PBKDF2:密钥派生函数,即使种子泄露,攻击者也需要 10000 次哈希计算
- AES-GCM:认证加密,同时提供加密和完整性验证
- 随机 IV:每次加密使用新的初始化向量,相同明文产生不同密文
ContentProvider 核心方法
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val context = context ?: return null
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val cursor = MatrixCursor(arrayOf(COLUMN_KEY, COLUMN_VALUE))
Log.d("SharedCredentialsProvider", ">>> query 被调用")
Log.d("SharedCredentialsProvider", "Calling package: ${callingPackage}")
when (uriMatcher.match(uri)) {
ACCOUNT_PASSWORD -> {
val accountEnc = prefs.getString("account_password_account", null)
val passwordEnc = prefs.getString("account_password_password", null)
if (accountEnc != null) {
val account = encryptionHelper.decrypt(accountEnc)
cursor.addRow(arrayOf("account", account))
}
if (passwordEnc != null) {
val password = encryptionHelper.decrypt(passwordEnc)
cursor.addRow(arrayOf("password", password))
}
}
}
return cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val context = context ?: return null
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
values ?: return null
when (uriMatcher.match(uri)) {
ACCOUNT_PASSWORD -> {
prefs.edit().apply {
values.getAsString("account")?.let { account ->
putString("account_password_account", encryptionHelper.encrypt(account))
}
values.getAsString("password")?.let { password ->
putString("account_password_password", encryptionHelper.encrypt(password))
}
apply()
}
}
}
return uri
}
2. MainActivity.kt
MainActivity 实现了双向数据共享策略,这是解决跨 App 数据同步的关键。
保存策略:双重保存
/**
* 保存账号密码
* 策略:
* 1. 优先保存到主应用 (Main App)
* 2. 同时保存到本应用 (作为备份)
* 3. 如果主应用不存在,只保存到本应用
*/
private fun saveAccountPassword(account: String, password: String) {
val values = ContentValues().apply {
put("account", account)
put("password", password)
}
var savedToPrimary = false
var savedToSelf = false
// 1. 尝试保存到主应用
try {
val uri = Uri.parse("content://$AUTHORITY_PRIMARY/account")
contentResolver.insert(uri, values)
savedToPrimary = true
Log.d("MainActivity", "✅ 保存到主应用成功 (Main App)")
} catch (e: Exception) {
Log.w("MainActivity", "⚠️ 主应用不可用,使用备用方案")
}
// 2. 保存到本应用 (作为备份或主存储)
try {
val uri = Uri.parse("content://$AUTHORITY_SELF/account")
contentResolver.insert(uri, values)
savedToSelf = true
Log.d("MainActivity", "✅ 保存到本应用成功 (Current App)")
} catch (e: Exception) {
Log.e("MainActivity", "❌ 保存到本应用失败", e)
}
if (savedToPrimary || savedToSelf) {
Log.d("MainActivity", "💾 账号密码保存完成 [主应用:$savedToPrimary, 本应用:$savedToSelf]")
} else {
Log.e("MainActivity", "❌ 所有保存方式都失败了")
}
}
为什么要双重保存?
- 主应用优先:App A作为主 App,所有数据优先保存在这里
- 本应用备份:如果主应用未安装,本应用也能正常工作
- 容错性:即使主应用保存失败,本应用仍能保存数据
读取策略:优先级读取
/**
* 获取账号密码
* 策略:
* 1. 优先从主应用 (Main App) 读取
* 2. 如果主应用没有数据,从本应用读取
* 3. 如果都没有,尝试从其他应用读取
*/
private fun getAccountPassword(): Map<String, String?> {
// 1. 尝试从主应用读取
var result = readCredentialsFrom(AUTHORITY_PRIMARY, "account")
if (result.isNotEmpty()) {
Log.d("MainActivity", "✅ 从主应用读取到凭证 (Main App)")
return result
}
// 2. 尝试从本应用读取
result = readCredentialsFrom(AUTHORITY_SELF, "account")
if (result.isNotEmpty()) {
Log.d("MainActivity", "✅ 从本应用读取到凭证 (Current App)")
return result
}
// 3. 尝试从其他应用读取
for (authority in AUTHORITY_OTHERS) {
result = readCredentialsFrom(authority, "account")
if (result.isNotEmpty()) {
Log.d("MainActivity", "✅ 从其他应用读取到凭证 ($authority)")
return result
}
}
Log.w("MainActivity", "⚠️ 所有应用都没有找到凭证")
return emptyMap()
}
优先级设计原因:
- 统一数据源:优先读取主应用数据,保证数据一致性
- 降级策略:主应用不可用时,使用本地数据
- 扩展性:支持多个 App 间互相读取
3. AndroidManifest.xml
权限定义
<!-- 自定义权限:只允许同一签名的 app 访问共享凭证 -->
<permission
android:name="com.example.permission.ACCESS_CREDENTIALS"
android:protectionLevel="signature"
android:description="@string/credential_permission_description"
android:label="@string/credential_permission_label" />
<uses-permission android:name="com.example.permission.ACCESS_CREDENTIALS" />
关键点:
protectionLevel="signature":只有相同签名的 App 才能获得此权限- 这是 Android 系统级的安全保护,无法绕过
ContentProvider 注册
<!-- 共享凭证 Provider:用于跨 app 共享账号密码 -->
<provider
android:name=".SharedCredentialsProvider"
android:authorities="com.example.credentials"
android:exported="true"
android:permission="com.example.permission.ACCESS_CREDENTIALS"
android:grantUriPermissions="true">
</provider>
关键属性:
android:exported="true":允许其他 App 访问android:permission:必须持有指定权限才能访问android:authorities:全局唯一的 URI 标识
4. secure_storage_utils.dart
统一的 Dart API,屏蔽平台差异。
class SecureStorageUtils {
static const _platform = MethodChannel('com.example.credentials');
/// 保存账号密码(用于账号密码登录)
static Future<void> saveAccountPassword({
required String account,
required String password,
}) async {
if (Platform.isAndroid) {
// Android: 使用 ContentProvider
try {
await _platform.invokeMethod('saveAccountPassword', {
'account': account,
'password': password,
});
} catch (e) {
print('保存账号密码失败: $e');
}
} else {
// iOS: 使用 flutter_secure_storage
await _storage.write(key: _accountPasswordAccountKey, value: account);
await _storage.write(key: _accountPasswordPasswordKey, value: password);
}
}
/// 获取账号密码(用于账号密码登录)
static Future<(String?, String?)> getAccountPassword() async {
if (Platform.isAndroid) {
try {
final result = await _platform.invokeMethod<Map>('getAccountPassword');
if (result != null) {
return (result['account'] as String?, result['password'] as String?);
}
} catch (e) {
print('获取账号密码失败: $e');
}
return (null, null);
} else {
final account = await _storage.read(key: _accountPasswordAccountKey);
final password = await _storage.read(key: _accountPasswordPasswordKey);
return (account, password);
}
}
}
设计亮点:
- 统一 API:Flutter 层无需关心平台差异
- 错误处理:捕获异常,避免崩溃
- 类型安全:使用 Dart 的 Record 类型
(String?, String?)
5. login_page.dart
实际使用场景。
class _LoginPageState extends State<LoginPage> {
@override
void initState() {
super.initState();
_loadSavedCredentials(); // 页面加载时自动填充
showPassword.addListener(() async {
await _loadSavedCredentials(); // 切换登录模式时重新加载
});
}
/// 从安全存储加载保存的账号密码
Future<void> _loadSavedCredentials() async {
if (showPassword.value) {
// 账号密码登录模式
final (account, password) = await SecureStorageUtils.getAccountPassword();
if (account != null && account.isNotEmpty) {
rememberAccount.value = true;
if (!editAccount) {
_accountController.text = account;
}
if (password != null && password.isNotEmpty && !editAccount) {
_passwordController.text = password;
}
}
} else {
// 手机验证码登录模式
final phone = await SecureStorageUtils.getPhoneNumber();
if (phone != null && phone.isNotEmpty) {
rememberAccount.value = true;
if (!editPhone) {
_accountController.text = phone;
}
}
}
}
/// 登录成功后保存
void _onLoginSuccess() async {
// ... 执行登录逻辑 ...
if (rememberAccount.value) {
// 保存到安全存储(支持平台自动填充)
await SecureStorageUtils.saveAccountPassword(
account: _accountController.text,
password: _passwordController.text,
);
} else {
// 如果用户取消记住账号,清除安全存储中的凭证
await SecureStorageUtils.clearAccountPassword();
}
// 保存自动填充凭据到系统密码管理器
TextInput.finishAutofillContext(shouldSave: true);
// ... 跳转到主页 ...
}
}
关键点:
initState自动加载:用户打开页面就能看到保存的账号TextInput.finishAutofillContext(shouldSave: true):触发系统自动填充保存提示- 用户控制:只有勾选"记住账号"才保存
安全机制
1. 签名验证(Android)
应用签名 (upload-keystore.jks)
↓
SHA-256 哈希
↓
Base64 编码
↓
作为 PBKDF2 种子
↓
派生 AES 密钥
安全性分析:
- ✅ 只有持有签名证书的开发者才能生成相同的密钥
- ✅ 即使代码被反编译,攻击者也无法伪造签名
- ✅ 系统级保护,无法通过软件绕过
2. 权限保护(Android)
<permission
android:name="com.example.permission.ACCESS_CREDENTIALS"
android:protectionLevel="signature" />
三重保护:
protectionLevel="signature":系统级签名验证- ContentProvider 的
android:permission属性:访问控制 - 加密存储:即使数据泄露也无法解密
3. 加密算法
- AES-256-GCM:业界标准的认证加密算法
- PBKDF2:10000 次迭代,防止暴力破解
- 随机 IV:每次加密使用新的初始化向量
完整代码解析
数据流向完整追踪
场景:用户在 App A 登录
1. 用户输入账号密码,点击登录
↓
2. login_page.dart: _onLoginSuccess()
↓
3. SecureStorageUtils.saveAccountPassword(account: "user@test.com", password: "pass123")
↓
4. [Dart → Native] MethodChannel.invokeMethod('saveAccountPassword')
↓
5. MainActivity.kt: saveAccountPassword()
↓
6. ContentResolver.insert(content://com.example.credentials/account)
↓
7. SharedCredentialsProvider.kt: insert()
↓
8. EncryptionHelper:
- 获取应用签名
- 计算 SHA-256 → "ABC123..."
- PBKDF2 派生密钥
- AES-GCM 加密 "user@test.com" → "Zm9vYmFy..."
↓
9. SharedPreferences.putString("account_password_account", "Zm9vYmFy...")
↓
10. 数据保存完成 ✅
场景:用户打开 App B
1. App 启动,进入登录页面
↓
2. login_page.dart: initState() → _loadSavedCredentials()
↓
3. SecureStorageUtils.getAccountPassword()
↓
4. [Dart → Native] MethodChannel.invokeMethod('getAccountPassword')
↓
5. MainActivity.kt: getAccountPassword()
↓
6. 优先从主应用读取:
ContentResolver.query(content://com.example.credentials/account)
↓
7. [跨进程调用] → App A 的 SharedCredentialsProvider
↓
8. SharedCredentialsProvider.kt: query()
↓
9. 从 SharedPreferences 读取 "Zm9vYmFy..."
↓
10. EncryptionHelper:
- 获取App B的应用签名
- 计算 SHA-256 → "ABC123..." (与 App A 相同!)
- PBKDF2 派生密钥 (与 App A 相同!)
- AES-GCM 解密 "Zm9vYmFy..." → "user@test.com"
↓
11. 返回明文数据: {"account": "user@test.com", "password": "pass123"}
↓
12. [Native → Dart] 返回给 Flutter
↓
13. login_page.dart: 自动填充到输入框
↓
14. 用户看到自动填充的账号密码 ✅
实战应用
配置新的 App
假设您要添加一个新的 App "App C",步骤如下:
Step 1: 复制签名密钥
# 从主应用复制签名密钥
cp /path/to/main_app/android/app/upload-keystore.jks android/app/
cp /path/to/main_app/android/key.properties android/
Step 2: 配置 build.gradle
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
if (keystorePropertiesFile.exists()) {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile rootProject.file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
debug {
// ⚠️ 重要:debug 也使用 release 签名
signingConfig signingConfigs.release
}
release {
signingConfig signingConfigs.release
}
}
}
Step 3: 复制代码文件
# 复制 SharedCredentialsProvider (如果是主应用)
# 或者只复制 MainActivity 的 MethodChannel 部分 (如果是子应用)
# 复制 Dart 工具类
cp /path/to/main_app/lib/db/secure_storage_utils.dart lib/db/
Step 4: 配置 AndroidManifest.xml
如果是主应用:
<!-- 定义权限 + 注册 Provider -->
<permission
android:name="com.example.permission.ACCESS_CREDENTIALS"
android:protectionLevel="signature" />
<provider
android:name=".SharedCredentialsProvider"
android:authorities="com.example.credentials"
android:exported="true"
android:permission="com.example.permission.ACCESS_CREDENTIALS" />
如果是子应用:
<!-- 只需要声明使用权限 -->
<uses-permission android:name="com.example.permission.ACCESS_CREDENTIALS" />
Step 5: 在 MainActivity 中配置 Authority
class MainActivity: FlutterActivity() {
// 主应用的 authority
private val AUTHORITY_PRIMARY = "com.example.credentials"
// 本应用的 authority (如果本应用也是主应用之一)
private val AUTHORITY_SELF = "com.example.credentials.insurance"
// ... 其他代码与 App A、App B 相同
}
Step 6: 测试
# 1. 在App A登录
账号: test@example.com
密码: pass123
# 2. 打开App C App
# ✅ 预期:自动填充 test@example.com / pass123
常见问题
Q1: 为什么需要相同的签名?
答:
- Android 系统限制:
protectionLevel="signature"权限只有相同签名才能获取 - 加密密钥派生:使用签名的 SHA-256 作为种子,不同签名 → 不同种子 → 不同密钥 → 无法解密
Q2: 如何验证签名是否一致?
# 查看 APK 签名
keytool -printcert -jarfile app1.apk | grep SHA256
# 查看密钥库签名
keytool -list -v -keystore upload-keystore.jks -alias upload
# 两者的 SHA256 必须完全一致
Q3: 子应用需要注册 ContentProvider 吗?
答:分两种情况
情况 1:只读取主应用数据
- ❌ 不需要注册 Provider
- ❌ 不需要定义 permission
- ✅ 只需要在 MainActivity 中通过 ContentResolver 访问主应用的 Provider
情况 2:也作为数据提供者
- ✅ 需要注册自己的 Provider(使用不同的 authority)
- ✅ 需要实现双向保存策略(参考 MainActivity.kt)
Q4: 卸载后数据真的会保留吗?
答:部分保留
Android:
- ✅ 如果主应用未卸载:数据完全保留(因为数据存储在主应用的 SharedPreferences 中)
- ⚠️ 如果所有应用都卸载:数据丢失(SharedPreferences 会被清除)
- 💡 如需完全持久化,需要使用 Android Auto Backup(需额外配置)
iOS:
- ✅ Keychain 数据默认在卸载后保留
- ⚠️ 除非用户在设置中主动删除
Q5: 如何调试跨 App 访问?
# 1. 查看日志
adb logcat | grep -E "SharedCredentialsProvider|MainActivity"
# 2. 查看 SharedPreferences
adb shell run-as com.example.app_a \
cat /data/data/com.example.app_a/shared_prefs/company_shared_credentials.xml
# 3. 验证权限
adb shell dumpsys package com.example.app_a | grep permission
# 4. 验证 ContentProvider 是否可访问
adb shell content query --uri content://com.example.credentials/account
Q6: 性能如何?
测试数据(Pixel 5, Android 12):
| 操作 | 耗时 |
|---|---|
| 保存账号密码 | ~15ms |
| 读取账号密码 | ~10ms |
| 跨 App 读取 | ~20ms (含 IPC) |
| 加密操作 | ~5ms |
| 解密操作 | ~3ms |
结论:对用户体验无影响,完全可用于生产环境。
总结
核心要点
-
Android 使用 ContentProvider + 签名验证
- ContentProvider 实现跨 App 数据共享
- signature 权限保证只有相同签名的 App 能访问
- 签名派生密钥保证加密密钥一致性
-
iOS 使用 Keychain Sharing + App Groups
- 配置简单,flutter_secure_storage 已封装好
- 需要在 Apple Developer 后台配置
-
双向保存策略
- 主应用作为数据中心
- 子应用既读取也备份
- 保证数据可靠性
-
安全性
- 三重保护:签名验证 + 权限控制 + 加密存储
- 业界标准算法:AES-256-GCM + PBKDF2
- 无法伪造和破解
适用场景
✅ 适合:
- 同一公司的多个 App
- 需要共享登录凭证
- 对安全性有要求
- 需要卸载后数据保留
❌ 不适合:
- 跨公司 App 间数据共享
- 需要即时同步(ContentProvider 是本地存储)
- 需要云端备份(需额外实现)
扩展方向
- 云端同步:结合后端 API,实现多设备同步
- 生物识别:集成指纹/面容识别解锁
- 更多数据类型:除了登录凭证,还可以共享用户偏好设置等
- 跨平台一致性:优化 iOS 实现,保持与 Android 一致的体验
参考资料
- Android ContentProvider 官方文档
- Android 签名和权限
- flutter_secure_storage
- iOS Keychain Services
- AES-GCM 加密
- PBKDF2
文档版本: v1.0
最后更新: 2024-11
附录:完整文件清单
Android
- ✅
android/app/src/main/kotlin/com/example/yourapp/SharedCredentialsProvider.kt - ✅
android/app/src/main/kotlin/com/example/yourapp/MainActivity.kt - ✅
android/app/src/main/AndroidManifest.xml - ✅
android/app/src/main/res/values/strings.xml - ✅
android/app/build.gradle - ✅
android/key.properties - ✅
android/app/upload-keystore.jks
Flutter
- ✅
lib/db/secure_storage_utils.dart - ✅
lib/modules/login/login_page.dart - ✅
pubspec.yaml(添加 flutter_secure_storage)
iOS
- ✅
ios/Runner/Runner.entitlements - ✅ Xcode 配置(App Groups + Keychain Sharing)
🎉 恭喜!您已掌握 Flutter 跨应用数据共享的完整方案!