Android跨应用数据共享权限管理

194 阅读4分钟

本文将深入探讨Android跨应用数据共享的安全机制,结合完整Kotlin代码实现,覆盖ContentProvider、FileProvider等核心技术的权限控制策略,并附赠最佳实践和性能优化技巧。

一、ContentProvider深度解析与实战

1.1 权限声明与配置

<!-- AndroidManifest.xml -->
<provider
    android:name=".data.UserDataProvider"
    android:authorities="com.example.app.provider.userdata"
    android:exported="true"
    android:readPermission="com.example.app.permission.READ_USER_DATA"
    android:writePermission="com.example.app.permission.WRITE_USER_DATA">
    
    <!-- 细粒度路径权限控制 -->
    <path-permission
        android:pathPattern="/sensitive/.*"
        android:permission="com.example.app.permission.ACCESS_SENSITIVE_DATA"
        android:readPermission=""/>
        
    <!-- 允许动态授权的URI -->
    <grant-uri-permission android:path="/public/*"/>
</provider>

1.2 ContentProvider完整实现

class UserDataProvider : ContentProvider() {

    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
        addURI(AUTHORITY, "users", USERS)
        addURI(AUTHORITY, "users/#", USER_ID)
        addURI(AUTHORITY, "sensitive/*", SENSITIVE)
    }

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        // 权限检查
        when (uriMatcher.match(uri)) {
            USERS, USER_ID -> {
                checkPermission(READ_PERMISSION)
            }
            SENSITIVE -> {
                // 特殊路径需要额外权限
                context?.checkCallingPermission(SENSITIVE_PERMISSION)?.let {
                    if (it != PERMISSION_GRANTED) throw SecurityException("Requires $SENSITIVE_PERMISSION")
                }
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
        
        // 实际数据库查询逻辑
        return db.query(
            "users", 
            projection, 
            selection, 
            selectionArgs, 
            null, 
            null, 
            sortOrder
        )
    }

    private fun checkPermission(permission: String) {
        context?.checkCallingOrSelfPermission(permission)?.let {
            if (it != PERMISSION_GRANTED) {
                throw SecurityException("Requires $permission")
            }
        }
    }

    companion object {
        const val AUTHORITY = "com.example.app.provider.userdata"
        const val READ_PERMISSION = "com.example.app.permission.READ_USER_DATA"
        const val SENSITIVE_PERMISSION = "com.example.app.permission.ACCESS_SENSITIVE_DATA"
        
        // URI匹配码
        const val USERS = 1
        const val USER_ID = 2
        const val SENSITIVE = 3
    }
}

1.3 动态URI权限授予

// 数据提供方
fun shareDataWithApp(targetPackage: String) {
    val contentUri = Uri.parse("content://$AUTHORITY/public/shared_data")
    
    // 创建临时授权Intent
    val intent = Intent(Intent.ACTION_VIEW).apply {
        data = contentUri
        `package` = targetPackage
        flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    }
    
    // 可选:持久化授权(重启后仍有效)
    context.grantUriPermission(
        targetPackage, 
        contentUri, 
        Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
    )
    
    startActivity(intent)
}

// 数据接收方
fun accessSharedData(uri: Uri) {
    try {
        contentResolver.query(uri, null, null, null, null)?.use { cursor ->
            // 处理数据
        }
    } catch (se: SecurityException) {
        // 处理权限异常
    }
}

二、FileProvider安全文件共享

2.1 配置与声明

<!-- res/xml/file_paths.xml -->
<paths>
    <files-path name="internal_files" path="." />
    <cache-path name="internal_cache" path="." />
    <external-files-path name="external_files" path="documents/" />
    <external-cache-path name="external_cache" path="." />
    <external-media-path name="external_media" path="." />
</paths>

<!-- AndroidManifest.xml -->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.app.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>
</provider>

2.2 安全共享文件

