Signature 权限使用完整 Demo
整体架构
text
📦 APP A (主应用: com.ambank.ambankonline)
├── 声明权限 (declare)
├── 保护组件 (protect)
└── 用权限守住大门
📦 APP B (辅助应用: com.ambank.ambankhelper)
├── 申请权限 (request)
└── 访问 APP A 的组件
🔑 两个 APP 用同一把密钥签名 → 才能通信
一、APP A(主应用)— 声明 & 使用权限
1. AndroidManifest.xml
XML
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ambank.ambankonline">
<!-- ========== 第一步:声明三个自定义权限 ========== -->
<permission
android:name="com.ambank.ambankonline.permission.PROCESS_PUSH_MSG"
android:label="处理推送消息"
android:description="@string/perm_process_push_desc"
android:protectionLevel="signature" />
<permission
android:name="com.ambank.ambankonline.permission.PUSH_PROVIDER"
android:label="读取推送数据"
android:description="@string/perm_push_provider_desc"
android:protectionLevel="signature" />
<permission
android:name="com.ambank.ambankonline.permission.PUSH_WRITE_PROVIDER"
android:label="写入推送数据"
android:description="@string/perm_push_write_desc"
android:protectionLevel="signature" />
<application ...>
<!-- ========== 第二步:用权限保护 BroadcastReceiver ========== -->
<!-- 权限1: PROCESS_PUSH_MSG 保护推送消息接收器 -->
<receiver
android:name=".receiver.PushMessageReceiver"
android:permission="com.ambank.ambankonline.permission.PROCESS_PUSH_MSG"
android:exported="true">
<intent-filter>
<action android:name="com.ambank.ambankonline.ACTION_PUSH_MSG" />
</intent-filter>
</receiver>
<!-- ========== 第三步:用权限保护 ContentProvider ========== -->
<!-- 权限2 & 3: 分别保护读/写操作 -->
<provider
android:name=".provider.PushDataProvider"
android:authorities="com.ambank.ambankonline.pushprovider"
android:exported="true"
android:readPermission="com.ambank.ambankonline.permission.PUSH_PROVIDER"
android:writePermission="com.ambank.ambankonline.permission.PUSH_WRITE_PROVIDER" />
</application>
</manifest>
2. PushMessageReceiver.java(被权限1保护)
Java
package com.ambank.ambankonline.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* 推送消息接收器
* 被 PROCESS_PUSH_MSG 权限保护
* 只有同签名的 APP 发送的广播才能触发
*/
public class PushMessageReceiver extends BroadcastReceiver {
private static final String TAG = "PushMsgReceiver";
@Override
public void onReceive(Context context, Intent intent) {
// 能走到这里,说明发送者已经通过了签名验证 ✅
String title = intent.getStringExtra("title");
String body = intent.getStringExtra("body");
String transactionId = intent.getStringExtra("transaction_id");
Log.d(TAG, "收到推送: " + title);
// 处理推送消息(比如显示通知、更新UI等)
handlePushMessage(context, title, body, transactionId);
}
private void handlePushMessage(Context context, String title,
String body, String transactionId) {
// 显示通知
Log.d(TAG, "处理推送消息: title=" + title
+ ", body=" + body
+ ", txnId=" + transactionId);
// 例如:显示转账到账通知
// NotificationHelper.show(context, title, body);
}
}
3. PushDataProvider.java(被权限2、3保护)
Java
package com.ambank.ambankonline.provider;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
/**
* 推送数据提供者
* 读操作被 PUSH_PROVIDER 权限保护
* 写操作被 PUSH_WRITE_PROVIDER 权限保护
*/
public class PushDataProvider extends ContentProvider {
private static final String TAG = "PushDataProvider";
private static final String AUTHORITY = "com.ambank.ambankonline.pushprovider";
private static final String TABLE_NAME = "push_messages";
private SQLiteOpenHelper dbHelper;
@Override
public boolean onCreate() {
dbHelper = new PushDbHelper(getContext());
return true;
}
/**
* 查询推送记录
* 需要 PUSH_PROVIDER (读权限) 才能调用
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 能执行到这里,说明调用者有读权限(同签名)✅
Log.d(TAG, "查询推送数据 - 权限验证通过");
SQLiteDatabase db = dbHelper.getReadableDatabase();
return db.query(TABLE_NAME, projection, selection,
selectionArgs, null, null, sortOrder);
}
/**
* 插入推送记录
* 需要 PUSH_WRITE_PROVIDER (写权限) 才能调用
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
// 能执行到这里,说明调用者有写权限(同签名)✅
Log.d(TAG, "写入推送数据 - 权限验证通过");
SQLiteDatabase db = dbHelper.getWritableDatabase();
long id = db.insert(TABLE_NAME, null, values);
getContext().getContentResolver().notifyChange(uri, null);
return ContentUris.withAppendedId(uri, id);
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
return db.update(TABLE_NAME, values, selection, selectionArgs);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
return db.delete(TABLE_NAME, selection, selectionArgs);
}
@Override
public String getType(Uri uri) {
return "vnd.android.cursor.dir/push_messages";
}
// ========== 内部数据库 ==========
private static class PushDbHelper extends SQLiteOpenHelper {
PushDbHelper(android.content.Context context) {
super(context, "push.db", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, "
+ "title TEXT, "
+ "body TEXT, "
+ "timestamp INTEGER, "
+ "is_read INTEGER DEFAULT 0)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldV, int newV) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
}
二、APP B(辅助应用)— 请求权限 & 访问
1. AndroidManifest.xml
XML
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ambank.ambankhelper">
<!-- ========== 申请使用这三个权限 ========== -->
<uses-permission
android:name="com.ambank.ambankonline.permission.PROCESS_PUSH_MSG" />
<uses-permission
android:name="com.ambank.ambankonline.permission.PUSH_PROVIDER" />
<uses-permission
android:name="com.ambank.ambankonline.permission.PUSH_WRITE_PROVIDER" />
<application ...>
<activity android:name=".PushHelperActivity" />
</application>
</manifest>
2. PushHelperActivity.java(APP B 的调用代码)
Java
package com.ambank.ambankhelper;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/**
* APP B(辅助应用)
* 必须和 APP A 使用同一个签名密钥,否则以下操作全部失败
*/
public class PushHelperActivity extends AppCompatActivity {
private static final String TAG = "PushHelper";
private static final Uri PUSH_URI =
Uri.parse("content://com.ambank.ambankonline.pushprovider/push_messages");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_push_helper);
// 演示三种权限的使用
findViewById(R.id.btn_send_push).setOnClickListener(v -> sendPushMessage());
findViewById(R.id.btn_read_push).setOnClickListener(v -> readPushData());
findViewById(R.id.btn_write_push).setOnClickListener(v -> writePushData());
}
/**
* ========== 权限1: PROCESS_PUSH_MSG ==========
* 发送广播给 APP A 的 PushMessageReceiver
*/
private void sendPushMessage() {
try {
Intent intent = new Intent("com.ambank.ambankonline.ACTION_PUSH_MSG");
intent.setPackage("com.ambank.ambankonline"); // 指定目标包名
intent.putExtra("title", "转账成功");
intent.putExtra("body", "您已成功转账 RM 1,000.00");
intent.putExtra("transaction_id", "TXN20240101001");
// 发送带权限的广播
sendBroadcast(intent,
"com.ambank.ambankonline.permission.PROCESS_PUSH_MSG");
Log.d(TAG, "✅ 推送消息已发送");
Toast.makeText(this, "推送消息已发送", Toast.LENGTH_SHORT).show();
} catch (SecurityException e) {
// ❌ 如果签名不同,会抛出 SecurityException
Log.e(TAG, "❌ 权限被拒绝: " + e.getMessage());
Toast.makeText(this, "无权限发送推送", Toast.LENGTH_SHORT).show();
}
}
/**
* ========== 权限2: PUSH_PROVIDER (读) ==========
* 读取 APP A 的推送历史记录
*/
private void readPushData() {
try {
Cursor cursor = getContentResolver().query(
PUSH_URI, // URI
new String[]{"title", "body", "timestamp"}, // 列
"is_read = ?", // 条件
new String[]{"0"}, // 未读的
"timestamp DESC" // 排序
);
if (cursor != null) {
Log.d(TAG, "✅ 查询到 " + cursor.getCount() + " 条推送记录");
while (cursor.moveToNext()) {
String title = cursor.getString(
cursor.getColumnIndex("title"));
String body = cursor.getString(
cursor.getColumnIndex("body"));
Log.d(TAG, " 推送: " + title + " - " + body);
}
cursor.close();
}
Toast.makeText(this, "读取推送数据成功", Toast.LENGTH_SHORT).show();
} catch (SecurityException e) {
// ❌ 签名不同 → 无法读取
Log.e(TAG, "❌ 读取被拒绝: " + e.getMessage());
Toast.makeText(this, "无权限读取推送数据", Toast.LENGTH_SHORT).show();
}
}
/**
* ========== 权限3: PUSH_WRITE_PROVIDER (写) ==========
* 向 APP A 的推送数据库写入记录
*/
private void writePushData() {
try {
ContentValues values = new ContentValues();
values.put("title", "系统通知");
values.put("body", "您的密码将在30天后过期");
values.put("timestamp", System.currentTimeMillis());
values.put("is_read", 0);
Uri result = getContentResolver().insert(PUSH_URI, values);
Log.d(TAG, "✅ 写入成功: " + result);
Toast.makeText(this, "写入推送数据成功", Toast.LENGTH_SHORT).show();
} catch (SecurityException e) {
// ❌ 签名不同 → 无法写入
Log.e(TAG, "❌ 写入被拒绝: " + e.getMessage());
Toast.makeText(this, "无权限写入推送数据", Toast.LENGTH_SHORT).show();
}
}
}
三、验证效果对比
同签名 vs 不同签名
text
场景1: APP B 和 APP A 用同一把密钥签名
┌─────────────────────────────────────┐
│ sendPushMessage() → ✅ 成功发送 │
│ readPushData() → ✅ 成功读取 │
│ writePushData() → ✅ 成功写入 │
└─────────────────────────────────────┘
场景2: APP C(恶意APP)用不同密钥签名
┌─────────────────────────────────────┐
│ sendPushMessage() → ❌ SecurityException │
│ readPushData() → ❌ SecurityException │
│ writePushData() → ❌ SecurityException │
└─────────────────────────────────────┘
四、完整流程图
text
APP B (同签名) Android系统 APP A (主应用)
│ │ │
│ 1. 发送广播 │ │
│ (PROCESS_PUSH_MSG) │ │
│ ──────────────────────────► │ │
│ │ 2. 检查签名 │
│ │ APP B签名 == APP A签名? │
│ │ ✅ 是! │
│ │ ──────────────────────────► │
│ │ │ 3. onReceive()
│ │ │ 处理推送
│ │ │
│ 4. 查询Provider │ │
│ (PUSH_PROVIDER) │ │
│ ──────────────────────────► │ │
│ │ 5. 检查签名 ✅ │
│ │ ──────────────────────────► │
│ │ │ 6. query()
│ ◄───────────────────────────────────────────────────────── │
│ 返回 Cursor 数据 │
│ │ │
恶意APP (不同签名) Android系统
│ │
│ 发送广播 │
│ ──────────────────────────► │
│ │ 检查签名
│ │ 恶意APP签名 ≠ APP A签名
│ ◄────────────────────────── │ ❌ SecurityException!
│ 拒绝! │
五、签名配置(关键!)
Bash
# 两个 APP 必须用同一个 keystore 签名
# 签名 APP A
jarsigner -keystore ambank-release.keystore \
app-a-release.apk ambank_key
# 签名 APP B(用同一个 keystore 和同一个 alias)
jarsigner -keystore ambank-release.keystore \
app-b-release.apk ambank_key
# 这样两个 APP 的签名证书就一致了
# Android 系统会自动授予 signature 级别的权限
核心要点:代码层面不需要做任何额外的签名验证,Android 系统在安装时就自动完成了签名比对,应用层只需要声明(
<permission>)和使用(<uses-permission>)即可。
免责声明
本文档所有内容仅供安全研究、学术交流与技术学习使用,严禁用于任何未经授权的逆向破解、网络攻击、隐私窃取、恶意软件开发及其他违反《中华人民共和国网络安全法》《数据安全法》等法律法规的行为,使用者应确保已获得目标软件权利人的合法授权并自行承担因使用本文档内容所产生的一切法律责任与后果,作者不对任何直接或间接损害承担任何责任,继续阅读即视为您已知悉并同意上述全部条款。
浙公网安备 33010602011771号