GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

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>)即可。

posted on 2026-03-28 18:33  GKLBB  阅读(12)  评论(0)    收藏  举报