四、组件安全:Activity安全、Service安全、BroadcastReceiver安全、ContentProvider安全
Android的四大组件,说白了就是应用的四个"入口"。每个入口都可能被外部应用调用,也可能被恶意利用。我这些年做安全审计,十有八九的漏洞都跟组件暴露有关。今天咱们就一个一个捋清楚。
核心观点:组件安全的核心就一句话——最小暴露原则。不该对外暴露的,坚决不暴露;必须暴露的,做好权限校验。
4.1 Activity 安全
Activity是用户直接打交道的组件。你想想看,如果一个Activity被恶意应用直接拉起,会发生什么?轻则界面被劫持,重则用户密码被钓鱼。
最常见的坑:在AndroidManifest里给Activity加了
⚠️ 注意:只要Activity包含
我遇到过这样一个案例:某社交App的"重置密码"Activity加了
安全编码建议:
- 所有Activity默认设置
android:exported="false" - 必须暴露的Activity,做好调用方身份校验
- 敏感操作前检查调用来源
- 使用
getCallingActivity()判断调用者
// 安全示例:校验调用者身份
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 检查是谁调用了这个Activity
ComponentName caller = getCallingActivity();
if (caller == null || !"com.yourapp.trusted".equals(caller.getPackageName())) {
// 非法调用,直接关闭
finish();
return;
}
// 正常业务逻辑
setContentView(R.layout.activity_sensitive);
}
4.2 Service 安全
Service在后台默默干活,容易被忽视。但恰恰是这种"没人注意"的组件,最容易出问题。
Service的安全风险主要有两个:权限提升和拒绝服务。恶意应用可以启动你的Service消耗资源,或者通过Service获取敏感数据。
💡 我的习惯:所有Service都先设成exported="false",除非明确需要跨进程调用。而且我建议用IntentService代替普通Service,生命周期更可控。
安全编码要点:
- Service默认不导出
- 必须导出的Service,声明
android:permission - 在
onStartCommand()中校验Intent参数 - Android 5.0以上禁止隐式启动Service
<!-- AndroidManifest.xml 安全配置 -->
<service
android:name=".SecureService"
android:exported="true"
android:permission="com.yourapp.permission.ACCESS_SECURE_SERVICE">
</service>
// Service 内部校验
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null || intent.getAction() == null) {
stopSelf();
return START_NOT_STICKY;
}
// 校验Intent中的参数
String data = intent.getStringExtra("key_data");
if (data == null || !isValidData(data)) {
// 参数不合法,拒绝处理
stopSelf();
return START_NOT_STICKY;
}
// 处理业务逻辑
handleData(data);
return START_NOT_STICKY;
}
4.3 BroadcastReceiver 安全
BroadcastReceiver是组件安全的重灾区。为什么?因为广播是全局的,任何应用都能发也能收。
我记得有一次帮客户做安全审计,发现他们的动态广播接收器没有做任何权限限制。结果第三方应用不停地发送伪造广播,导致App频繁弹窗,用户体验极差。这还算轻的,更严重的是数据泄露。
广播安全三大原则:
- 能用局部广播就别用全局广播——LocalBroadcastManager是你的好朋友
- 动态注册比静态注册更安全——生命周期可控
- 敏感广播必须加权限——发送和接收都要加
重点:Android 8.0(API 26)开始,静态注册的广播接收器大部分不能用了。这是好事,因为动态注册更安全。但要注意,动态注册记得在不需要时取消注册。
// 安全的广播注册方式
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 校验发送者权限
if (!hasRequiredPermission(context, intent)) {
return;
}
// 处理广播
handleBroadcast(intent);
}
};
// 注册时指定权限
IntentFilter filter = new IntentFilter("com.yourapp.SECURE_ACTION");
registerReceiver(mReceiver, filter, "com.yourapp.permission.RECEIVE_SECURE", null);
// 别忘了取消注册
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}
4.4 ContentProvider 安全
ContentProvider是四大组件里最容易出数据泄露的。它直接操作数据,一旦权限没设好,数据库就裸奔了。
我曾经见过一个App,ContentProvider的exported设成了true,但readPermission和writePermission都没配。结果呢?任何App都能读取用户的通讯录和聊天记录。这简直是灾难。
ContentProvider安全配置表:
| 配置项 | 说明 | 推荐值 |
|---|---|---|
| android:exported | 是否允许外部访问 | false(除非必须) |
| android:readPermission | 读取权限 | 自定义签名权限 |
| android:writePermission | 写入权限 | 自定义签名权限 |
| android:grantUriPermissions | 临时授权 | 谨慎使用 |
⚠️ 避坑指南:我曾经遇到一个案例,开发者在ContentProvider里用了grantUriPermissions="true",但没有做临时权限回收。结果一个恶意App拿到Uri后,在权限过期前疯狂读取数据。记住:临时授权一定要设置有效期。
// ContentProvider 安全实现示例
public class SecureProvider extends ContentProvider {
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 1. 校验Uri
if (!isValidUri(uri)) {
throw new IllegalArgumentException("Invalid URI");
}
// 2. 参数化查询,防止SQL注入
String table = getTableName(uri);
SQLiteDatabase db = getReadableDatabase();
return db.query(table, projection, selection, selectionArgs,
null, null, sortOrder);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 校验输入数据
if (!validateInput(values)) {
return null;
}
// 安全插入
return super.insert(uri, values);
}
// Uri校验
private boolean isValidUri(Uri uri) {
return uri != null &&
"content".equals(uri.getScheme()) &&
"com.yourapp.provider".equals(uri.getAuthority());
}
}
4.5 组件安全总结
说了这么多,其实核心就三点:
- 默认不暴露——exported="false"是默认选项
- 暴露必校验——权限、身份、数据都要验
- 输入皆有毒——所有外部来的数据都不可信
做安全开发这么多年,我最大的体会是:安全不是加功能,而是减风险。少暴露一个组件,就少一个攻击面。这个道理,越早明白越好。
💡 最后分享一个小技巧:写完代码后,用adb shell dumpsys package com.yourapp查看所有组件的exported状态。这是我每次上线前必做的检查。
公众号:蓝海资料掘金营,微信deep3321