一、问题背景
在开发需要账号密码登录的Android应用时,我们通常需要实现以下安全策略:
"连续输错密码5次后锁定账号10分钟,期间提示用户稍后重试。锁定状态在成功登录或设备重启后自动解除。"
传统方案的缺陷
早期实现可能直接依赖 System.currentTimeMillis() 记录锁定时间,但这种方式存在严重问题:
- 用户手动修改系统时间可绕过锁定机制
- 设备重启后无法准确判断是否应解除锁定
二、解决方案设计
核心思路
- 基于设备运行时间
- 使用
SystemClock.elapsedRealtime()(从系统启动开始累计的毫秒数,不受用户修改时间影响)
- 使用
- 可靠的重启检测
- 比较两次记录的设备运行时间,若当前值小于上次记录值,则判定为设备已重启
- 状态持久化
- 通过
SharedPreferences存储错误次数和锁定截止时间
- 通过
三、完整 Kotlin 实现
1. 定义常量
// Constants.kt
object LockConfig {
const val PREF_NAME = "account_lock_prefs"
const val KEY_ERROR_COUNT = "error_count"
const val KEY_LOCK_UNTIL = "lock_until" // 锁定截止时间(基于elapsedRealtime)
const val KEY_LAST_ELAPSED = "last_elapsed" // 上次设备运行时间
const val MAX_ATTEMPTS = 5
const val LOCK_DURATION_MS = 10 * 60 * 1000L // 10分钟(毫秒)
}
2. 设备重启检测
在 LoginActivity 的 onCreate 中初始化:
class LoginActivity : AppCompatActivity() {
private val prefs by lazy {
getSharedPreferences(LockConfig.PREF_NAME, MODE_PRIVATE)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
detectSystemReboot()
}
/** 检测设备是否重启并重置状态 */
private fun detectSystemReboot() {
val currentElapsed = SystemClock.elapsedRealtime()
val lastElapsed = prefs.getLong(KEY_LAST_ELAPSED, 0L)
// 当前时间小于上次记录时间 = 设备已重启
if (currentElapsed < lastElapsed) {
prefs.edit {
putInt(KEY_ERROR_COUNT, 0)
putLong(KEY_LOCK_UNTIL, 0L)
}
}
// 更新设备运行时间记录
prefs.edit { putLong(KEY_LAST_ELAPSED, currentElapsed) }
}
}
3. 登录逻辑实现
class LoginActivity : AppCompatActivity() {
// ...
fun attemptLogin(username: String, password: String) {
val (errorCount, lockUntil) = prefs.run {
getInt(KEY_ERROR_COUNT, 0) to getLong(KEY_LOCK_UNTIL, 0L)
}
val currentElapsed = SystemClock.elapsedRealtime()
// 检查锁定状态
if (lockUntil > currentElapsed) {
showLockMessage(lockUntil - currentElapsed)
return
}
when {
isValidCredentials(username, password) -> handleLoginSuccess()
else -> handleLoginError(errorCount, currentElapsed)
}
}
private fun showLockMessage(remainingMs: Long) {
val minutes = remainingMs / 60000
Toast.makeText(this, "请等待 ${minutes} 分钟后再试", LENGTH_SHORT).show()
}
private fun handleLoginSuccess() {
prefs.edit {
putInt(KEY_ERROR_COUNT, 0)
putLong(KEY_LOCK_UNTIL, 0L)
}
startActivity(Intent(this, MainActivity::class.java))
}
private fun handleLoginError(errorCount: Int, currentElapsed: Long) {
val newCount = errorCount + 1
prefs.edit {
if (newCount >= MAX_ATTEMPTS) {
putLong(KEY_LOCK_UNTIL, currentElapsed + LOCK_DURATION_MS)
Toast.makeText(this, "错误次数过多,请10分钟后重试", LENGTH_SHORT).show()
} else {
Toast.makeText(this, "密码错误,剩余尝试次数: ${MAX_ATTEMPTS - newCount}", LENGTH_SHORT).show()
}
putInt(KEY_ERROR_COUNT, newCount)
}
}
}
四、关键技术解析
1. 时间计算方案对比
| 方法 | 特点 | 是否受用户修改时间影响 |
|---|---|---|
System.currentTimeMillis() | 系统当前时间 | ✅ 受影响 |
SystemClock.elapsedRealtime() | 设备启动后运行时间 | ❌ 不受影响 |
2. 重启检测原理
val current = SystemClock.elapsedRealtime() // 当前设备运行时间
val last = prefs.getLong(KEY_LAST_ELAPSED, 0)
if (current < last) {
// 设备已重启,因为运行时间不可能倒流
resetLockState()
}
3. 状态流转图
stateDiagram-v2
[*] --> Idle: 初始状态
Idle --> Locked: 连续错误5次
Locked --> Idle: 10分钟到期或设备重启
Locked --> Idle: 成功登录
五、扩展思考
1. 增强安全性
- 使用
EncryptedSharedPreferences加密存储敏感数据 - 服务器端同步锁定状态(防止多设备绕过)
2. 用户体验优化
- 显示倒计时进度条
- 提供"忘记密码"快速入口
3. 测试要点
- 模拟修改系统时间验证锁定是否有效
- 快速重启设备检查状态重置
- 边界测试:第4次/第5次错误时的提示差异
六、总结
本文实现的登录锁定机制具有以下优势:
- 抗时间篡改:基于设备运行时间,无法通过修改系统时间绕过
- 低功耗:无需后台服务,仅依赖
SharedPreferences - 代码简洁:充分利用 Kotlin 特性(扩展函数、属性委托等)
最终代码已验证兼容性:
- Android 5.0+ (API 21+)
- 支持多语言/时区环境
你有更好的实现思路吗?欢迎在评论区讨论!