Android: 动态运行时权限(危险权限)源码分析、封装、及9.0权限改动

简介:

动态运行时权限的检测流程分析:(Android 6.0开始就有了危险权限)

  • ActivityCompat.requestPermissions()
  • ActivityCompat.shouldShowRequestPermissionRationale()
  • ActivityCompat.checkSelfPermission(value)

Android 9.0 对于权限处理和6.0的有较大区别。

在6.0中,申请了权限组中当中的某个组的子权限,比如申请写入外部存储空间的权限,系统同时会授予读外部存储空间的权限。

Android 9.0:如果只申请了权限组中的某个子权限,就只会授予该权限的使用,该权限组的其他子权限在使用的时候需要额外的去申请。

运行时权限涉及安全校验。【开发中很少涉及】

关于哪些是危险权限,实际请以Android Developer 官网资料为准。

Android 权限 = 普通权限 + 危险权限

权限组 子权限
SMS 短信 SEND_SMS      RECEIVE_SMS    READ_SMS   RECEIVE_WAP_PUSH
STORAGE 存储卡 READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE
CONTACTS 联系人

READ_CONTACTS  WRITE_CONTACTS  GET_ACCOUNTS

PHONE 手机 READ_PHONE_STATE  CALL_PHONE  READ_CALL_LOG  WRITE_CALL_LOG
CALENDAR日历 READ_CALENDAR  WRITE_CALENDAR
CAMERA 相机 CAMERA
LOCATION 位置 ACCESS_FINE_LOCATION  ACCESS_COASE_LOCATION
BODY_SENSORS BODY_SENSORS
MICROPHONE 麦克风 RECORD_AUDIO

相关概念:

1. uid 与 pid:

uid:本身是linux权限系统中用以区分用户身份的标识。android是单一用户权限系统,uid在android里可以理解为应用的标识Id,该Id自安装之日起就被分配,始终不变,除非重新安装。

pid:进程唯一标识id,用以和远程服务交互。

2. appId与callerId:

appId: 可理解为应用Id,和uid一样。是本地应用在远程服务中的叫法。

callerId: 是服务请求者的身份Id,可以是本地应用的身份id,可以是远程服务的身份标识id。


查看android.jar中隐藏的API: [关于hidden api 的官方措施,可查看bilibili上的Goolge账号进行查看视频]

Android Hidden API是经过修改的jar文件,它将android.jarAndroid SDK中的和framework.jar真实设备中的结合在一起。这个jar可以在开发中使用Android的内部/隐藏API。

什么是Android内部和隐藏API? 内部API位于com.android.internal包中,可framework.jar从真实的Android设备在文件中找到,而隐藏的API位于android.jar具有@hide javadoc属性的文件中。尽管类和方法是public您无法直接访问的。您可以从此软件包中使用许多的方法和资源。假定这是一个API,并将其称为隐藏API。

有什么好处? 当您想在项目中使用类似的函数时,无需创建新的类,常量,方法或资源。例如,如果您要创建一个String包含“接受”一词的新资源。然后,您想为另一种语言(例如阿拉伯语,印度尼西亚语)创建新的翻译,即使世界上所有语言也是如此。这太累了,浪费时间。您为什么不使用com.android.internal.R.string.accept通过检索的内容InternalAccessor.getString("accept")

  • Github上已有前辈去除了android.jar中的@hide注解:https://github.com/anggrayudi/android-hidden-api
  • 下载对应API版本Android.jar
  • 替换SDK/platforms/android-版本/Android.jar
  • 新建工程并更改targetVerison 、 compileVersion =  28 
  • 重新打开AS查看。

(以28为示例)

替换后新建一个项目改动如下


源码分析:

1. checkSelfPermission:

跳到了context里面,这里是抽象方法,众所周知,context类的实现类是ContextImpl,所以跳转到该类去查看本方法的实现。【关于context的理解可以看源码】

直接拿到了ActivityManager的实例【AMS家族~可能说法不准确,好像AM不属于AMS家族,可以分析一下系统和应用程序进程启动流程的源码】

如果appid是ROOTID/SYSTEM_ID即如果是根应用或系统应用就直接返回允许授权,否则返回拒绝授权。

如果ActivityManager的实例不为空就调用它的checkPermission(),这里就会走到远程服务【看catch异常~】

如果申请的权限为空,就直接返回未授权。