fun shareImage(imageFile: File) {
    val contentUri = FileProvider.getUriForFile(
        context, 
        "com.example.app.fileprovider", 
        imageFile
    )

    val shareIntent = Intent(Intent.ACTION_SEND).apply {
        type = "image/*"
        putExtra(Intent.EXTRA_STREAM, contentUri)
        
        // 关键:授予临时访问权限
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }

    startActivity(Intent.createChooser(shareIntent, "分享图片"))
}

三、广播通信权限控制

3.1 带权限的广播发送

// 发送带权限的广播
fun sendSecureBroadcast() {
    val intent = Intent("com.example.app.ACTION_SECURE_EVENT").apply {
        putExtra("data", "敏感信息")
    }
    
    // 只有持有指定权限的接收器才能接收
    sendBroadcast(intent, "com.example.app.permission.RECEIVE_SECURE_BROADCAST")
}

3.2 受保护的广播接收器

<!-- 接收方声明 -->
<receiver 
    android:name=".SecureBroadcastReceiver"
    android:permission="com.example.app.permission.SEND_SECURE_BROADCAST"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.app.ACTION_SECURE_EVENT"/>
    </intent-filter>
</receiver>
class SecureBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // 验证发送方身份
        if (!isValidSender(context)) {
            abortBroadcast()
            return
        }
        
        // 处理广播数据
        val data = intent.getStringExtra("data")
    }
    
    private fun isValidSender(context: Context): Boolean {
        // 检查发送方证书签名
        val packageManager = context.packageManager
        val callingUid = Binder.getCallingUid()
        val packageName = packageManager.getNameForUid(callingUid) ?: return false
        
        return packageManager.checkSignatures(
            context.packageName,
            packageName
        ) == PackageManager.SIGNATURE_MATCH
    }
}

四、跨技术方案对比

特性ContentProviderFileProviderBroadcastSharedPreferences
数据粒度行级控制文件级消息级键值对
权限模型声明式+运行时URI授权发送/接收控制无原生控制
适用场景结构化数据文件共享事件通知简单配置
安全性★★★★★★★★★☆★★★☆☆★☆☆☆☆
实现复杂度

五、自定义权限深度应用

5.1 定义签名级权限

<permission
    android:name="com.example.app.permission.INTERNAL_API"
    android:protectionLevel="signature"
    android:label="内部API访问权限"
    android:description="允许访问内部API,仅限相同签名应用"/>

5.2 权限使用与验证

// 服务端验证
fun verifyCallerSignature(context: Context): Boolean {
    val callingUid = Binder.getCallingUid()
    val packageManager = context.packageManager
    val callerPackage = packageManager.getPackagesForUid(callingUid)?.firstOrNull()
        ?: return false
    
    return packageManager.checkSignatures(
        context.packageName, 
        callerPackage
    ) == PackageManager.SIGNATURE_MATCH
}

六、Scoped Storage最佳实践

// 使用MediaStore保存图片
fun saveImageToGallery(bitmap: Bitmap, context: Context) {
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "my_image.jpg")
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
        }
    }

    val resolver = context.contentResolver
    val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
    
    uri?.let {
        resolver.openOutputStream(it)?.use { os ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os)
        }
    }
}

// 通过SAF访问文件
fun openDocument() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "*/*"
    }
    startActivityForResult(intent, REQUEST_CODE_OPEN_DOC)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_OPEN_DOC && resultCode == RESULT_OK) {
        data?.data?.let { uri ->
            // 获取持久化访问权限
            contentResolver.takePersistableUriPermission(
                uri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION
            )
            
            // 使用URI访问文件
            contentResolver.openInputStream(uri)?.use { input ->
                // 处理文件内容
            }
        }
    }
}

七、权限管理流程图解

7.1 ContentProvider访问控制流程

