Android响应式身份验证架构设计与实现
时间:2026-08-15 | 作者:星河游者 | 阅读:0本文译自「An Android auth architecture with encrypted tokens, auto-refresh, and reactive na vigation」,原文链接proandroiddev.com/an-android-…,由Shivam Agrawal发布于2026年6月27日。
简介
大多数“身份验证演示”会将令牌存储在 SharedPreferences 中,然后就万事大吉了。
这种方法确实有效。但当你面对真实应用时,几个绕不开的问题就会出现:
如果在请求进行过程中访问令牌过期会发生什么?
冷启动时,存储的令牌已经失效会发生什么?
如何让 UI 知道在即时登录成功后跳转到主屏幕——无论在应 用内的任何位置,无需重启
Activity?
在打磨 [android-secure-auth-architecture](https://github.com/shiwal25/android-secure-auth-architecture) 的过程中,这些问题反复出现,几乎贯穿了整个实现。
这个项目本质上是一份参考实现。它没有把身份验证简单理解成“通过/未通过”的状态标记,而是把它当作一条响应式的加密数据流来设计。
整体基于 Jetpack Compose、Ktor、Koin 和 Google Tink 搭建。 接下来会把促成它最终落地的四个关键部分讲清楚。
_ 这只是一个架构展示,并非最终产品。后端是一个独立的服务,并未包含在内——
_BASE_URL_指向一个占位符。重点在于展示各个组件如何协同工作,而不是提供一个即插即用的库。
依赖项
gradle/libs.versions.toml
[versions]
koin = "4.2.2"
[libraries]
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
gradle/build.gradle.kts
//koin
implementation(libs.koin.androidx.compose)
implementation(libs.koin.android)
implementation(platform("io.insert-koin:koin-bom:4.2.1"))
implementation("io.insert-koin:koin-android")
implementation("io.insert-koin:koin-androidx-compose")
implementation("io.insert-koin:koin-androidx-compose-na vigation")
//Jetpack Na vigation
implementation("androidx.na vigation3:na vigation3-ui:1.1.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-na vigation3:2.10.0")
//LifeCyle and ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
//Ktor
implementation(platform("io.ktor:ktor-bom:3.5.0"))
implementation("io.ktor:ktor-client-android")
implementation("io.ktor:ktor-client-content-negotiation")
implementation("io.ktor:ktor-serialization-kotlinx-json")
implementation("io.ktor:ktor-client-logging")
implementation("io.ktor:ktor-client-auth")
//Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
//DataStore
implementation("androidx.security:security-crypto:1.1.0")
implementation("androidx.datastore:datastore:1.3.0-alpha09")
implementation("androidx.datastore:datastore-tink:1.3.0-alpha07")
implementation("com.google.crypto.tink:tink-android:1.21.0")
//Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")
Manifest
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".SecureAuthApplication"
...>
application>
加密令牌存储
TokenDataStore 使用来自 androidx.datastore.tink 的 AeadSerializer 封装了 DataStore。
实际的加密密钥是由 AndroidKeysetManager 管理的 AES-256-GCM 密钥集。它被一个始终存储在 Android Keystore 中的密钥封装,而 Android Keystore 在大多数设备上都由硬件支持。
val keysetHandle = AndroidKeysetManager.Builder()
.withSharedPref(context, KEYSET_NAME, KEYSET_PREFS_NAME)
.withKeyTemplate(KeyTemplate.createFrom(PredefinedAeadParameters.AES256_GCM))
.withMasterKeyUri(MASTER_KEY_URI)
.build()
.keysetHandle
val aead: Aead = keysetHandle.getPrimitive(
RegistryConfiguration.get(), Aead::class.ja va
)
val encryptedSerializer = AeadSerializer(
aead = aead,
wrappedSerializer = AuthPreferencesSerializer,
associatedData = DATASTORE_FILE_NAME.encodeToByteArray()
)
EncryptedSharedPreferences 目前处于维护模式,并且基于已弃用的旧版 Security Crypto 库。
DataStore + Tink 可以提供相同的由 Keystore 支持的 AEAD 加密。同时,它还基于 DataStore 的 Flow 协程原生 API。
这一点很关键。因为应用程序的其他部分只有在把身份验证状态视为流,而不是轮询值时,才能真正协同工作。
响应式身份验证状态
AuthViewModel 直接收集 TokenDataStore.accessToken:
private fun observeTokenForAuthState() {
viewModelScope.launch {
tokenDataStore.accessToken.collect { token ->
if (isFirstEmission) {
isFirstEmission = false
handleStartup(token)
} else {
_authState.value = if (token != null) {
AuthState.Authenticated
} else {
AuthState.Unauthenticated
}
}
}
}
}
登录、注销和静默刷新会话,最终产生的是完全相同的效果:同一个 Flow 发出新的数据,并由同一个收集器捕获。
登录后没有单独的“现在导航到主页”调用。 将令牌保存到 DataStore 本身,就是导航触发器。
这是我最满意的设计部分。它把三个不同的“用户身份验证状态更改”代码路径,合并成了一个。
冷启动时如何处理令牌
首次发出的数据会先交给 handleStartup() 做专门处理。
原因很简单:冷启动比其他场景多了一个必须先确认的问题。那就是本地保存的令牌,现在到底还有效吗?
这里的 JwtUtils 会直接在本地解码 JWT 的有效负载,整个过程不需要发起网络调用。
同时,它还会带上 30 秒的缓冲时间去检查 exp 声明,避免请求恰好撞上令牌临近过期的时间点。
如果发现令牌已经失效,ViewModel 就会先触发刷新。随后再进一步判断,用户该进入主屏幕,还是跳转到登录屏幕。
根导航的职责
根导航只是监视 authState 并切换图。它对“已登录”的含义没有任何自己的看法:
when (state) {
AuthState.Loading -> SplashContent()
AuthState.Unauthenticated -> AuthNa vGraph(authViewModel = authViewModel)
AuthState.Authenticated -> MainNa vGraph(authViewModel = authViewModel)
}
自动刷新,通过 Ktor 的 **Auth** 插件
任何 JWT 应用程序中都有一个很典型的失败模式:经过身份验证的请求已经发出,但访问令牌刚好过期。
这时,请求需要在刷新后透明地重试。同时,每个存储库方法又不应该知道刷新逻辑。
Ktor 的 Auth { bearer { ... } } 提供者,正是为了解决这个问题。
install(Auth) {
bearer {
loadTokens {
// read current access + refresh token from DataStore
}
refreshTokens {
val response = plainClient.post(refreshUrl) {
setBody(RefreshRequest(refreshToken = storedRefreshToken))
}
when (response.status) {
HttpStatusCode.OK -> {
// sa ve new tokens, return them as BearerTokens
}
HttpStatusCode.Unauthorized,
HttpStatusCode.Forbidden -> {
tokenDataStore.clearSession()
null
}
else -> null
}
}
sendWithoutRequest { request -> request.url.host == BACKEND_HOST }
}
}
这里有三个深思熟虑的决定:
刷新调用使用普通客户端,而不是经过身份验证的客户端。 否则刷新请求本身可能会触发另一次刷新尝试并递归。
刷新期间的 5xx 不会清除会话。 只有“401”/“403”(服务器明确拒绝刷新令牌)才会使用户注销。暂时性服务器错误只会导致该请求失败;用户保持登录状态并可以重试。
前面提到的 30 秒过期缓冲区 是防止整个事情争分夺秒的原因。
两个 HTTP 客户端
HttpClientFactory 构建一个 plain 客户端,用于 /login、/register、/refresh。
同时,它还构建一个单独的 auth 客户端。这个客户端安装了 Auth 插件,用于需要登录用户的所有内容。
Koin 通过命名限定符将这些连接到正确的存储库中。
因此,从结构上来说,不知道如何刷新令牌的客户端意外调用经过身份验证的端点,是不可能的。
DI 图本身强制执行边界,而不是依赖代码审查来捕获它。
我做出的权衡,并将大规模重新考虑
Koin over Hilt :在单独的早期开发过程中迭代速度更快;没有代码生成,并且运行时 DI 图很容易根据该项目的规模进行推理。随着模块数量的增加,Hilt 的编译时安全性变得更有价值。
Na vigation 3 优于经典的 Na vigation Compose :较新,并且 API 表面仍在解决中,但基于“Na vKey”的返回堆栈比字符串路由更自然地适合类型安全、状态驱动的图形切换。
Tink + DataStore over
**EncryptedSharedPreferences**:主要是关于不构建维护模式 API。**Result跨越存储库边界抛出异常:每个** AuthRepository方法都会返回一个Result以及人类可读的失败消息,因此 ViewModel 永远不需要捕获任何内容,它只是在成功或失败时进行模式匹配。
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- 不卖课纯干货:详解Android分层架构与面试策略
- 时间:2026-08-27
-
- Jetpack 和 AndroidX 到底是什么关系?一篇讲清概念、区别与实际用法
- 时间:2026-08-25
-
- Android Spinner实战:用法、案例与常见问题解答
- 时间:2026-08-24
-
- Android 多层嵌套 RecyclerView 滚动详解:从 Fling 中断到丝滑联动
- 时间:2026-08-24
-
- Android Activity启动模式详解与应用场景解析
- 时间:2026-08-23
-
- Android 自定义 View 实战:打造一个跟随滑动的丝滑指示器
- 时间:2026-08-22
-
- 谷歌Android推虚假来电检测功能 基于RCS防范AI伪造诈骗
- 时间:2026-08-21
-
- Android工程师快速入门Dart开发实战攻略
- 时间:2026-08-21
精选合集
更多大家都在玩
大家都在看
更多-
- 糖尿病完全不能吃糖吗
- 时间:2026-09-15
-
- 蚂蚁庄园小课堂2026年9月16日最新题目答案
- 时间:2026-09-15
-
- 小鸡答题今天的答案是什么2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园每日答题答案2026年9月16日
- 时间:2026-09-15
-
- 以下哪种粮食是酿造绍兴黄酒的主要原料 蚂蚁庄园今日答案9月16日
- 时间:2026-09-15
-
- 劝学名句“及时当勉励,岁月不待人”出自哪位诗人 蚂蚁庄园今日答案9.16
- 时间:2026-09-15
-
- 蚂蚁庄园今天答题答案2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园答题今日答案2026年9月16日
- 时间:2026-09-15