公司动态
Android安全存储:SharedPreferences实现记住密码功能
1. 项目概述在Android应用开发中记住密码功能几乎是所有需要登录的应用标配。这个看似简单的功能背后涉及到用户数据的安全存储和便捷读取的平衡问题。SharedPreferences作为Android平台提供的一种轻量级数据存储方案非常适合用来实现这类小型键值对数据的持久化。我曾在多个商业项目中实现过记住密码功能踩过不少坑也积累了一些实用技巧。本文将基于SharedPreferences手把手带你实现一个安全可靠的记住密码功能同时分享那些官方文档里不会告诉你的实战经验。2. 核心实现原理2.1 SharedPreferences工作机制SharedPreferences本质上是一个XML文件存储在应用的/data/data/package_name/shared_prefs目录下。它的工作流程是这样的当首次调用getSharedPreferences()时系统会检查对应文件是否存在如果不存在则创建新文件存在则将其内容加载到内存中的Map对象后续的读写操作都是在内存中进行效率极高调用apply()或commit()时才会将内存中的数据异步写入文件这种机制带来的优势是读写速度快内存操作自动处理文件IO线程安全内部使用同步锁2.2 密码存储的安全考量直接存储明文密码是绝对不可取的我们需要对密码进行加密处理。常见的方案有对称加密如AES加解密使用相同密钥速度快适合移动设备需要妥善保管密钥非对称加密如RSA公钥加密私钥解密更安全但性能较差实现复杂度高哈希处理只能验证不能还原适合服务端存储本地记住密码场景不适用对于移动端记住密码功能推荐使用AES对称加密配合Android Keystore系统来保护密钥安全。3. 完整实现步骤3.1 基础工具类封装首先创建一个安全的SharedPreferences工具类object PreferenceHelper { private const val PREFS_NAME user_prefs private const val KEY_USERNAME username private const val KEY_PASSWORD password private const val KEY_REMEMBER remember_me private val sharedPref: SharedPreferences by lazy { App.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) } // AES加密工具实例 private val cryptoUtil by lazy { CryptoUtil() } fun saveCredentials(username: String, password: String, remember: Boolean) { sharedPref.edit().apply { putString(KEY_USERNAME, username) // 密码加密存储 putString(KEY_PASSWORD, cryptoUtil.encrypt(password)) putBoolean(KEY_REMEMBER, remember) apply() } } fun getCredentials(): PairString?, String? { val username sharedPref.getString(KEY_USERNAME, null) val encryptedPwd sharedPref.getString(KEY_PASSWORD, null) val password encryptedPwd?.let { cryptoUtil.decrypt(it) } return Pair(username, password) } fun shouldRemember(): Boolean { return sharedPref.getBoolean(KEY_REMEMBER, false) } fun clearCredentials() { sharedPref.edit().clear().apply() } }3.2 加密工具类实现下面是AES加密工具类的关键代码class CryptoUtil { private val key by lazy { // 从Android Keystore获取密钥 KeyStoreHelper.getOrCreateKey() } fun encrypt(data: String): String { val cipher Cipher.getInstance(AES/GCM/NoPadding) cipher.init(Cipher.ENCRYPT_MODE, key) val iv cipher.iv val encrypted cipher.doFinal(data.toByteArray(Charsets.UTF_8)) // 将IV和加密数据一起存储 return Base64.encodeToString(iv encrypted, Base64.DEFAULT) } fun decrypt(encryptedData: String): String? { return try { val decoded Base64.decode(encryptedData, Base64.DEFAULT) val iv decoded.copyOfRange(0, 12) // GCM IV长度通常为12字节 val data decoded.copyOfRange(12, decoded.size) val cipher Cipher.getInstance(AES/GCM/NoPadding) val spec GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, key, spec) String(cipher.doFinal(data), Charsets.UTF_8) } catch (e: Exception) { null } } }3.3 登录界面集成在登录Activity中的典型使用方式class LoginActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) // 自动填充记住的账号密码 if (PreferenceHelper.shouldRemember()) { val (username, password) PreferenceHelper.getCredentials() etUsername.setText(username) etPassword.setText(password) cbRemember.isChecked true } btnLogin.setOnClickListener { val username etUsername.text.toString() val password etPassword.text.toString() val remember cbRemember.isChecked // 验证登录... if (loginSuccess) { if (remember) { PreferenceHelper.saveCredentials(username, password, true) } else { PreferenceHelper.clearCredentials() } } } } }4. 安全增强方案4.1 使用Android Keystore保护密钥普通SharedPreferences存储加密密钥是不安全的应该使用Android Keystore系统object KeyStoreHelper { private const val KEY_ALIAS com.example.app.crypto_key private const val ANDROID_KEYSTORE AndroidKeyStore fun getOrCreateKey(): SecretKey { val keyStore KeyStore.getInstance(ANDROID_KEYSTORE) keyStore.load(null) if (!keyStore.containsAlias(KEY_ALIAS)) { val keyGenerator KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE ) val keySpec KeyGenParameterSpec.Builder( KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ).apply { setBlockModes(KeyProperties.BLOCK_MODE_GCM) setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) setRandomizedEncryptionRequired(true) setUserAuthenticationRequired(false) }.build() keyGenerator.init(keySpec) keyGenerator.generateKey() } return (keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry).secretKey } }4.2 防止数据篡改可以增加HMAC校验来验证数据完整性fun encryptWithHMAC(data: String): String { val encrypted encrypt(data) val hmac calculateHMAC(encrypted) return $encrypted|$hmac } fun decryptWithHMAC(encryptedData: String): String? { val parts encryptedData.split(|) if (parts.size ! 2) return null val (data, hmac) parts if (calculateHMAC(data) ! hmac) { // 数据被篡改 return null } return decrypt(data) } private fun calculateHMAC(data: String): String { // 使用Keystore保护的HMAC密钥 val key getHMACKey() val mac Mac.getInstance(HmacSHA256) mac.init(key) return Base64.encodeToString(mac.doFinal(data.toByteArray()), Base64.DEFAULT) }5. 性能优化与问题排查5.1 性能优化建议避免频繁写入批量操作时使用单个Editor实例优先使用apply()而非commit()控制数据量SharedPreferences不适合存储大文件单个文件建议不超过1MB懒加载优化private val sharedPref: SharedPreferences by lazy { App.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }5.2 常见问题排查数据不同步问题现象修改后立即读取发现值未更新原因apply()是异步的内存更新可能有延迟解决关键场景使用commit()或添加回调机制跨进程问题SharedPreferences不支持多进程安全解决方案使用ContentProvider封装迁移到支持多进程的存储方案加密失败处理fun decryptSafe(encrypted: String?): String? { return try { encrypted?.let { decrypt(it) } } catch (e: Exception) { // 记录异常并清除无效数据 PreferenceHelper.clearCredentials() null } }6. 替代方案比较虽然SharedPreferences适合简单场景但在复杂情况下可以考虑方案优点缺点适用场景SharedPreferences简单易用Android原生支持不适合复杂数据性能随数据量下降小型配置数据DataStore支持协程类型安全需要引入新库学习成本略高新项目推荐Room强大的查询能力支持复杂关系配置复杂资源占用高结构化数据存储文件存储灵活无大小限制需要手动处理IO和线程安全大文件或自定义格式对于记住密码功能如果只是存储简单的用户名密码SharedPreferences仍然是合适的选择。但如果应用有更复杂的需求如多设备同步、历史记录等建议考虑DataStore或Room。