不为空就调用checkComponentPermission()

如果申请权限的pid等于ActivityManagerService所在的进程id就直接返回该权限已授权,否则继续

ActivityManager.checkComponentPermission()

这里通过uid得到一个appid,又判断了ROOT_UID SYSTEM_UID就直接返回该权限已授权。

然后当执行

是判断如果我的进程是隔离进程就直接返回未被授权。【隔离进程:android源码里面有个进程的范围,如果有些进程被列为黑名单,则不允许获取权限】。

继续往下看,

如果uid和owningUid是来自同一个APP,这时需要去申请自己应用创建的权限,直接返回已授权。【关于自定义权限可查阅官方资料】

在上面exported传入的是true,意思是可以被外界访问【别的应用访问】

这段源码主要是取出包对象并调用checkUidPermission()

可以看见爆红无法跳转,于是看mPermissionManager是怎么来的:

发现是自然要去这个类找:

[上图截错了,懒得改,这里说下,应该查找下图中的类:自己看哈~]

通过callingUid得到callingUserId。uid是应用的标识,callinguserid是指访问者的id。主要是给goolge小程序使用的。

isCallerInstantApp、isUidInstantApp国内都用不到,具体可查阅google android的容器化技术instant app。

然后接着看if包对象是否为null,mSharedUserId ull,mSharedUserId 一般为空(除非在清单文件中配置了)

然后是filterAppAccess是进行列表当中。再后就是通过package对象取出权限列表getPermissionsState()。

如果权限已经被授权就,这时无论是isUidInstantApp还是普通的app都会返回已授权。

如果申请的名称是ACCESS_COARSE_LOCATION,并已经拥有ACCESS_FINE_LOCATION,就会告诉我们已授权。

如果package对象为null则走下面的逻辑:

通过uid取出该应用的权限列表,循环判断是否包含了申请的权限名称,如果已包含则说明已经授权。

2.shouldShowRequestPermission:

检查申请权限之前需不要告诉用户为什么需要该权限。

api大于等于23才走这个流程,否则返回false,即不向用户解释。

它是个抽象类,且PackageManager的实现类是ApplicationPackageManager

这里的mPM是PackMagerService的实例:

这里会检查callinguserid是不是来自小程序、instantApp等,如果不等于宿主userid的就会进行强校验:

如果来自小程序、插件等权限申请,且该权限没有被授权,就会抛出安全异常。

回过去继续看:

检查权限如果已经等于授权了就不管了,不再展示对话框告诉用户为什么需要授权。

再后看:如果权限的flag标记拥有

都会直接返回false,即不需要向用户展示。

如果一个权限具有这3个flag,说明是被系统固定的、被策略权限固定的、被用户手动设定的,是不用向用户解释的。

如果运行时去动态改变的,则会返回true.。

3. requestPermission:

当api 大于23

并activity是RequestPermissionsRequestCodeValidator的实例的话就调用validateRequestPermissionsRequestCode()检查一下我们传递的requestCode.是否合法【必须低于16位的整形】

然后调用activty的requestPermissions:

如果已经发起过了一次权限申请就会return并立马回调权限申请结果。

这里是创建一个隐式意图,并放入了参数【英语写的很明白了,不赘述】,

即调用了实际上是启动了一个Activity,通过隐式意图启动的。【可以运行模拟器,并在cmd中输入adb shell dumpsys activity top查看,然后通过在线的源码查看平台查看该具体的启动的acitvity的源码,直到查看到源码中的AppPermissionGroup.java完后回到AndroidStudio上继续查看】

回到Android Studio,查看到grantRuntimePermission:

这里又是走到远程服务里去了。

 

这里会做一些安全检测,并做强制检查。

细节掠过。。。。

如果小于23就return 掉即不申请【android 6.0后才需要授权】。

开发阶段的权限的逻辑:【DEBUG阶段才能申请的权限】

后面的内容就自己查看源码啦~~


AbstractProcessor 调试方式

  • gradle.properties文件下:增加

org.gradle.daemon = true

org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

  • 项目的terminal中运行gradle-daemon开启守护进程
  • 配置调试程序
  • 点击调试启动按钮,在项目的terminal中运行gradle clean assembleDebug运行项目

然后设置断点调试


AOP运行时权限检测的封装: (码云)

如果是平常的申请一条都需要写很多代码:

