21、安全优化:混淆与加固、WebView安全、数据加密存储、HTTPS证书校验
安全优化这个话题,说实话,很多开发同学容易忽视。总觉得「我的App又不是银行,谁会来攻击我?」。我早年也这么想,直到有一次在项目中看到用户明文密码直接存在SharedPreferences里……嗯,从那以后,我对安全这块就格外上心了。
今天咱们聊聊Android安全优化的四个核心点:混淆与加固、WebView安全、数据加密存储、HTTPS证书校验。这四个点,说白了就是App的「四道防线」。
核心观点:安全不是功能,而是属性。它应该贯穿整个开发流程,而不是上线前才想起来补。
一、混淆与加固:让逆向工程师头疼
混淆,说白了就是把你的代码变得「面目全非」。类名变成a、b、c,方法名变成a()、b(),让反编译的人看得一头雾水。
我个人习惯在build.gradle里这样配置ProGuard(现在Android Studio默认用R8了,但配置方式类似):
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
但光混淆还不够。混淆只是「改名」,逻辑还在。真正的加固,需要配合第三方加固壳(比如360、腾讯、梆梆等)。
我的经验:混淆规则里,一定要保留反射调用的类和方法。我在项目中遇到过因为混淆导致Gson解析失败的问题——实体类被混淆了,字段名全变了,解析出来全是null。后来在proguard-rules.pro里加了:
-keep class com.example.model.** { *; }
注意:混淆不是万能的。字符串常量、资源ID、四大组件声明这些,混淆是动不了的。所以敏感字符串(比如API Key)不要硬编码在代码里,建议用NDK存到native层,或者用加密方式存储。
二、WebView安全:最容易忽视的漏洞
WebView这玩意儿,用起来方便,但坑也最多。我见过太多App直接把setJavaScriptEnabled(true)一开,然后就不管了。你想想看,如果WebView加载了一个恶意页面,它能做什么?
这里有几个必须注意的点:
- 关闭文件域访问:除非必要,否则关掉
setAllowFileAccess()和setAllowFileAccessFromFileURLs()。否则恶意JS可以读取本地文件。 - 慎用JS桥:@JavascriptInterface注解的方法,所有网页都能调用。我建议对JS桥做来源校验,只允许特定域名调用。
- HTTPS加载:WebView加载的URL尽量用HTTPS,避免中间人攻击。
举个例子,安全的WebView配置应该是这样的:
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setAllowFileAccess(false);
settings.setAllowFileAccessFromFileURLs(false);
settings.setAllowContentAccess(false);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
// JS桥接口,只允许特定来源
webView.addJavascriptInterface(new SafeBridge(), "Android");
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
// 只允许加载白名单内的域名
if (url.startsWith("https://trusted-domain.com")) {
return false;
}
return true;
}
});
避坑指南:我曾经遇到一个线上问题——WebView加载了第三方广告SDK的页面,那个页面里有个恶意JS脚本,通过JS桥调用了本地文件读取接口,把用户手机里的通讯录上传了。从那以后,我对JS桥的权限控制就格外严格。所有JS桥方法,必须做来源校验,而且只暴露最小必要接口。
三、数据加密存储:别把密码当明文存
数据存储这块,我见过最离谱的——把用户密码直接存到SharedPreferences里,key叫"password",value就是明文。这等于把家门钥匙挂在门外。
Android官方推荐的做法是:
- 使用EncryptedSharedPreferences:AndroidX Security库提供的,自动帮你做AES加密。
- 敏感数据用KeyStore:密钥存在硬件安全模块里,App拿不到密钥本身。
- 数据库加密:使用SQLCipher或Room的加密版本。
代码示例:
// 使用EncryptedSharedPreferences
MasterKey masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
// 存储和读取都是常规API,但数据在磁盘上是加密的
sharedPreferences.edit().putString("token", "your_token_here").apply();
String token = sharedPreferences.getString("token", null);
我的建议:不要自己实现加密算法。用现成的、经过验证的库。自己写AES加密,很容易踩到IV初始化、Padding模式、密钥管理的坑。我见过一个项目自己实现了「加密」,结果是把每个字符的ASCII码+1……这跟没加密有什么区别?
四、HTTPS证书校验:别让中间人得逞
HTTPS大家都用,但很多人只是「用了」,没有「校验」。默认的HttpsURLConnection会校验系统根证书,但如果你为了调试方便,把校验关了——那在生产环境里就是个大漏洞。
我见过这样的代码:
// 千万别这么干!
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}
};
这个TrustManager信任所有证书,包括自签名的、过期的、被篡改的。中间人攻击者只要伪造一个证书,就能解密你的所有通信数据。
正确的做法是:
- 证书锁定(Certificate Pinning):在客户端硬编码服务端证书的公钥或证书指纹,只信任这个证书。
- 使用OkHttp的CertificatePinner:OkHttp原生支持,配置起来很方便。
OkHttp的证书锁定示例:
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build())
.build();
注意:证书锁定后,如果服务端更换了证书,你的App就无法连接了。所以建议锁定多个备用证书,或者使用「证书透明度」机制。我个人习惯锁定两个证书:当前证书和备份证书,同时保留一个「紧急开关」——如果证书校验失败,可以远程下发新的证书指纹。
总结一下
安全优化不是一蹴而就的事。混淆加固、WebView安全、数据加密、HTTPS校验,这四个点每个都能写一整篇文章。但核心思路是一样的:不要信任任何输入,不要暴露任何敏感信息,不要给攻击者任何可乘之机。
嗯,今天就聊到这儿。安全这块,多花点心思,总比出事后再补救强得多。
公众号:蓝海资料掘金营,微信deep3321