简介:
动态运行时权限的检测流程分析:(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.jar
Android 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 、插入生成的代码生成文件供运行时调用。