正在加载,请稍候…

移动应用安全最佳实践:证书锁定、安全存储与代码保护

全面指南涵盖证书锁定、安全存储、越狱/root检测、代码混淆及API安全,适用于iOS和Android应用

移动应用安全最佳实践:证书锁定、安全存储与代码保护

移动应用安全最佳实践:证书锁定、安全存储与代码保护

移动应用安全需要多层防护。从网络通信到本地数据存储,攻击者会探测每一个表面。本指南涵盖了适用于 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 请求进行签名和验证。在发布版本中应用代码混淆。定期进行安全审计和渗透测试。将这些层结合起来,可以构建纵深防御体系,保护您的用户和业务免受最常见的移动攻击向量的侵害。