sequenceDiagram
    participant ClientApp
    participant System
    participant ProviderApp
    
    ClientApp->>System: query(content://provider/data)
    System->>ProviderApp: 传递请求
    ProviderApp->>ProviderApp: 检查android:readPermission
    alt 有权限
        ProviderApp->>ProviderApp: 执行查询
        ProviderApp-->>System: 返回Cursor
        System-->>ClientApp: 返回数据
    else 无权限
        ProviderApp-->>System: 抛出SecurityException
        System-->>ClientApp: 权限错误
    end

7.2 URI动态授权流程

graph TD
    A[提供方应用] -->|1. 创建Content URI| B
    B[生成Intent] -->|2. 添加FLAG_GRANT_URI_PERMISSION| C
    C[系统处理] -->|3. 授予目标应用临时权限| D
    D[目标应用] -->|4. 通过ContentResolver访问URI| E
    E[系统验证] -->|5. 检查临时权限| F
    F{权限有效?} -->|是| G[允许数据访问]
    F -->|否| H[抛出SecurityException]

八、安全最佳实践与性能优化

  1. 权限最小化原则

    <!-- 显式设置exported属性 -->
    <activity android:exported="false"/>
    <service android:exported="false"/>
    
  2. 深度防御策略

    // 在ContentProvider中二次验证
    override fun insert(uri: Uri, values: ContentValues?): Uri {
        // Manifest声明的权限检查
        checkWritePermission()
        
        // 运行时二次验证
        if (isSensitiveUri(uri)) {
            val caller = callingPackage
            if (!isTrustedPackage(caller)) {
                throw SecurityException("Untrusted package: $caller")
            }
        }
        // ...
    }
    
  3. URI权限回收

    // 在适当时机回收权限
    fun revokeUriPermissions() {
        val uri = Uri.parse("content://$AUTHORITY/public/shared_data")
        context.revokeUriPermission(uri, 
            Intent.FLAG_GRANT_READ_URI_PERMISSION or 
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
    }
    
  4. Binder调用优化

    // 使用ParcelFileDescriptor传输大文件
    fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
        val file = File(getContext().filesDir, uri.lastPathSegment)
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    }
    

九、前沿技术与扩展

9.1 Android 12更细粒度媒体权限

// 请求特定媒体类型权限
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    arrayOf(
        Manifest.permission.READ_MEDIA_IMAGES,
        Manifest.permission.READ_MEDIA_VIDEO
    )
} else {
    arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}

requestPermissions(permissions, MEDIA_PERMISSION_REQUEST)

9.2 使用AppSearch实现安全数据共享

// 配置共享数据模式
val schema = AppSearchSchema.Builder("UserSchema")
    .addProperty(....)
    .build()

val setSchemaRequest = SetSchemaRequest.Builder()
    .addSchemas(schema)
    .setSchemaTypeVisibilityForPackage(
        "UserSchema", 
        /* visible= */ true,
        /* packageName= */ "com.trusted.app"
    ).build()

val future = session.setSchema(setSchemaRequest)

十、关键点总结

  1. 权限控制三原则:最小权限、显式声明、运行时验证

  2. ContentProvider最佳实践

    • 使用<path-permission>实现细粒度控制
    • 结合grantUriPermission实现安全数据共享
    • 在查询方法中执行二次验证
  3. 文件共享安全

    • 始终使用FileProvider代替file:// URI
    • 设置android:grantUriPermissions="true"
    • 及时回收不再需要的URI权限
  4. 防御性编程

    // 典型的安全检查模板
    fun sensitiveOperation() {
        // 1. 检查声明权限
        checkPermission(MANIFEST_PERMISSION)
        
        // 2. 验证调用方身份
        validateCallerIdentity()
        
        // 3. 校验输入参数
        validateInputParameters()
        
        // 4. 执行核心逻辑
        executeCoreLogic()
    }
    
  5. 性能优化要点

    • 使用ParcelFileDescriptor传输大文件
    • 分页加载大数据集(Paging 3.0)
    • 异步处理耗时操作(协程/WorkManager)
  6. 前沿适配

    • Android 12+使用分区存储媒体权限
    • 使用AppSearch替代共享Preferences
    • 适配PendingIntent可变性标志

最佳实践建议:对于新项目,优先采用ContentProvider + URI动态授权方案;对于文件共享,必须使用FileProvider;跨应用通信考虑自定义签名级权限。始终在AndroidManifest.xml中显式设置android:exported属性,这是Android 12+的强制要求。