东西多了就会很臃肿。

能不能发起请求将结果送到自定义的方法呢?而不是回调到onRequestPermissionResult()【用代码自动生成文件】

/**
 * 几种类型的Element:
 * 1. ExecutableEelement:   获取方法
 * 代表一个类或接口的方法,构造器或初始块(静态或实例的),包括注解类型
 * 2. TypedElement:  操作类
 *  程序中的类或接口提供访问该类型和其内部成员的一些方法
 * 3. PackageElement: 操作包
 *  代表包的接口
 *  4. TypeParameterElement:
 *  代表泛型类,接口,方法或构造器的形式类型参数
 */

由于用到编译时注解,所以创建一个Module:

编译时注解处理器的标配写法:

package com.yinlei.libcompile;

import com.google.auto.service.AutoService;
import com.yinlei.libanotaion.PermissionDenied;
import com.yinlei.libanotaion.PermissionGrant;
import com.yinlei.libanotaion.PermissionRational;

import java.util.HashSet;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;


@AutoService(Processor.class)
public class RuntimePermissionAbstractProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
//        return super.getSupportedAnnotationTypes();
        HashSet<String> supportList = new HashSet();
        supportList.add(PermissionGrant.class.getCanonicalName());
        supportList.add(PermissionDenied.class.getCanonicalName());
        supportList.add(PermissionRational.class.getCanonicalName());
        return supportList;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
//        return super.getSupportedSourceVersion();
        return SourceVersion.latestSupported();
    }
}
package com.yinlei.runtimepermissionanalyze;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_CODE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 21;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                // alert ,dialog 什么是哦胡返回true、false

            }else{
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_PERMISSIONS_WRITE_EXTERNAL_STORAGE);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        
        switch (requestCode){
            case REQUEST_CODE_PERMISSIONS_WRITE_EXTERNAL_STORAGE:
                StringBuilder buildr = new StringBuilder();
                for (int i = 0, len = grantResults.length; i < len; i++) {
                    if (grantResults[i] != PackageManager.PERMISSION_GRANTED){
                        buildr.append(permissions[i]);
                    }
                }
                if(buildr.length()>0){
                    new AlertDialog.Builder(this).setTitle("权限授权提示")
                            .setMessage("请授权一下,以继续功能的使用\n\n"+buildr.toString())
                            .setPositiveButton("ok", new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    
                                }
                            }).create().show();
                }
                break;
                default:
                    break;
        }
    }
}

封装后的源码地址:

package com.yinlei.libcompile;

import com.google.auto.service.AutoService;
import com.yinlei.libanotaion.PermissionDenied;
import com.yinlei.libanotaion.PermissionGrant;
import com.yinlei.libanotaion.PermissionRational;

import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;


@AutoService(Processor.class)
public class RuntimePermissionAbstractProcessor extends AbstractProcessor {
    private Elements elementUtils;
    private Messager messager;

