Flutter 跨应用数据共享技术方案

53 阅读12分钟

深入解析:如何在 Flutter 中实现同一开发者多个 App 之间的安全数据共享

📖 目录

  1. 业务场景
  2. 技术挑战
  3. 技术方案
  4. 架构设计
  5. 核心实现
  6. 安全机制
  7. 完整代码解析
  8. 实战应用
  9. 常见问题
  10. 总结

业务场景

问题描述

假设您的公司有多个 Flutter App:

  • App A(主应用):核心业务平台
  • App B(子应用):辅助功能工具
  • App C(子应用):其他业务应用

需求:用户在任意一个 App 中登录后,打开其他 App 时,能够自动填充账号密码,无需重复输入。

核心要求

  1. 数据持久化:卸载重装后数据仍然保留
  2. 跨 App 共享:多个 App 能访问相同的数据
  3. 安全加密:敏感数据必须加密存储
  4. 签名验证:只有同一开发者的 App 能访问
  5. 双向同步:任意 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
    }
}

核心原理

  1. 提取应用的签名证书
  2. 计算签名的 SHA-256 哈希值
  3. 将哈希值作为 PBKDF2 的种子
  4. 所有使用相同签名的 App 得到相同的种子
  5. 相同的种子 → 相同的加密密钥 → 能解密彼此的数据

加密实现

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)
}

为什么这样设计?

  1. PBKDF2:密钥派生函数,即使种子泄露,攻击者也需要 10000 次哈希计算
  2. AES-GCM:认证加密,同时提供加密和完整性验证
  3. 随机 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", "❌ 所有保存方式都失败了")
    }
}

为什么要双重保存?

  1. 主应用优先:App A作为主 App,所有数据优先保存在这里
  2. 本应用备份:如果主应用未安装,本应用也能正常工作
  3. 容错性:即使主应用保存失败,本应用仍能保存数据

读取策略:优先级读取

/**
 * 获取账号密码
 * 策略:
 * 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()
}

优先级设计原因

  1. 统一数据源:优先读取主应用数据,保证数据一致性
  2. 降级策略:主应用不可用时,使用本地数据
  3. 扩展性:支持多个 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);
    
    // ... 跳转到主页 ...
  }
}

关键点

  1. initState 自动加载:用户打开页面就能看到保存的账号
  2. TextInput.finishAutofillContext(shouldSave: true):触发系统自动填充保存提示
  3. 用户控制:只有勾选"记住账号"才保存

安全机制

1. 签名验证(Android)

应用签名 (upload-keystore.jks)
    ↓
SHA-256 哈希
    ↓
Base64 编码
    ↓
作为 PBKDF2 种子
    ↓
派生 AES 密钥

安全性分析

  • ✅ 只有持有签名证书的开发者才能生成相同的密钥
  • ✅ 即使代码被反编译,攻击者也无法伪造签名
  • ✅ 系统级保护,无法通过软件绕过

2. 权限保护(Android)

<permission
    android:name="com.example.permission.ACCESS_CREDENTIALS"
    android:protectionLevel="signature" />

三重保护

  1. protectionLevel="signature":系统级签名验证
  2. ContentProvider 的 android:permission 属性:访问控制
  3. 加密存储:即使数据泄露也无法解密

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: 为什么需要相同的签名?

  1. Android 系统限制protectionLevel="signature" 权限只有相同签名才能获取
  2. 加密密钥派生:使用签名的 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

结论:对用户体验无影响,完全可用于生产环境。


总结

核心要点

  1. Android 使用 ContentProvider + 签名验证

    • ContentProvider 实现跨 App 数据共享
    • signature 权限保证只有相同签名的 App 能访问
    • 签名派生密钥保证加密密钥一致性
  2. iOS 使用 Keychain Sharing + App Groups

    • 配置简单,flutter_secure_storage 已封装好
    • 需要在 Apple Developer 后台配置
  3. 双向保存策略

    • 主应用作为数据中心
    • 子应用既读取也备份
    • 保证数据可靠性
  4. 安全性

    • 三重保护:签名验证 + 权限控制 + 加密存储
    • 业界标准算法:AES-256-GCM + PBKDF2
    • 无法伪造和破解

适用场景

适合

  • 同一公司的多个 App
  • 需要共享登录凭证
  • 对安全性有要求
  • 需要卸载后数据保留

不适合

  • 跨公司 App 间数据共享
  • 需要即时同步(ContentProvider 是本地存储)
  • 需要云端备份(需额外实现)

扩展方向

  1. 云端同步:结合后端 API,实现多设备同步
  2. 生物识别:集成指纹/面容识别解锁
  3. 更多数据类型:除了登录凭证,还可以共享用户偏好设置等
  4. 跨平台一致性:优化 iOS 实现,保持与 Android 一致的体验

参考资料


文档版本: 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 跨应用数据共享的完整方案!