应用设备唯一识别码的解决方案
唯一标识必须满足两个特性才能完美解决定位唯一设备的问题,但这个问题的解决却注定只能极限接近完美
唯一性:标识必须在所有使用该应用的设备上保持唯一性
不变性:标识必须在同一设备上保持不变
方向一:使用硬件标识
硬件标识实际上在硬件生产之时就被要求满足这两个特性(依然有人工生产的不确定性),但标识的获取趋于困难性,使得使用硬件标识作为唯一识别码的方案所能使用的范围越来越狭窄,不能作为全局方案使用。
wifiManager = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE));
String macAddress = wifiManager.getConnectionInfo().getMacAddress();
4. 使用 SERIAL NUMBER
通过 android.os.Build.SERIAL来获取
劣势:
硬件标识 局限
DEVICE_ID - 适用 Android9以下设备,但需要申请Manifest.permission.READ_PHONE_STATE权限
UUID的实现原理简析:
Wiki解释:通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息数目的一个128位标识符,还有相关的术语:全局唯一标识符(GUID)。根据标准方法生成,不依赖中央机构的注册和分配,UUID具有唯一性,这与其他大多数编号方案不同。重复UUID码概率接近零,可以忽略不计
组成: 8-4-4-4-12 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx M表示 UUID 版本,数字 N的一至三个最高有效位表示 UUID 变体
UUID根据版本不同,依赖的组成有不同的变种,
基于时间的UUID版本是通过计算当前时间戳、随机数和机器MAC地址得到 。UUID的核心算法保证了即使在多处理器同时生成的UUID重复性为0,因为他们所在的时间、空间(节点:通常是MAC地址)必然不一致。
由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址--Java的UUID往往是这样实现的(当然也考虑了获取MAC的难度)。
String uniqueID = UUID.randomUUID().toString();
趋于完美的方案
尽可能的获取硬件标识来满足两个特性,在有限制或其他因素的条件下,尽可能满足不变性,将UUID存储在外部环境来进行读写。
方案思路
尽可能的获取硬件标识
硬件标识为空,进行UUID的生成、存储
方案说明:
需要在使用之前拿到设备信息权限(没有会导致DeviceID不可取,但仍然可用),外部存储读写权限(必须,否则不可用)
最好在Application中使用,唯一标识在app与服务器直接交互很常用,放在全局统一的地方方便管理使用
还有一种方案是拿到设备的某些唯一信息,生成特定的UUID,这样保持不变就可以跳过存储,但是既然拿到了唯一信息,那为啥还要生成UUID呢?
public class UniqueIDUtils {
private static final String TAG = "UniqueIDUtils";
private static String uniqueID;
private static String uniqueKey = "unique_id";
private static String uniqueIDDirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath();
private static String uniqueIDFile = "unique.txt";
public static String getUniqueID(Context context) {
//三步读取:内存中,存储的SP表中,外部存储文件中
if (!TextUtils.isEmpty(uniqueID)) {
Log.e(TAG, "getUniqueID: 内存中获取" + uniqueID);
return uniqueID;
}
uniqueID = PreferenceManager.getDefaultSharedPreferences(context).getString(uniqueKey, "");
if (!TextUtils.isEmpty(uniqueID)) {
Log.e(TAG, "getUniqueID: SP中获取" + uniqueID);
return uniqueID;
}
readUniqueFile(context);
if (!TextUtils.isEmpty(uniqueID)) {
Log.e(TAG, "getUniqueID: 外部存储中获取" + uniqueID);
return uniqueID;
}
//两步创建:硬件获取;自行生成与存储
getDeviceID(context);
getAndroidID(context);
getSNID();
createUniqueID(context);
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(uniqueKey, uniqueID);
return uniqueID;
}
@SuppressLint("MissingPermission")
private static void getDeviceID(Context context) {
if (!TextUtils.isEmpty(uniqueID)) {
return;
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
return;
}
String deviceId = null;
try {
deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId();
if (TextUtils.isEmpty(deviceId)) {
return;
}
} catch (Exception e) {
e.printStackTrace();
return;
}
uniqueID = deviceId;
Log.e(TAG, "getUniqueID: DeviceId获取成功" + uniqueID);
}
private static void getAndroidID(Context context) {
if (!TextUtils.isEmpty(uniqueID)) {
return;
}
String androidID = null;
try {
androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (TextUtils.isEmpty(androidID) || "9774d56d682e549c".equals(androidID)) {
return;
}
} catch (Exception e) {
e.printStackTrace();
return;
}
uniqueID = androidID;
Log.e(TAG, "getUniqueID: AndroidID获取成功" + uniqueID);
}
private static void getSNID() {
if (!TextUtils.isEmpty(uniqueID)) {
return;
}
String snID = Build.SERIAL;
if (TextUtils.isEmpty(snID)) {
return;
}
uniqueID = snID;
Log.e(TAG, "getUniqueID: SNID获取成功" + uniqueID);
}
private static void createUniqueID(Context context) {
if (!TextUtils.isEmpty(uniqueID)) {
return;
}
uniqueID = UUID.randomUUID().toString();
Log.e(TAG, "getUniqueID: UUID生成成功" + uniqueID);
File filesDir = new File(uniqueIDDirPath + File.separator + context.getApplicationContext().getPackageName());
if (!filesDir.exists()) {
filesDir.mkdir();
}
File file = new File(filesDir, uniqueIDFile);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
outputStream.write(uniqueID.getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static void readUniqueFile(Context context) {
File filesDir = new File(uniqueIDDirPath + File.separator + context.getApplicationContext().getPackageName());
File file = new File(filesDir, uniqueIDFile);
if (file.exists()) {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
inputStream.read(bytes);
uniqueID = new String(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public static void clearUniqueFile(Context context) {
File filesDir = new File(uniqueIDDirPath + File.separator + context.getApplicationContext().getPackageName());
deleteFile(filesDir);
}
private static void deleteFile(File file) {
if (file.isDirectory()) {
for (File listFile : file.listFiles()) {
deleteFile(listFile);
}
} else {
file.delete();
}
}
}
希望但又矛盾的完美方案
硬件标识既然对获取方关闭,那提供基于硬件标识生成的标识(类似UUID)暴露给获取方,但Android10上对于设备隐私的控制又明确了Google是不想app能够长久定位同一台设备的。不过如果基于硬件标识及app包名来生成的呢?
名词解释
设备码缩写(全称) 定义
IMEI(International Mobile Equipment Identity) 国际移动电话设备识别码:由15位数字组成的"电子串号",它与每台手机一一对应,而且该码是全世界唯一的
UUID(Universally Unique Identifier) 全局唯一标识符:指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的,由以下几部分的组合:当前日期和时间(UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同),时钟序列,全局唯一的IEEE机器识别号(如果有网卡,从网卡获得,没有网卡以其他方式获得)
MEID(Mobile Equipment IDentifier ) 是全球唯一的56bit CDMA制式移动终端标识号。标识号会被烧入终端里,并且不能被修改。可用来对CDMA制式移动式设备进行身份识别和跟踪
IMEI是手机的身份证,MEID是CDMA制式(电信运营的)的专用身份证;IMEI是15位,MEID是14位
DEVICE_ID Android系统为开发者提供的用于标识手机设备的串号 ; 它根据不同的手机设备返回IMEI,MEID或者ESN码 ;它返回的是设备的真实标识(因此Android10上更新的隐私保护上无法对它进行正常获取了)
ANDROID_ID 在设备首次启动时,系统会随机生成一个64位的数字,并把这个数字以16进制字符串的形式保存下来 。 当设备被wipe后该值会被重置 (wipe:手机恢复出厂设置、刷机或其他类似操作)
Serial Number SN码是Serial Number的缩写,有时也叫SerialNo,也就是产品序列号,产品序列是为了验证“产品的合法身份”而引入的一个概念,它是用来保障用户的正版权益,享受合法服务的;一套正版的产品只对应一组产品序列号。SN码别称:机器码、认证码、注册申请码等
MAC ADDRESS 媒体访问控制地址,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address),它是一个用来确认网络设备位置的地址。 在OSI模型中,第三层网络层负责IP地址,第二层数据链接层则负责MAC地址。MAC地址用于在网络中唯一标示一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的MAC地址。详细参考WIKI百科
ESN码 (Electronic Serial Number ) 美国联邦通信委员会规定的,每一台移动设备(例如移动电话、智能手机、平板电脑等)独有的参数,其长度为32位
ESN码一开始使用于AMPS和D-AMPS手机上,当前则于CDMA手机上最为常见;IMEI则最常使用在GSM制式的手机上