Service安全:前台服务、绑定服务、权限检查
Service 是 Android 四大组件里最容易被忽视的安全短板。我见过太多应用,Activity 层防护做得滴水不漏,结果 Service 接口直接裸奔。说白了,Service 就像你家后门——前门锁得再好,后门开着也没用。
今天咱们就聊聊 Service 安全的三个核心点:前台服务、绑定服务、权限检查。每个点我都踩过坑,希望能帮你少走弯路。
前台服务:别让通知成为摆设
前台服务(Foreground Service)从 Android 8.0 开始就成了刚需。后台 Service 一杀就死,只有前台服务能活下来。但很多人只把它当「保活手段」,忽略了安全层面的东西。
前台服务的核心安全风险有两个:
- 通知欺骗:恶意应用可以伪造前台服务通知,诱导用户点击
- 资源耗尽:未授权的前台服务可能长期占用系统资源
我个人习惯,每次启动前台服务前,都会做两件事:
- 检查调用方是否有
FOREGROUND_SERVICE权限 - 验证通知内容是否来自可信来源
关键点:前台服务的通知必须包含明确的用户可识别信息,比如「正在录音」「正在定位」。别用「正在运行」这种模糊描述——用户看不懂,也容易成为钓鱼目标。
来看一段安全的前台服务启动代码:
// 安全的前台服务启动示例
public class SecureForegroundService extends Service {
private static final String TAG = "SecureForeground";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 1. 检查调用方权限
if (checkCallingPermission("android.permission.FOREGROUND_SERVICE")
!= PackageManager.PERMISSION_GRANTED) {
stopSelf();
return START_NOT_STICKY;
}
// 2. 验证通知来源
String action = intent.getAction();
if (!"com.example.ACTION_START_FOREGROUND".equals(action)) {
stopSelf();
return START_NOT_STICKY;
}
// 3. 构建安全的通知
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("安全扫描进行中")
.setContentText("正在检查应用权限...")
.setSmallIcon(R.drawable.ic_shield)
.setOngoing(true)
.build();
startForeground(NOTIFICATION_ID, notification);
return START_STICKY;
}
}
我的经验:我曾经接手过一个项目,前台服务通知里直接显示了用户手机号。这不仅是安全问题,还违反了隐私政策。记住:通知内容要最小化,只展示必要信息。
绑定服务:接口暴露的噩梦
绑定服务(Bound Service)通过 Binder 暴露接口,这玩意儿一旦没控制好,就是灾难。你想想看,任何应用都能 bindService 进来调用你的方法,多可怕。
绑定服务的安全要点:
- 使用
android:exported="false"限制外部访问 - 必须暴露时,用自定义权限做访问控制
- Binder 接口方法要做参数校验
我在项目中遇到过一个问题:某个金融应用的绑定服务暴露了转账接口,结果被恶意应用直接调用。原因就是没做调用方身份校验。嗯,这里要注意——Binder 接口的 onTransact() 方法里,一定要检查调用方的 UID 或 PID。
// 安全的 Binder 接口实现
public class SecureBinder extends IMyService.Stub {
@Override
public boolean transferMoney(String account, double amount) {
// 1. 获取调用方 UID
int callingUid = Binder.getCallingUid();
// 2. 检查 UID 是否属于我们的应用
if (callingUid != Process.myUid()) {
// 记录异常行为
Log.w(TAG, "Unauthorized call from UID: " + callingUid);
return false;
}
// 3. 参数校验
if (account == null || account.isEmpty()) {
throw new IllegalArgumentException("Invalid account");
}
if (amount <= 0 || amount > MAX_TRANSFER_LIMIT) {
throw new SecurityException("Amount out of range");
}
// 执行转账逻辑
return doTransfer(account, amount);
}
}
避坑指南:我曾经在 Binder 接口里直接返回了数据库查询结果,没做数据脱敏。结果被其他应用通过反射拿到了敏感数据。记住:Binder 返回的数据也要做权限校验和脱敏处理。
权限检查:别只依赖 manifest
很多人以为在 AndroidManifest 里声明了权限就万事大吉。其实不然——运行时权限检查才是真正的防线。特别是 Service 场景,调用方可能来自不同进程。
Service 权限检查的三种方式:
| 方式 | 适用场景 | 安全等级 |
|---|---|---|
| Manifest 声明 | 基础防护 | 低 |
| onBind/onStartCommand 检查 | 绑定服务、前台服务 | 中 |
| Binder 方法内检查 | 精细权限控制 | 高 |
我个人推荐第三种方式——在 Binder 方法内部做权限检查。这样即使 Service 被绑定,也能控制每个方法的访问权限。
// 在 Binder 方法内做权限检查
@Override
public void onBind(Intent intent) {
// 检查调用方是否有自定义权限
int check = checkCallingPermission("com.example.permission.ACCESS_SERVICE");
if (check != PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Missing permission");
}
// 记录调用方信息
String caller = getPackageManager()
.getNameForUid(Binder.getCallingUid());
Log.i(TAG, "Service bound by: " + caller);
return mBinder;
}
核心原则:权限检查要「层层设防」。Manifest 是第一道门,onBind 是第二道,Binder 方法是第三道。三道门都锁上,才能挡住大多数攻击。
知识体系总览
下面这张图总结了 Service 安全的三个核心维度,以及它们之间的关系:
这张图把 Service 安全的三个维度串起来了。前台服务负责「保活」和「通知安全」,绑定服务管「接口暴露」,权限检查做「访问控制」。三者缺一不可。
最后说一句:Service 安全没有银弹。我见过太多人只关注 Activity 安全,结果 Service 成了突破口。记住:攻击者永远找最薄弱的环节。把 Service 的安全做好,你的应用才算真正「锁好了后门」。