    private HashMap<String, MethodInfo> methodMap = new HashMap<>();
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        elementUtils = processingEnvironment.getElementUtils();//Activity有类的概念、变量、方法、参数,这些都称为元素,ElementUtils就可以方便的操作元素
        messager = processingEnvironment.getMessager();// 方便开发的时候做日志输出,这里不能用log
        //filer是用来生成文件的
        filer = processingEnvironment.getFiler();

    }

    /**
     *
     * @param set
     * @param roundEnvironment
     * @return false 为不处理,true为处理
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {//代码的真正的执行
// 以类为单位,收集每个类中打上了我们自定义注解的方法
        methodMap.clear();

        messager.printMessage(Diagnostic.Kind.NOTE, "process start !");
        if(!handleAnotationInfo(roundEnvironment,PermissionGrant.class)){
            return false;
        }
        if(!handleAnotationInfo(roundEnvironment,PermissionDenied.class)){
            return false;
        }
        if (!handleAnotationInfo(roundEnvironment,PermissionRational.class)){
            return false;
        }

        for(String className : methodMap.keySet()){
            MethodInfo methodInfo = methodMap.get(className);
            try {
                JavaFileObject sourceFile = filer.createSourceFile(methodInfo.packageName + "." + methodInfo.fileName);
                Writer writer = sourceFile.openWriter();
                writer.write(methodInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
                messager.printMessage(Diagnostic.Kind.NOTE,"write file failed: "+e.getMessage());
            }

        }
        messager.printMessage(Diagnostic.Kind.NOTE, "process end !");
        return false;
    }

    private boolean handleAnotationInfo(RoundEnvironment roundEnvironment, Class<? extends Annotation> annotation) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(annotation);
        for (Element element : elements) {//此处得到的元素是加了注解的方法
            if(!checkMethodValidator(element, annotation)){ // 检查方法是否有效
                return false;
            }
            //检验通过,是个有效的方法,那么这个元素就是个ExecutableElement
            ExecutableElement methodElement = (ExecutableElement) element;
            TypeElement enclosingElement = (TypeElement) methodElement.getEnclosingElement();// 得到其父节点【即得到元素的类名 MainActivity所在的层级的节点】
            String className = enclosingElement.getQualifiedName().toString();

            MethodInfo methodInfo = methodMap.get(className);
            if(methodInfo == null){
                methodInfo = new MethodInfo(elementUtils, enclosingElement);
                methodMap.put(className, methodInfo);
            }

            //得到方法上标记的注解
            Annotation annotationClaz = methodElement.getAnnotation(annotation);
            String methodName = methodElement.getSimpleName().toString();//得到该方法的名称
            List<? extends VariableElement> parameters = methodElement.getParameters();//得到该方法的入参

            if(parameters == null || parameters.size()<1){
                String message = "the method %s marked by annotation %s must have an unique parmater [String[] permissions] paramter";
                throw new IllegalArgumentException(String.format(message,methodName,annotationClaz.getClass().getSimpleName()));
            }
            if(annotationClaz instanceof PermissionGrant){
                int reqeustCode = ((PermissionGrant)annotationClaz).value(); //得到注解上传入的值,即Mainactivity方法上自定义注解传入的请求码
                methodInfo.grantMethodMap.put(reqeustCode, methodName);
            }else if (annotationClaz instanceof PermissionDenied){
                int reqeustCode = ((PermissionDenied)annotationClaz).value(); //得到注解上传入的值,即Mainactivity方法上自定义注解传入的请求码
                methodInfo.deniedMethodMap.put(reqeustCode, methodName);
            }else if (annotationClaz instanceof PermissionRational){
                int reqeustCode = ((PermissionRational)annotationClaz).value(); //得到注解上传入的值,即Mainactivity方法上自定义注解传入的请求码
                methodInfo.rationalMethodMap.put(reqeustCode, methodName);
            }

        }

        return true;
    }

    private boolean checkMethodValidator(Element element, Class<? extends Annotation> annotation) {
        if(element.getKind()!= ElementKind.METHOD){//得到的元素类型不是方法类型则不处理
            return false;
        }
        if(ClassValidator.isPrivate(element) || ClassValidator.isAbstract(element)){// 方法是private , abstact的也处理不掉
            return false;
        }
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
//        return super.getSupportedAnnotationTypes();
        HashSet<String> supportList = new HashSet();
        supportList.add(PermissionGrant.class.getCanonicalName());
        supportList.add(PermissionDenied.class.getCanonicalName());
        supportList.add(PermissionRational.class.getCanonicalName());
        return supportList;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
//        return super.getSupportedSourceVersion();
        return SourceVersion.latestSupported();
    }
}

总结:

1. 编译时:

设置支持的注解类型集合 - ------》 methodInfo 收集每个注解类型标记的方法 ----------》 遍历注解标记方法集合,利用动态注入代码、生成文件。

2. 运行时:

根据context.class找到生成的PermissionProxy类 --------》 Class.forName(clazz.getName())生成代理类对象 ---------》根据申请结果,把申请结果分发给PermissionProxy类的grant 、 denied 、rationale方法--------》各个方法根据switch request code 执行context对象对应的方法。

3. 权限申请的安全校验:

由client携带自己的pid ,uid身份发起i请求,server端收到请求后,拿到身份id,去验证。

4. 权限申请对话框:

弹出的权限申请对话框是Activity-----GrantPermissionActivity(前天看群里的一个老哥的需求是不要系统的原始的请求权限对话框。)

5.权限的持久化:

每次权限变更后都会写入runtime-permissions.xml中。

6. AOP编译时的代码生成:

断点调试abstractProcessor 、插入生成的代码生成文件供运行时调用。

发布了307 篇原创文章 · 获赞 45 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_39969226/article/details/104188173