围绕工信部开展的《APP侵害用户权益专项整治》337号函、《纵深推进APP侵害用户权益专项整治行动》164号函涉及的相关要求,展开说说,在APP开发过程中技术层面的合规注意事项。
-
问题类型:APP、SDK未告知用户收集个人信息的目的、方式、范围且未经用户同意,私自收集用户个人信息的行为。
-
问题类型:APP、SDK未向用户告知且未经用户同意,私自使用个人信息,将用户个人信息用于其提供服务之外的目的,特别是私自向其他应用或服务器发送、共享用户个人信息的行为。
-
Android系统:IMEI、MAC地址、MEID、IMSI、SN、ICCID等设备唯一标识符,Android ID、WiFi(WiFi名称、WiFi MAC地址以及设备扫描到的所有WiFi信息),SIM卡信息(IMSI、SIM卡序列号ICCID、手机号、运营商信息),应用安装列表(设备所有已安装应用的包名和应用名),传感器(传感器列表、加速度传感器、温度传感器等),蓝牙信息(设备蓝牙地址和设备扫描到的蓝牙设备信息),基站定位、GPS(用户地理位置信息),账户(各类应用注册的不同账号信息)、剪切板、IP地址、硬件序列号、SDCard信息(公有目录)等。
-
iOS系统:IDFA,IDFV,WiFi(bssid, ssid), GPS,运营商,传感器(加速器、陀螺仪、磁力器)、IP地址等。尤其注意苹果上架会检测是否调用TrueDepth APIs(面部追踪)。
-
同意《隐私政策》弹窗前不进行调用
-
不同意《隐私政策》弹窗进入APP时不进行调用(访客态或停留在APP内)
-
结合官方合规接入指南,注意不要在未同意隐私政策前初始化收集信息。
-
SDK无合理必要功能不能超范围收集信息,如,非定位型SDK默认收集位置信息等。
-
问题类型:APP、SDK非服务所必需或无合理应用场景,特别是在静默状态下或在后台运行时,超范围收集个人信息的行为。
❌ 每隔一分钟请求一次位置更新:
public class LocationService extends Service {
private static final long INTERVAL = 60 * 1000; // 一分钟
private LocationManager locationManager;
public IBinder onBind(Intent intent) {
return null;
}
public int onStartCommand(Intent intent, int flags, int startId) {
// 获取位置管理器
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
// 请求位置更新
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, INTERVAL, 0, locationListener);
return super.onStartCommand(intent, flags, startId);
}
public void onDestroy() {
super.onDestroy();
// 停止位置更新
locationManager.removeUpdates(locationListener);
}
private final LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
// 处理位置更新
Log.d("LocationService", "latitude: " + location.getLatitude() + ", longitude: " + location.getLongitude());
}
};
}
2、收集频率尽可能保证全局只收集1次(最多不超过3次),收集频次不要超过1次/秒。注意:
-
非必要情况下,禁止每使用一次用户信息,就调用API获取一次用户个人信息。
-
建议通过缓存技术将收集的用户信息储存在缓存中,当需要使用用户信息时从缓存中调用。
✅ 将Android ID保存到缓存中,从缓存中读取:
private String getAndroidId() {
String androidId = null;
SharedPreferences preferences = getSharedPreferences("my_app", Context.MODE_PRIVATE);
if (preferences.contains("android_id")) {
// 如果缓存中已经有android_id,则从缓存中读取
androidId = preferences.getString("android_id", null);
} else {
// 如果缓存中没有android_id,则从系统设置中获取
androidId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
// 将android_id保存到缓存中
preferences.edit().putString("android_id", androidId).apply();
}
return androidId;
}
3、默认不调用全量应用列表或不通过shell命令获取全量应用列表。
调用应用列表相关方法包括但不限于: getInstalledApplications
getInstalledApplicationsAsUser getInstalledPackages getInstalledPackagesAsUser queryIntentActivitiesAsUser(此方法可以输出包名,如果输出了手机已安装应用的全部包名就属于全量) 4、注册流程中不应强制申请权限,因用户拒绝授权而影响注册登录,会被认定为过度索权,即拒绝非必要信息影响正常注册登录。注册必要信息建议参考网信《常见类型移动互联网应用程序必要个人信息范围规定》 http://www.cac.gov.cn/2021-03/22/c_1617990997054277.htm 5、按Home键退出APP后(后台静默状态下),APP或SDK不能有收集行为。
三、APP强制、频繁、过度索取权限 1、调用时机:需遵循场景化授权,即在服务所必要的场景中,用户主动触发功能后申请,在用户主动触发前不应有相关调用行为。 2、权限声明:不需要的权限不应在Android的manifest.xml文件、iOS的info.plist声明,且Android系统中targetSDKVersion应不低于23。 3、敏感权限(通讯录、定位、相册(存储)、相机、麦克风等):Android端申请时用顶栏浮窗等形式同步告知目的,iOS端可直接编辑系统弹窗。 ✅ Android端顶栏浮窗代码实现示例
private static final int REQUEST_LOCATION_PERMISSION = 1;
private void requestLocationPermissionWithAlert() {
// 弹出悬浮窗提示用户申请权限原因
showPermissionAlert();
// 向用户索取定位权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_LOCATION_PERMISSION);
}
private void showPermissionAlert() {
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.TOP;
params.y = 0;
View permissionAlertView = LayoutInflater.from(this).inflate(R.layout.permission_alert, null);
TextView tvAlertMessage = permissionAlertView.findViewById(R.id.tv_alert_message);
tvAlertMessage.setText("为您提供导航功能,需要您授权开启地理位置权限");
windowManager.addView(permissionAlertView, params);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_LOCATION_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户同意授权
// TODO: 定位相关操作
} else {
// 用户拒绝授权
// TODO: 处理用户拒绝授权的情况
}
}
}
4、APP运行时场景化向用户申请授权,用户拒绝授权后,APP不应退出、关闭、循环弹窗申请权限使用户无法继续使用或者影响正常注册或登录。
❌ 用户拒绝授权APP退出或关闭 private static final int REQUEST_LOCATION_PERMISSION = 1;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 检查是否已经获得了定位权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 如果未获得权限,则请求定位权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_LOCATION_PERMISSION);
} else {
// 如果已经获得权限,则可以执行定位操作
// TODO: do something with the location information
}
}
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_LOCATION_PERMISSION) {
// 如果用户授权,则继续执行定位操作
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// TODO: do something with the location information
} else {
// 如果用户拒绝授权,则退出应用程序
finish();
}
}
}
❌ 用户拒绝授权重复向用户申请权限,使用户陷入弹窗循环 private static final int REQUEST_LOCATION_PERMISSION = 1;
private boolean locationPermissionGranted = false;
private void requestLocationPermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
// 如果用户之前拒绝了权限申请,则弹出提示信息,向用户解释为什么需要此权限
new AlertDialog.Builder(this)
.setTitle("需要访问定位权限")
.setMessage("为您提供导航功能,需要您授权地理位置权限")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 向用户索取定位权限
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_LOCATION_PERMISSION);
}
})
.setNegativeButton("取消", null)
.show();
} else {
// 向用户索取定位权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_LOCATION_PERMISSION);
}
}
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_LOCATION_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户同意授权
locationPermissionGranted = true;
// TODO: 定位相关操作
} else {
// 用户拒绝授权
locationPermissionGranted = false;
// 重复向用户索取定位权限
requestLocationPermission();
}
}
}
5、电话权限(READ_PHONE_STATE):属于Android系统权限,APP可通过此权限获取设备 IMSI(国际移动用户识别码)、IMEI(国际移动设备识别码)等设备唯一标识信息,建议不做申请。不可变更的唯一设备标识(IMEI、MAC地址、MEID、IMSI、SN、ICCID),建议采用可变标识(AndroidID、OAID等)代替。 ❌ 收集不可变更的唯一设备标识: // 禁止收集不可变更的唯一设备标识,如 IMEI
private String getIMEI(Context context) {
TelephonyManager tm = (TelephonyManager) context.getSystemService(Service.TELEPHONY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return tm.getImei();
} else {
return tm.getDeviceId();
}
}
6、存储权限(android.permission-group.STORAGE/photos):安卓申请存储权限可用mediastore或SAF框架实现,iOS可用进程外选取器或共享列表替代申请photos权限,不能频繁提示用户更改授权方式。
7、特殊敏感权限(设备管理器、辅助功能、监听通知栏、悬浮窗):需APP内弹窗,用户单独同意授权后才能使用。 THANKS FOR WATCHING
About us 陌陌安全 致力于以务实的工作保障陌陌旗下所有产品及亿万用户的信息安全 以开放的心态拥抱信息安全机构、团队与个人之间的共赢协作 以自由的氛围和丰富的资源支撑优秀同学的个人发展与职业成长 / 往 期 分 享 / 「陌陌安全」 扫上方二维码码关注我们,惊喜不断哦 M O M O S E C U R I T Y
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论