2026-01-15 15:34:23 +08:00
|
|
|
|
# 安全最佳实践
|
|
|
|
|
|
|
2026-01-15 16:42:52 +08:00
|
|
|
|
Android应用安全是开发过程中必须重视的方面。本文档提供Android应用安全开发的最佳实践指南。
|
2026-01-15 15:34:23 +08:00
|
|
|
|
|
2026-01-15 16:42:52 +08:00
|
|
|
|
## 目录
|
|
|
|
|
|
|
|
|
|
|
|
- [安全开发原则](#安全开发原则)
|
|
|
|
|
|
- [数据安全](#数据安全)
|
|
|
|
|
|
- [网络安全](#网络安全)
|
|
|
|
|
|
- [代码安全](#代码安全)
|
|
|
|
|
|
- [安全测试](#安全测试)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 安全开发原则
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 最小权限原则
|
|
|
|
|
|
|
|
|
|
|
|
只申请和使用应用真正需要的权限,避免过度申请权限。
|
|
|
|
|
|
|
|
|
|
|
|
#### 权限申请原则
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// ❌ 错误:申请不必要的权限
|
|
|
|
|
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
|
|
|
|
|
<uses-permission android:name="android.permission.READ_SMS" />
|
|
|
|
|
|
// 如果应用不需要这些功能,不应该申请
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 正确:只申请必要的权限
|
|
|
|
|
|
<uses-permission android:name="android.permission.CAMERA" />
|
|
|
|
|
|
// 只在需要相机功能时申请
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 运行时权限检查
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 使用权限前检查
|
|
|
|
|
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
|
|
|
|
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
|
|
|
|
// 解释为什么需要权限
|
|
|
|
|
|
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
|
|
|
|
|
|
Manifest.permission.CAMERA)) {
|
|
|
|
|
|
// 显示解释对话框
|
|
|
|
|
|
showPermissionRationale();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 直接申请权限
|
|
|
|
|
|
ActivityCompat.requestPermissions(this,
|
|
|
|
|
|
new String[]{Manifest.permission.CAMERA},
|
|
|
|
|
|
REQUEST_CODE_CAMERA);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 权限已授予,使用功能
|
|
|
|
|
|
useCamera();
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 纵深防御
|
|
|
|
|
|
|
|
|
|
|
|
采用多层安全防护机制,不依赖单一安全措施。
|
|
|
|
|
|
|
|
|
|
|
|
#### 多层防护示例
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 第一层:权限检查
|
|
|
|
|
|
if (checkPermission()) {
|
|
|
|
|
|
// 第二层:输入验证
|
|
|
|
|
|
if (validateInput(data)) {
|
|
|
|
|
|
// 第三层:数据加密
|
|
|
|
|
|
String encrypted = encrypt(data);
|
|
|
|
|
|
// 第四层:安全存储
|
|
|
|
|
|
saveSecurely(encrypted);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 安全设计
|
|
|
|
|
|
|
|
|
|
|
|
从设计阶段就考虑安全问题,而不是事后补救。
|
|
|
|
|
|
|
|
|
|
|
|
#### 安全设计要点
|
|
|
|
|
|
|
|
|
|
|
|
- **威胁建模**:识别潜在的安全威胁
|
|
|
|
|
|
- **安全架构**:设计安全的应用架构
|
|
|
|
|
|
- **安全编码**:遵循安全编码规范
|
|
|
|
|
|
- **安全测试**:进行安全测试和审计
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 数据最小化
|
|
|
|
|
|
|
|
|
|
|
|
只收集和存储必要的数据,及时删除不需要的数据。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// ❌ 错误:收集过多数据
|
|
|
|
|
|
class User {
|
|
|
|
|
|
String name;
|
|
|
|
|
|
String email;
|
|
|
|
|
|
String phone;
|
|
|
|
|
|
String address;
|
|
|
|
|
|
String idCard; // 不需要身份证号
|
|
|
|
|
|
String bankCard; // 不需要银行卡号
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 正确:只收集必要数据
|
|
|
|
|
|
class User {
|
|
|
|
|
|
String name;
|
|
|
|
|
|
String email;
|
|
|
|
|
|
// 只在需要时收集其他信息
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 默认安全
|
|
|
|
|
|
|
|
|
|
|
|
默认采用安全配置,而不是默认开放。
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- ❌ 错误:默认导出组件 -->
|
|
|
|
|
|
<activity
|
|
|
|
|
|
android:name=".SensitiveActivity"
|
|
|
|
|
|
android:exported="true" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ✅ 正确:默认不导出,需要时显式设置 -->
|
|
|
|
|
|
<activity
|
|
|
|
|
|
android:name=".SensitiveActivity"
|
|
|
|
|
|
android:exported="false" />
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 数据安全
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 敏感数据加密
|
|
|
|
|
|
|
|
|
|
|
|
对敏感数据进行加密存储,包括密码、密钥、个人信息等。
|
|
|
|
|
|
|
|
|
|
|
|
#### 使用Android密钥库
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 使用Android密钥库存储密钥
|
|
|
|
|
|
public class KeyStoreHelper {
|
|
|
|
|
|
private static final String KEYSTORE_ALIAS = "my_key";
|
|
|
|
|
|
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
|
|
|
|
|
|
|
|
|
|
|
|
public static void generateKey() throws Exception {
|
|
|
|
|
|
KeyGenerator keyGenerator = KeyGenerator.getInstance(
|
|
|
|
|
|
KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
|
|
|
|
|
|
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
|
|
|
|
|
|
KEYSTORE_ALIAS,
|
|
|
|
|
|
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
|
|
|
|
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
|
|
|
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
keyGenerator.init(spec);
|
|
|
|
|
|
keyGenerator.generateKey();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static byte[] encrypt(String data) throws Exception {
|
|
|
|
|
|
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
|
|
|
|
|
|
keyStore.load(null);
|
|
|
|
|
|
KeyStore.SecretKeyEntry secretKeyEntry =
|
|
|
|
|
|
(KeyStore.SecretKeyEntry) keyStore.getEntry(KEYSTORE_ALIAS, null);
|
|
|
|
|
|
SecretKey secretKey = secretKeyEntry.getSecretKey();
|
|
|
|
|
|
|
|
|
|
|
|
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
|
|
|
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
|
|
|
|
|
|
|
|
|
|
|
byte[] iv = cipher.getIV();
|
|
|
|
|
|
byte[] encrypted = cipher.doFinal(data.getBytes());
|
|
|
|
|
|
|
|
|
|
|
|
// 将IV和加密数据组合
|
|
|
|
|
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
|
|
|
|
outputStream.write(iv);
|
|
|
|
|
|
outputStream.write(encrypted);
|
|
|
|
|
|
return outputStream.toByteArray();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 数据加密存储
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 加密敏感数据
|
|
|
|
|
|
public class SecureStorage {
|
|
|
|
|
|
public static void saveEncryptedData(String key, String value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
byte[] encrypted = KeyStoreHelper.encrypt(value);
|
|
|
|
|
|
// 存储加密后的数据
|
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("secure_prefs", MODE_PRIVATE);
|
|
|
|
|
|
String base64 = Base64.encodeToString(encrypted, Base64.DEFAULT);
|
|
|
|
|
|
prefs.edit().putString(key, base64).apply();
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static String getDecryptedData(String key) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("secure_prefs", MODE_PRIVATE);
|
|
|
|
|
|
String base64 = prefs.getString(key, null);
|
|
|
|
|
|
if (base64 == null) return null;
|
|
|
|
|
|
|
|
|
|
|
|
byte[] encrypted = Base64.decode(base64, Base64.DEFAULT);
|
|
|
|
|
|
// 解密数据
|
|
|
|
|
|
return KeyStoreHelper.decrypt(encrypted);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 安全存储
|
|
|
|
|
|
|
|
|
|
|
|
使用安全的存储方式,避免明文存储敏感信息。
|
|
|
|
|
|
|
|
|
|
|
|
#### SharedPreferences安全使用
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// ❌ 错误:明文存储敏感信息
|
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
|
|
|
|
|
|
prefs.edit().putString("password", password).apply();
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 正确:加密后存储
|
|
|
|
|
|
String encryptedPassword = encrypt(password);
|
|
|
|
|
|
prefs.edit().putString("password", encryptedPassword).apply();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 使用EncryptedSharedPreferences(Android 6.0+)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 使用EncryptedSharedPreferences
|
|
|
|
|
|
MasterKey masterKey = new MasterKey.Builder(context)
|
|
|
|
|
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
|
|
SharedPreferences encryptedPrefs = EncryptedSharedPreferences.create(
|
|
|
|
|
|
context,
|
|
|
|
|
|
"encrypted_prefs",
|
|
|
|
|
|
masterKey,
|
|
|
|
|
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
|
|
|
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
encryptedPrefs.edit()
|
|
|
|
|
|
.putString("sensitive_data", data)
|
|
|
|
|
|
.apply();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 数据清理
|
|
|
|
|
|
|
|
|
|
|
|
及时清理不再需要的敏感数据。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 清理敏感数据
|
|
|
|
|
|
public void clearSensitiveData() {
|
|
|
|
|
|
// 清理内存中的数据
|
|
|
|
|
|
password = null;
|
|
|
|
|
|
apiKey = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 清理SharedPreferences
|
|
|
|
|
|
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
|
|
|
|
|
|
prefs.edit().clear().apply();
|
|
|
|
|
|
|
|
|
|
|
|
// 清理文件
|
|
|
|
|
|
File sensitiveFile = new File(getFilesDir(), "sensitive.txt");
|
|
|
|
|
|
if (sensitiveFile.exists()) {
|
|
|
|
|
|
// 安全删除:覆盖后删除
|
|
|
|
|
|
secureDelete(sensitiveFile);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安全删除文件
|
|
|
|
|
|
private void secureDelete(File file) {
|
|
|
|
|
|
if (file.exists()) {
|
|
|
|
|
|
// 覆盖文件内容
|
|
|
|
|
|
RandomAccessFile raf = new RandomAccessFile(file, "rws");
|
|
|
|
|
|
raf.setLength(0);
|
|
|
|
|
|
raf.close();
|
|
|
|
|
|
// 删除文件
|
|
|
|
|
|
file.delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 日志安全
|
|
|
|
|
|
|
|
|
|
|
|
避免在日志中输出敏感信息。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// ❌ 错误:日志中输出敏感信息
|
|
|
|
|
|
Log.d("TAG", "User password: " + password);
|
|
|
|
|
|
Log.d("TAG", "API key: " + apiKey);
|
|
|
|
|
|
Log.d("TAG", "User token: " + token);
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 正确:敏感信息脱敏或移除
|
|
|
|
|
|
Log.d("TAG", "User login attempt");
|
|
|
|
|
|
// 或使用条件编译
|
|
|
|
|
|
if (BuildConfig.DEBUG) {
|
|
|
|
|
|
Log.d("TAG", "Debug info: " + debugInfo);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 内存安全
|
|
|
|
|
|
|
|
|
|
|
|
避免在内存中长时间保存敏感数据。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 使用后立即清理
|
|
|
|
|
|
public void processSensitiveData(String data) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 处理数据
|
|
|
|
|
|
process(data);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
// 清理内存
|
|
|
|
|
|
data = null;
|
|
|
|
|
|
System.gc(); // 建议垃圾回收
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用char[]而不是String存储密码(可被清理)
|
|
|
|
|
|
public void handlePassword(char[] password) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 使用密码
|
|
|
|
|
|
authenticate(password);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
// 清理密码
|
|
|
|
|
|
Arrays.fill(password, '\0');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 网络安全
|
|
|
|
|
|
|
|
|
|
|
|
### 1. HTTPS通信
|
|
|
|
|
|
|
|
|
|
|
|
使用HTTPS进行网络通信,避免HTTP明文传输。
|
|
|
|
|
|
|
|
|
|
|
|
#### 配置网络安全
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- res/xml/network_security_config.xml -->
|
|
|
|
|
|
<network-security-config>
|
|
|
|
|
|
<!-- 默认配置:只允许HTTPS -->
|
|
|
|
|
|
<base-config cleartextTrafficPermitted="false">
|
|
|
|
|
|
<trust-anchors>
|
|
|
|
|
|
<certificates src="system" />
|
|
|
|
|
|
</trust-anchors>
|
|
|
|
|
|
</base-config>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 特定域名配置 -->
|
|
|
|
|
|
<domain-config cleartextTrafficPermitted="false">
|
|
|
|
|
|
<domain includeSubdomains="true">example.com</domain>
|
|
|
|
|
|
<trust-anchors>
|
|
|
|
|
|
<certificates src="system" />
|
|
|
|
|
|
<!-- 自定义证书 -->
|
|
|
|
|
|
<certificates src="@raw/my_ca" />
|
|
|
|
|
|
</trust-anchors>
|
|
|
|
|
|
</domain-config>
|
|
|
|
|
|
</network-security-config>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- AndroidManifest.xml -->
|
|
|
|
|
|
<application
|
|
|
|
|
|
android:networkSecurityConfig="@xml/network_security_config"
|
|
|
|
|
|
... />
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 证书固定(Certificate Pinning)
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- 证书固定配置 -->
|
|
|
|
|
|
<domain-config>
|
|
|
|
|
|
<domain includeSubdomains="true">api.example.com</domain>
|
|
|
|
|
|
<pin-set expiration="2025-12-31">
|
|
|
|
|
|
<!-- SHA-256指纹 -->
|
|
|
|
|
|
<pin digest="SHA-256">base64==</pin>
|
|
|
|
|
|
<!-- 备用证书 -->
|
|
|
|
|
|
<pin digest="SHA-256">base64==</pin>
|
|
|
|
|
|
</pin-set>
|
|
|
|
|
|
</domain-config>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 输入验证
|
|
|
|
|
|
|
|
|
|
|
|
验证所有网络输入,防止注入攻击。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 输入验证
|
|
|
|
|
|
public class InputValidator {
|
|
|
|
|
|
// 验证URL
|
|
|
|
|
|
public static boolean isValidUrl(String url) {
|
|
|
|
|
|
if (url == null || url.isEmpty()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
URL urlObj = new URL(url);
|
|
|
|
|
|
// 只允许HTTPS
|
|
|
|
|
|
return "https".equals(urlObj.getProtocol());
|
|
|
|
|
|
} catch (MalformedURLException e) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证输入长度
|
|
|
|
|
|
public static boolean isValidLength(String input, int maxLength) {
|
|
|
|
|
|
return input != null && input.length() <= maxLength;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证输入格式
|
|
|
|
|
|
public static boolean isValidEmail(String email) {
|
|
|
|
|
|
if (email == null || email.isEmpty()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
String pattern = "^[A-Za-z0-9+_.-]+@(.+)$";
|
|
|
|
|
|
return email.matches(pattern);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 防止SQL注入
|
|
|
|
|
|
public static String sanitizeSql(String input) {
|
|
|
|
|
|
if (input == null) return "";
|
|
|
|
|
|
// 转义特殊字符
|
|
|
|
|
|
return input.replace("'", "''")
|
|
|
|
|
|
.replace(";", "")
|
|
|
|
|
|
.replace("--", "");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 请求签名
|
|
|
|
|
|
|
|
|
|
|
|
对重要请求进行签名,防止篡改。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 请求签名
|
|
|
|
|
|
public class RequestSigner {
|
|
|
|
|
|
public static String signRequest(String data, String secret) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Mac mac = Mac.getInstance("HmacSHA256");
|
|
|
|
|
|
SecretKeySpec secretKey = new SecretKeySpec(
|
|
|
|
|
|
secret.getBytes(), "HmacSHA256");
|
|
|
|
|
|
mac.init(secretKey);
|
|
|
|
|
|
byte[] hash = mac.doFinal(data.getBytes());
|
|
|
|
|
|
return Base64.encodeToString(hash, Base64.NO_WRAP);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static boolean verifyRequest(String data, String signature, String secret) {
|
|
|
|
|
|
String calculatedSignature = signRequest(data, secret);
|
|
|
|
|
|
return signature != null && signature.equals(calculatedSignature);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 超时和重试
|
|
|
|
|
|
|
|
|
|
|
|
设置合理的超时和重试机制。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 配置OkHttp超时
|
|
|
|
|
|
OkHttpClient client = new OkHttpClient.Builder()
|
|
|
|
|
|
.connectTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
|
.writeTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 敏感信息传输
|
|
|
|
|
|
|
|
|
|
|
|
避免在URL或请求头中传输敏感信息。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// ❌ 错误:敏感信息在URL中
|
|
|
|
|
|
String url = "https://api.example.com/login?username=user&password=pass";
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 正确:使用POST请求体
|
|
|
|
|
|
RequestBody body = new FormBody.Builder()
|
|
|
|
|
|
.add("username", username)
|
|
|
|
|
|
.add("password", password)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
Request request = new Request.Builder()
|
|
|
|
|
|
.url("https://api.example.com/login")
|
|
|
|
|
|
.post(body)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 代码安全
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 组件安全
|
|
|
|
|
|
|
|
|
|
|
|
正确配置组件权限和导出状态。
|
|
|
|
|
|
|
|
|
|
|
|
#### Activity安全配置
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- AndroidManifest.xml -->
|
|
|
|
|
|
<!-- ❌ 错误:导出且无权限保护 -->
|
|
|
|
|
|
<activity
|
|
|
|
|
|
android:name=".SensitiveActivity"
|
|
|
|
|
|
android:exported="true" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ✅ 正确:不导出或设置权限 -->
|
|
|
|
|
|
<activity
|
|
|
|
|
|
android:name=".SensitiveActivity"
|
|
|
|
|
|
android:exported="false"
|
|
|
|
|
|
android:permission="com.example.PERMISSION" />
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### Service安全配置
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- Service安全配置 -->
|
|
|
|
|
|
<service
|
|
|
|
|
|
android:name=".MyService"
|
|
|
|
|
|
android:exported="false"
|
|
|
|
|
|
android:permission="com.example.SERVICE_PERMISSION" />
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### BroadcastReceiver安全配置
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- 动态注册更安全 -->
|
|
|
|
|
|
<!-- 静态注册需要设置权限 -->
|
|
|
|
|
|
<receiver
|
|
|
|
|
|
android:name=".MyReceiver"
|
|
|
|
|
|
android:exported="false"
|
|
|
|
|
|
android:permission="com.example.BROADCAST_PERMISSION">
|
|
|
|
|
|
<intent-filter>
|
|
|
|
|
|
<action android:name="com.example.ACTION" />
|
|
|
|
|
|
</intent-filter>
|
|
|
|
|
|
</receiver>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### ContentProvider安全配置
|
|
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
|
<!-- ContentProvider安全配置 -->
|
|
|
|
|
|
<provider
|
|
|
|
|
|
android:name=".MyProvider"
|
|
|
|
|
|
android:authorities="com.example.provider"
|
|
|
|
|
|
android:exported="false"
|
|
|
|
|
|
android:readPermission="com.example.READ_PERMISSION"
|
|
|
|
|
|
android:writePermission="com.example.WRITE_PERMISSION" />
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Intent安全
|
|
|
|
|
|
|
|
|
|
|
|
验证Intent的来源和内容。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// Intent安全处理
|
|
|
|
|
|
public class IntentValidator {
|
|
|
|
|
|
// 验证Intent来源
|
|
|
|
|
|
public static boolean isValidIntent(Intent intent) {
|
|
|
|
|
|
// 检查组件是否存在
|
|
|
|
|
|
ComponentName component = intent.getComponent();
|
|
|
|
|
|
if (component == null) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证包名
|
|
|
|
|
|
String packageName = component.getPackageName();
|
|
|
|
|
|
if (!packageName.equals(getPackageName())) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证Intent数据
|
|
|
|
|
|
public static boolean isValidIntentData(Intent intent) {
|
|
|
|
|
|
Uri data = intent.getData();
|
|
|
|
|
|
if (data == null) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证scheme
|
|
|
|
|
|
String scheme = data.getScheme();
|
|
|
|
|
|
if (!"https".equals(scheme) && !"content".equals(scheme)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安全启动Activity
|
|
|
|
|
|
public static void startActivitySafely(Context context, Intent intent) {
|
|
|
|
|
|
if (isValidIntent(intent) && isValidIntentData(intent)) {
|
|
|
|
|
|
context.startActivity(intent);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 记录安全事件
|
|
|
|
|
|
Log.w("Security", "Invalid intent blocked");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 代码混淆
|
|
|
|
|
|
|
|
|
|
|
|
使用ProGuard或R8进行代码混淆。
|
|
|
|
|
|
|
|
|
|
|
|
#### ProGuard配置
|
|
|
|
|
|
|
|
|
|
|
|
```proguard
|
|
|
|
|
|
# proguard-rules.pro
|
|
|
|
|
|
|
|
|
|
|
|
# 保留必要的类
|
|
|
|
|
|
-keep class com.example.model.** { *; }
|
|
|
|
|
|
|
|
|
|
|
|
# 移除日志
|
|
|
|
|
|
-assumenosideeffects class android.util.Log {
|
|
|
|
|
|
public static *** d(...);
|
|
|
|
|
|
public static *** v(...);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 混淆配置
|
|
|
|
|
|
-dontpreverify
|
|
|
|
|
|
-repackageclasses ''
|
|
|
|
|
|
-allowaccessmodification
|
|
|
|
|
|
-optimizations !code/simplification/arithmetic
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### build.gradle配置
|
|
|
|
|
|
|
|
|
|
|
|
```gradle
|
|
|
|
|
|
android {
|
|
|
|
|
|
buildTypes {
|
|
|
|
|
|
release {
|
|
|
|
|
|
minifyEnabled true
|
|
|
|
|
|
shrinkResources true
|
|
|
|
|
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
|
|
|
|
|
'proguard-rules.pro'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 反调试保护
|
|
|
|
|
|
|
|
|
|
|
|
防止应用被调试和分析。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 检测调试器
|
|
|
|
|
|
public class AntiDebug {
|
|
|
|
|
|
public static boolean isDebugging() {
|
|
|
|
|
|
// 检查调试标志
|
|
|
|
|
|
if (Debug.isDebuggerConnected()) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查tracerpid
|
|
|
|
|
|
try {
|
|
|
|
|
|
BufferedReader reader = new BufferedReader(
|
|
|
|
|
|
new FileReader("/proc/self/status"));
|
|
|
|
|
|
String line;
|
|
|
|
|
|
while ((line = reader.readLine()) != null) {
|
|
|
|
|
|
if (line.startsWith("TracerPid:")) {
|
|
|
|
|
|
int tracerPid = Integer.parseInt(line.split("\\s+")[1]);
|
|
|
|
|
|
if (tracerPid != 0) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
reader.close();
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检测模拟器
|
|
|
|
|
|
public static boolean isEmulator() {
|
|
|
|
|
|
return Build.FINGERPRINT.startsWith("generic")
|
|
|
|
|
|
|| Build.FINGERPRINT.startsWith("unknown")
|
|
|
|
|
|
|| Build.MODEL.contains("google_sdk")
|
|
|
|
|
|
|| Build.MODEL.contains("Emulator")
|
|
|
|
|
|
|| Build.MODEL.contains("Android SDK built for x86")
|
|
|
|
|
|
|| Build.MANUFACTURER.contains("Genymotion")
|
|
|
|
|
|
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|
|
|
|
|
|
|| "google_sdk".equals(Build.PRODUCT);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 输入验证
|
|
|
|
|
|
|
|
|
|
|
|
验证所有用户输入,防止注入攻击。
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 输入验证工具
|
|
|
|
|
|
public class InputValidator {
|
|
|
|
|
|
// 验证文件名
|
|
|
|
|
|
public static boolean isValidFileName(String fileName) {
|
|
|
|
|
|
if (fileName == null || fileName.isEmpty()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 防止路径遍历
|
|
|
|
|
|
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证JSON
|
|
|
|
|
|
public static boolean isValidJson(String json) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
new JSONObject(json);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (JSONException e) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证XML
|
|
|
|
|
|
public static boolean isValidXml(String xml) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
|
|
|
|
|
factory.setNamespaceAware(true);
|
|
|
|
|
|
XmlPullParser parser = factory.newPullParser();
|
|
|
|
|
|
parser.setInput(new StringReader(xml));
|
|
|
|
|
|
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
|
|
|
|
|
// 解析XML
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 安全测试
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 静态代码分析
|
|
|
|
|
|
|
|
|
|
|
|
使用工具进行静态代码分析,发现潜在安全问题。
|
|
|
|
|
|
|
|
|
|
|
|
#### 使用工具
|
|
|
|
|
|
|
|
|
|
|
|
- **Android Lint**:Android官方代码检查工具
|
|
|
|
|
|
- **FindBugs/SpotBugs**:Java代码缺陷检测
|
|
|
|
|
|
- **SonarQube**:代码质量分析平台
|
|
|
|
|
|
- **Checkmarx**:安全漏洞扫描
|
|
|
|
|
|
|
|
|
|
|
|
#### Lint配置
|
|
|
|
|
|
|
|
|
|
|
|
```gradle
|
|
|
|
|
|
android {
|
|
|
|
|
|
lintOptions {
|
|
|
|
|
|
// 将警告视为错误
|
|
|
|
|
|
warningsAsErrors true
|
|
|
|
|
|
// 检查所有问题
|
|
|
|
|
|
checkAllWarnings true
|
|
|
|
|
|
// 忽略某些警告
|
|
|
|
|
|
disable 'UnusedResources'
|
|
|
|
|
|
// 输出报告
|
|
|
|
|
|
htmlReport true
|
|
|
|
|
|
htmlOutput file("$project.buildDir/reports/lint/lint.html")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 动态安全测试
|
|
|
|
|
|
|
|
|
|
|
|
在运行时测试应用的安全性。
|
|
|
|
|
|
|
|
|
|
|
|
#### 权限测试
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 测试权限检查
|
|
|
|
|
|
@Test
|
|
|
|
|
|
public void testPermissionCheck() {
|
|
|
|
|
|
// 测试权限未授予时的行为
|
|
|
|
|
|
when(ContextCompat.checkSelfPermission(any(), anyString()))
|
|
|
|
|
|
.thenReturn(PackageManager.PERMISSION_DENIED);
|
|
|
|
|
|
|
|
|
|
|
|
// 验证应用正确处理权限拒绝
|
|
|
|
|
|
assertFalse(hasPermission());
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 网络安全测试
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 测试HTTPS连接
|
|
|
|
|
|
@Test
|
|
|
|
|
|
public void testHttpsConnection() {
|
|
|
|
|
|
// 验证只使用HTTPS
|
|
|
|
|
|
String url = getApiUrl();
|
|
|
|
|
|
assertTrue(url.startsWith("https://"));
|
|
|
|
|
|
|
|
|
|
|
|
// 测试证书验证
|
|
|
|
|
|
// ...
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 渗透测试
|
|
|
|
|
|
|
|
|
|
|
|
模拟攻击者进行渗透测试。
|
|
|
|
|
|
|
|
|
|
|
|
#### 测试项目
|
|
|
|
|
|
|
|
|
|
|
|
- **组件导出测试**:检查是否有不必要的组件导出
|
|
|
|
|
|
- **权限测试**:测试权限是否正确配置
|
|
|
|
|
|
- **数据泄露测试**:检查是否有敏感信息泄露
|
|
|
|
|
|
- **注入攻击测试**:测试SQL注入、XSS等
|
|
|
|
|
|
- **加密测试**:验证数据是否正确加密
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 安全审计
|
|
|
|
|
|
|
|
|
|
|
|
定期进行安全审计。
|
|
|
|
|
|
|
|
|
|
|
|
#### 审计清单
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 权限使用是否合理
|
|
|
|
|
|
- [ ] 敏感数据是否加密
|
|
|
|
|
|
- [ ] 网络通信是否使用HTTPS
|
|
|
|
|
|
- [ ] 组件是否正确配置
|
|
|
|
|
|
- [ ] 输入是否验证
|
|
|
|
|
|
- [ ] 日志是否包含敏感信息
|
|
|
|
|
|
- [ ] 代码是否混淆
|
|
|
|
|
|
- [ ] 依赖库是否安全
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 漏洞扫描
|
|
|
|
|
|
|
|
|
|
|
|
使用自动化工具扫描已知漏洞。
|
|
|
|
|
|
|
|
|
|
|
|
#### 工具推荐
|
|
|
|
|
|
|
|
|
|
|
|
- **OWASP Mobile Top 10**:移动应用安全风险清单
|
|
|
|
|
|
- **MobSF (Mobile Security Framework)**:移动应用安全测试框架
|
|
|
|
|
|
- **QARK (Quick Android Review Kit)**:Android应用安全分析工具
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
|
|
Android应用安全是一个持续的过程,需要:
|
|
|
|
|
|
|
|
|
|
|
|
1. **遵循安全原则**:最小权限、纵深防御、安全设计
|
|
|
|
|
|
2. **保护数据安全**:加密存储、安全传输、及时清理
|
|
|
|
|
|
3. **确保网络安全**:HTTPS通信、输入验证、请求签名
|
|
|
|
|
|
4. **加强代码安全**:组件安全、代码混淆、输入验证
|
|
|
|
|
|
5. **进行安全测试**:静态分析、动态测试、渗透测试
|
|
|
|
|
|
|
|
|
|
|
|
建议:
|
|
|
|
|
|
- 定期进行安全审计
|
|
|
|
|
|
- 关注安全更新和漏洞公告
|
|
|
|
|
|
- 使用官方推荐的安全实践
|
|
|
|
|
|
- 参与安全社区,学习最新安全知识
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
*最后更新:2024年*
|