
移动应用安全最佳实践:证书锁定、安全存储与代码保护
移动应用安全需要多层防护。从网络通信到本地数据存储,攻击者会探测每一个表面。本指南涵盖了适用于 iOS 和 Android 应用的生产级安全措施。
网络安全:证书锁定
证书锁定通过将服务器证书与已知可信值进行比对,防止中间人攻击。
iOS 证书锁定(使用 URLSession)
class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
private let pinnedCertificates: [Data]
init(certificates: [Data]) {
self.pinnedCertificates = certificates
}
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 验证证书链
var error: CFError?
guard SecTrustEvaluateWithError(serverTrust, &error) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 获取服务器证书
guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let serverCertData = SecCertificateCopyData(serverCertificate) as Data
// 与锁定证书比对
if pinnedCertificates.contains(serverCertData) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
// 报告潜在的 MITM 攻击
SecurityLogger.report(event: .certificatePinningFailure)
}
}
}
// 使用示例
let pinnedDelegate = PinnedURLSessionDelegate(
certificates: [loadCertificateData(named: "api.example.com")]
)
let session = URLSession(
configuration: .default,
delegate: pinnedDelegate,
delegateQueue: nil
)
Android 证书锁定(使用 OkHttp)
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 备用
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
网络安全配置(Android)
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- 备用锁定 -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</domain-config>
</network-security-config>
安全存储
iOS Keychain
import Security
class KeychainManager {
static func save(key: String, value: String) throws {
let data = value.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
}
static func load(key: String) throws -> String {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let value = String(data: data, encoding: .utf8) else {
throw KeychainError.loadFailed(status)
}
return value
}
static func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}
Android EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class SecureStorage(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
fun saveToken(token: String) {
sharedPreferences.edit()
.putString("auth_token", token)
.apply()
}
fun getToken(): String? = sharedPreferences.getString("auth_token", null)
fun clearToken() {
sharedPreferences.edit().remove("auth_token").apply()
}
}
越狱和 Root 检测
iOS 越狱检测
class IntegrityChecker {
static func isDeviceJailbroken() -> Bool {
// 检查越狱文件
let jailbreakPaths = [
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/",
]
for path in jailbreakPaths {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
// 尝试写入系统目录
let testPath = "/private/jailbreak_test_\(UUID().uuidString)"
do {
try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
try FileManager.default.removeItem(atPath: testPath)
return true
} catch {}
// 检查沙盒违规
if let _ = try? String(contentsOfFile: "/etc/passwd") {
return true
}
return false
}
}
Android Root 检测
object RootDetector {
fun isDeviceRooted(): Boolean {
return checkSuBinary() || checkBuildTags() || checkRootApps()
}
private fun checkSuBinary(): Boolean {
val paths = arrayOf(
"/system/bin/su", "/system/xbin/su",
"/sbin/su", "/data/local/xbin/su",
"/data/local/bin/su", "/data/local/su",
)
return paths.any { File(it).exists() }
}
private fun checkBuildTags(): Boolean {
val buildTags = Build.TAGS
return buildTags != null && buildTags.contains("test-keys")
}
private fun checkRootApps(): Boolean {
val rootApps = arrayOf(
"com.noshufou.android.su",
"com.thirdparty.superuser",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.zachspong.temprootremovejb",
)
val pm = appContext.packageManager
return rootApps.any { packageName ->
try {
pm.getPackageInfo(packageName, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
}
}
API 安全

JWT 令牌处理
import * as SecureStore from 'expo-secure-store';
import jwtDecode from 'jwt-decode';
class TokenManager {
private static ACCESS_TOKEN_KEY = 'access_token';
private static REFRESH_TOKEN_KEY = 'refresh_token';
static async saveTokens(accessToken: string, refreshToken: string) {
await Promise.all([
SecureStore.setItemAsync(this.ACCESS_TOKEN_KEY, accessToken),
SecureStore.setItemAsync(this.REFRESH_TOKEN_KEY, refreshToken),
]);
}
static async getAccessToken(): Promise<string | null> {
const token = await SecureStore.getItemAsync(this.ACCESS_TOKEN_KEY);
if (!token) return null;
const decoded = jwtDecode<{ exp: number }>(token);
const isExpired = decoded.exp * 1000 < Date.now() + 30000; // 30秒缓冲
if (isExpired) {
return this.refreshAccessToken();
}
return token;
}
static async refreshAccessToken(): Promise<string | null> {
const refreshToken = await SecureStore.getItemAsync(this.REFRESH_TOKEN_KEY);
if (!refreshToken) return null;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const { accessToken, newRefreshToken } = await response.json();
await this.saveTokens(accessToken, newRefreshToken);
return accessToken;
} catch {
await this.clearTokens();
return null;
}
}
static async clearTokens() {
await Promise.all([
SecureStore.deleteItemAsync(this.ACCESS_TOKEN_KEY),
SecureStore.deleteItemAsync(this.REFRESH_TOKEN_KEY),
]);
}
}
请求签名
import CryptoJS from 'crypto-js';
function signRequest(
method: string,
path: string,
body: object | null,
secretKey: string
): Record<string, string> {
const timestamp = Date.now().toString();
const nonce = Math.random().toString(36).substring(2);
const bodyHash = body
? CryptoJS.SHA256(JSON.stringify(body)).toString()
: '';
const message = `${method}\n${path}\n${timestamp}\n${nonce}\n${bodyHash}`;
const signature = CryptoJS.HmacSHA256(message, secretKey).toString();
return {
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
}
代码混淆
Android ProGuard/R8
# proguard-rules.pro
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose
# 保留模型类
-keep class com.example.app.models.** { *; }
# 混淆其他所有内容
-keepattributes Signature
-keepattributes *Annotation*
# 在发布版本中移除日志
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}
结论
移动应用安全不是单一功能,而是一门整体学科。实施证书锁定以防止网络拦截。使用平台原生安全存储来保护敏感数据。检测受损设备并做出适当响应。对 API 请求进行签名和验证。在发布版本中应用代码混淆。定期进行安全审计和渗透测试。将这些层结合起来,可以构建纵深防御体系,保护您的用户和业务免受最常见的移动攻击向量的侵害。