移动端登录态安全设计(5):OkHttp 登录态闭环:Interceptor、Authenticator、401 自动刷新

前面几篇我们已经把 Token 的本地安全存储讲清楚了:

复制代码
第1篇:App 登录时密码到底要不要加密?为什么通常走 HTTPS?
第2篇:Token 拿到后,为什么不能明文存?
第3篇:AES-GCM + Android Keystore:Android Token 本地安全存储
第4篇:DataStore、MMKV、SharedPreferences:Token 到底应该存哪里?

到这里,移动端已经解决了一个核心问题:

Token 怎么安全保存?

但是登录态安全不只是"保存 Token"。

真正的 App 网络库里,还要解决:

复制代码
请求时怎么自动加 accessToken?
accessToken 过期了怎么办?
服务端返回 401,客户端怎么刷新 Token?
多个接口同时 401,怎么避免重复刷新?
refreshToken 也失效了,怎么退出登录?

这篇文章就讲移动端登录态的 OkHttp 闭环:

Interceptor 负责加 Token,Authenticator 负责处理 401,TokenManager 负责管理登录态。


一、先看完整登录态链路

一个比较完整的移动端登录态流程大概是这样:

复制代码
用户登录成功
  ↓
后端返回 accessToken + refreshToken
  ↓
客户端加密保存 Token
  ↓
TokenManager 恢复 Token 到内存
  ↓
OkHttp Interceptor 给请求添加 Authorization
  ↓
后端校验 accessToken
  ↓
如果 accessToken 有效,正常返回数据
  ↓
如果 accessToken 过期,后端返回 401
  ↓
OkHttp Authenticator 尝试用 refreshToken 刷新
  ↓
刷新成功,保存新 Token,重新发起原请求
  ↓
刷新失败,清空 Token,跳登录页

这一整套才叫:

复制代码
移动端登录态闭环

如果只做了 Interceptor 加 Token,没有处理 401 自动刷新,那么用户体验会很差。

如果只做了 401 刷新,但没有并发控制,那么首页多个接口同时过期时,可能会同时发起多个刷新请求。

如果只做了刷新,但刷新失败不清登录态,就会出现登录状态混乱。

所以登录态不是一个点,而是一条链。


二、Interceptor 负责什么?

OkHttp 的 Interceptor 主要负责拦截请求和响应。

在登录态场景中,最常见的作用是:

复制代码
给请求统一添加 Authorization Header

比如:

复制代码
Authorization: Bearer accessToken

也就是说,业务接口不用每个都手动传 Token。

只要请求经过 OkHttp,Interceptor 会自动帮你加上。

示例:

复制代码
class AuthInterceptor(
    private val tokenManager: TokenManager
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()

        val accessToken = tokenManager.getAccessTokenFromMemory()

        val newRequest = if (accessToken.isNullOrBlank()) {
            originalRequest
        } else {
            originalRequest.newBuilder()
                .header("Authorization", "Bearer $accessToken")
                .build()
        }

        return chain.proceed(newRequest)
    }
}

这段代码的职责很清楚:

复制代码
1. 从 TokenManager 拿 accessToken
2. 如果 Token 不为空,就加 Authorization Header
3. 继续执行请求

注意,Interceptor 不应该关心:

复制代码
Token 存在哪里
Token 怎么加密
Token 怎么刷新
Token 什么时候失效

这些都应该交给 TokenManager 和 Authenticator。


三、Authenticator 负责什么?

Interceptor 是请求前加 Token。

Authenticator 是收到 401 后处理认证失败。

当后端返回:

复制代码
HTTP/1.1 401 Unauthorized

OkHttp 可以通过 Authenticator 尝试重新认证。

在登录态场景中,它通常负责:

复制代码
用 refreshToken 刷新 accessToken
刷新成功后,重新构造原请求
刷新失败后,返回 null,让请求失败

伪流程:

复制代码
业务请求返回 401
  ↓
Authenticator 被调用
  ↓
读取 refreshToken
  ↓
请求刷新 Token 接口
  ↓
刷新成功:保存新 Token,重试原请求
  ↓
刷新失败:清空登录态,返回 null

所以 Interceptor 和 Authenticator 的分工是:

复制代码
Interceptor:
请求前加 Token。

Authenticator:
401 后刷新 Token。

四、为什么不能只在 Interceptor 里处理 401?

有些项目会在 Interceptor 里判断响应码:

复制代码
val response = chain.proceed(request)

if (response.code == 401) {
    // refresh token
}

这种方式不是不能做,但容易把 Interceptor 写得很重。

因为 Interceptor 本来已经负责加 Header,如果再处理:

复制代码
401 判断
refreshToken 调用
刷新并发锁
重试请求
退出登录

最后就会变成一个巨大的网络拦截器。

更清晰的职责划分是:

复制代码
AuthInterceptor:
只负责加 Authorization。

TokenAuthenticator:
只负责 401 后刷新 Token 和重试请求。

TokenManager:
负责 Token 保存、读取、清理。

LoginStateManager:
负责登录态失效通知。

这样结构更清楚,也更容易维护。


五、TokenManager 应该承担什么职责?

TokenManager 是登录态的核心管理类。

它不应该只是一个简单的 SharedPreferences 工具类。

它应该负责:

复制代码
保存 Token
读取 Token
恢复 Token 到内存
获取 accessToken
获取 refreshToken
更新 Token
清空 Token
判断是否登录

示例:

复制代码
data class TokenEntity(
    val accessToken: String,
    val refreshToken: String,
    val expiresAt: Long
)

TokenManager:

复制代码
class TokenManager(
    private val secureTokenStore: SecureTokenStore
) {

    @Volatile
    private var memoryToken: TokenEntity? = null

    suspend fun saveToken(token: TokenEntity) {
        memoryToken = token
        secureTokenStore.saveToken(token)
    }

    suspend fun restoreToken(): TokenEntity? {
        val token = secureTokenStore.getToken()
        memoryToken = token
        return token
    }

    fun getAccessTokenFromMemory(): String? {
        return memoryToken?.accessToken
    }

    fun getRefreshTokenFromMemory(): String? {
        return memoryToken?.refreshToken
    }

    suspend fun getToken(): TokenEntity? {
        memoryToken?.let {
            return it
        }

        return restoreToken()
    }

    suspend fun clearToken() {
        memoryToken = null
        secureTokenStore.clearToken()
    }
}

这里有一个重点:

复制代码
accessToken 可以放一份内存缓存。
refreshToken 必须加密持久化。

因为 OkHttp Interceptor 是同步接口,直接在里面读 DataStore 会很别扭。

所以更推荐:

复制代码
App 启动时恢复 Token 到内存
请求时 Interceptor 从内存拿 accessToken
刷新成功后更新内存和本地密文
退出登录时同时清内存和本地密文

六、刷新 Token 接口怎么设计?

一般刷新接口大概是:

复制代码
POST /auth/refresh

请求:

复制代码
{
  "refreshToken": "xxx"
}

响应:

复制代码
{
  "accessToken": "new_access_token",
  "refreshToken": "new_refresh_token",
  "expiresAt": 1780000000000
}

更推荐服务端做 refreshToken 轮换:

复制代码
每次刷新成功,都返回新的 accessToken 和新的 refreshToken。
旧 refreshToken 立即失效。

这样可以降低 refreshToken 泄漏后的风险。

移动端拿到新的 Token 后,要立刻:

复制代码
1. 更新内存 Token
2. 加密保存新 Token
3. 使用新 accessToken 重试原请求

七、Authenticator 基础实现

先看一个基础版本:

复制代码
class TokenAuthenticator(
    private val tokenManager: TokenManager,
    private val authApi: AuthApi,
    private val loginStateManager: LoginStateManager
) : Authenticator {

    override fun authenticate(route: Route?, response: Response): Request? {
        // 避免无限重试
        if (responseCount(response) >= 2) {
            return null
        }

        val newToken = runBlocking {
            refreshToken()
        } ?: return null

        return response.request.newBuilder()
            .header("Authorization", "Bearer ${newToken.accessToken}")
            .build()
    }

    private suspend fun refreshToken(): TokenEntity? {
        val token = tokenManager.getToken() ?: return null

        return runCatching {
            val result = authApi.refreshToken(
                RefreshTokenRequest(token.refreshToken)
            )

            val newToken = TokenEntity(
                accessToken = result.accessToken,
                refreshToken = result.refreshToken,
                expiresAt = result.expiresAt
            )

            tokenManager.saveToken(newToken)

            newToken
        }.getOrElse {
            tokenManager.clearToken()
            loginStateManager.notifyLoginExpired()
            null
        }
    }

    private fun responseCount(response: Response): Int {
        var count = 1
        var priorResponse = response.priorResponse

        while (priorResponse != null) {
            count++
            priorResponse = priorResponse.priorResponse
        }

        return count
    }
}

这个版本能表达基本思路:

复制代码
401 后刷新 Token
刷新成功后重试原请求
刷新失败后清 Token
避免无限重试

但它还不够完整。

因为真实项目里会遇到一个经典问题:

多个接口同时 401 怎么办?


八、多个接口同时 401 的问题

假设首页同时请求 5 个接口:

复制代码
/user/info
/order/list
/message/count
/config
/banner

这时 accessToken 刚好过期。

于是 5 个接口同时返回 401。

如果每个请求都进入 Authenticator,然后都去刷新 Token,就会出现:

复制代码
请求 A 发起 refreshToken
请求 B 发起 refreshToken
请求 C 发起 refreshToken
请求 D 发起 refreshToken
请求 E 发起 refreshToken

这会带来几个问题:

复制代码
1. 重复刷新,浪费请求。
2. 如果后端 refreshToken 轮换,第一次刷新成功后旧 refreshToken 失效。
3. 后面几个刷新请求还拿旧 refreshToken 去刷新,可能全部失败。
4. 客户端状态混乱。
5. 可能误触发退出登录。

所以 401 自动刷新必须加锁。

核心原则是:

同一时间只允许一个刷新请求执行,其他 401 请求等待刷新结果。


九、刷新 Token 必须加锁

可以在 TokenAuthenticator 里加一个锁。

因为 OkHttp 的 Authenticator 是同步接口,可以用 synchronizedMutex + runBlocking

这里先用比较直观的 synchronized 版本。

复制代码
class TokenAuthenticator(
    private val tokenManager: TokenManager,
    private val authApi: AuthApi,
    private val loginStateManager: LoginStateManager
) : Authenticator {

    private val refreshLock = Any()

    override fun authenticate(route: Route?, response: Response): Request? {
        if (responseCount(response) >= 2) {
            return null
        }

        synchronized(refreshLock) {
            val requestToken = response.request.header("Authorization")
                ?.removePrefix("Bearer ")
                ?.trim()

            val currentToken = tokenManager.getAccessTokenFromMemory()

            // 说明别的请求已经刷新成功了
            if (!currentToken.isNullOrBlank() && currentToken != requestToken) {
                return response.request.newBuilder()
                    .header("Authorization", "Bearer $currentToken")
                    .build()
            }

            val newToken = runBlocking {
                refreshTokenInternal()
            } ?: return null

            return response.request.newBuilder()
                .header("Authorization", "Bearer ${newToken.accessToken}")
                .build()
        }
    }

    private suspend fun refreshTokenInternal(): TokenEntity? {
        val token = tokenManager.getToken()

        if (token?.refreshToken.isNullOrBlank()) {
            tokenManager.clearToken()
            loginStateManager.notifyLoginExpired()
            return null
        }

        return runCatching {
            val result = authApi.refreshToken(
                RefreshTokenRequest(token!!.refreshToken)
            )

            val newToken = TokenEntity(
                accessToken = result.accessToken,
                refreshToken = result.refreshToken,
                expiresAt = result.expiresAt
            )

            tokenManager.saveToken(newToken)

            newToken
        }.getOrElse {
            tokenManager.clearToken()
            loginStateManager.notifyLoginExpired()
            null
        }
    }

    private fun responseCount(response: Response): Int {
        var count = 1
        var priorResponse = response.priorResponse

        while (priorResponse != null) {
            count++
            priorResponse = priorResponse.priorResponse
        }

        return count
    }
}

这里有一个关键判断:

复制代码
if (!currentToken.isNullOrBlank() && currentToken != requestToken) {
    return response.request.newBuilder()
        .header("Authorization", "Bearer $currentToken")
        .build()
}

它的意思是:

复制代码
当前失败请求用的是旧 accessToken。
但是内存里的 accessToken 已经变成新的了。
说明其他请求已经刷新成功。
当前请求不需要再刷新,直接用新 Token 重试即可。

这个判断非常重要。

它可以避免多个 401 请求重复刷新。


十、为什么要判断 responseCount?

如果刷新后重试仍然 401,Authenticator 可能再次被调用。

如果不限制,就可能无限循环:

复制代码
请求接口
  ↓
401
  ↓
刷新 Token
  ↓
重试请求
  ↓
还是 401
  ↓
再次刷新
  ↓
再次重试
  ↓
死循环

所以要限制重试次数。

复制代码
if (responseCount(response) >= 2) {
    return null
}

意思是:

复制代码
当前请求已经重试过一次了,就不要继续刷新了。

返回 null 后,OkHttp 会把原来的 401 返回给上层。

上层可以统一识别登录态失效。


十一、哪些接口不能触发刷新?

不是所有 401 都应该触发刷新。

比如:

复制代码
登录接口返回 401
刷新 Token 接口返回 401
退出登录接口返回 401
注册接口返回 401
验证码接口返回 401

这些接口不应该再触发 refresh。

否则可能出现:

复制代码
refreshToken 接口 401
  ↓
Authenticator 又调用 refreshToken
  ↓
又 401
  ↓
死循环

可以通过请求 tag 或 path 判断。

示例:

复制代码
private fun shouldSkipAuth(request: Request): Boolean {
    val path = request.url.encodedPath

    return path.contains("/auth/login") ||
            path.contains("/auth/refresh") ||
            path.contains("/auth/logout") ||
            path.contains("/auth/register")
}

在 Authenticator 里:

复制代码
override fun authenticate(route: Route?, response: Response): Request? {
    if (shouldSkipAuth(response.request)) {
        return null
    }

    if (responseCount(response) >= 2) {
        return null
    }

    // refresh token
}

这样可以避免认证接口之间互相触发。


十二、刷新接口不要共用同一个带 Authenticator 的 OkHttpClient

这是一个很容易忽视的坑。

如果 refreshToken 接口也使用同一个 OkHttpClient,而这个 client 上挂了 TokenAuthenticator。

当 refreshToken 接口自己返回 401 时,可能会再次触发 Authenticator。

所以更稳的做法是:

复制代码
业务接口 OkHttpClient:
带 AuthInterceptor
带 TokenAuthenticator

刷新 Token 接口 OkHttpClient:
可以不带 TokenAuthenticator
或者明确跳过认证逻辑

也就是说,刷新接口最好有清晰边界。

不要让 refreshToken 请求自己又触发 refreshToken。


十三、runBlocking 能不能用?

Authenticator 的接口是同步的:

复制代码
override fun authenticate(route: Route?, response: Response): Request?

而你项目里的 Token 存储、刷新接口可能是 suspend 函数。

这时很多人会在 Authenticator 里用:

复制代码
runBlocking {
    refreshToken()
}

能不能用?

可以用,但要谨慎。

因为 Authenticator 本身运行在 OkHttp 的线程里,runBlocking 会阻塞当前线程。

所以要注意:

复制代码
1. 不要在里面做很重的操作。
2. refreshToken 接口不要再依赖当前卡住的请求链导致死锁。
3. 刷新逻辑要尽快返回。
4. 避免多个请求同时 runBlocking 刷新,所以必须加锁。

更稳的工程做法是:

复制代码
TokenManager 内存缓存 accessToken。
refreshToken 使用独立 OkHttpClient。
Authenticator 内部只做必要同步刷新。

如果团队想完全避免 suspend,可以给 refreshToken 单独提供同步 API。


十四、刷新成功后要更新哪里?

刷新成功后,不是只更新内存就完了。

应该同时更新:

复制代码
1. 内存 Token
2. 本地加密 Token
3. 后续请求使用的新 accessToken

流程:

复制代码
refreshToken 成功
  ↓
拿到 newAccessToken + newRefreshToken
  ↓
tokenManager.saveToken(newToken)
  ↓
内部更新 memoryToken
  ↓
内部 AES-GCM 加密保存到 DataStore / MMKV
  ↓
Authenticator 用新 accessToken 重试原请求

这里有一个重点:

复制代码
如果服务端返回了新的 refreshToken,客户端必须保存新的 refreshToken。

不要只保存 accessToken。

否则下一次刷新时,还拿旧 refreshToken,可能会失败。


十五、刷新失败后怎么处理?

刷新失败通常意味着:

复制代码
refreshToken 过期
refreshToken 被服务端删除
用户在其他设备改密
后台踢下线
服务端 Token 版本变了
账号状态异常

这时客户端应该:

复制代码
1. 清空内存 Token
2. 清空本地加密 Token
3. 通知登录态失效
4. 跳转登录页
5. 避免重复弹多个登录失效弹窗

示例:

复制代码
class LoginStateManager {

    private val _loginExpiredEvent = MutableSharedFlow<Unit>(
        replay = 0,
        extraBufferCapacity = 1
    )

    val loginExpiredEvent = _loginExpiredEvent.asSharedFlow()

    fun notifyLoginExpired() {
        _loginExpiredEvent.tryEmit(Unit)
    }
}

上层统一监听:

复制代码
lifecycleScope.launch {
    loginStateManager.loginExpiredEvent.collect {
        // 清页面栈
        // 跳转登录页
        // 提示登录已过期
    }
}

注意:

复制代码
登录失效事件要统一处理。
不要每个接口自己弹登录失效。

否则首页 5 个接口同时失败,可能弹 5 次。


十六、如何避免重复弹登录失效?

可以在 LoginStateManager 里做一个状态保护:

复制代码
class LoginStateManager {

    @Volatile
    private var hasNotifiedExpired = false

    private val _loginExpiredEvent = MutableSharedFlow<Unit>(
        replay = 0,
        extraBufferCapacity = 1
    )

    val loginExpiredEvent = _loginExpiredEvent.asSharedFlow()

    fun notifyLoginExpired() {
        if (hasNotifiedExpired) {
            return
        }

        hasNotifiedExpired = true
        _loginExpiredEvent.tryEmit(Unit)
    }

    fun reset() {
        hasNotifiedExpired = false
    }
}

登录成功后:

复制代码
loginStateManager.reset()

这样可以避免多个请求同时触发登录失效事件。


十七、服务端应该怎么配合?

客户端 401 自动刷新不是客户端单方面能完成的。

服务端也要有清晰语义。

建议后端区分:

复制代码
accessToken 过期:
返回 401,允许客户端用 refreshToken 刷新。

refreshToken 过期:
刷新接口返回 401 或业务错误码,客户端退出登录。

Token 被踢下线:
返回明确错误码,比如 TOKEN_KICKED。

密码已修改:
返回明确错误码,比如 TOKEN_VERSION_EXPIRED。

账号被冻结:
返回明确错误码,比如 ACCOUNT_DISABLED。

客户端拿到这些错误后,可以统一处理:

复制代码
刷新成功:重试原请求
刷新失败:清 Token,回登录页
被踢下线:清 Token,提示账号已在其他设备登录
密码修改:清 Token,提示请重新登录
账号冻结:清 Token,提示账号状态异常

如果后端只返回一个模糊的 401,客户端就很难做精细化处理。


十八、401 和业务错误码怎么配合?

有些公司喜欢所有错误都返回 HTTP 200,然后通过 code 判断:

复制代码
{
  "code": 401,
  "msg": "token expired"
}

有些公司喜欢标准 HTTP 状态码:

复制代码
HTTP/1.1 401 Unauthorized

两种都能做,但要统一。

如果使用 OkHttp Authenticator,最好让服务端对认证失败返回真正的 HTTP 401。

因为 Authenticator 是基于 HTTP 401 触发的。

如果服务端一直返回 HTTP 200,那 Authenticator 不会自动触发。

这时只能在业务拦截器里解析 body code,再手动处理刷新。

所以如果你想利用 OkHttp Authenticator,后端最好这样设计:

复制代码
accessToken 无效 / 过期:
HTTP 401

refreshToken 失效:
HTTP 401 或明确业务错误码

权限不足:
HTTP 403

业务失败:
HTTP 200 + 业务 code,或者使用规范错误状态

不要把认证失败、权限不足、业务异常全部混成一个 code。


十九、Interceptor 和 Authenticator 的完整组合

OkHttpClient 可以这样配置:

复制代码
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(authInterceptor)
    .authenticator(tokenAuthenticator)
    .addInterceptor(loggingInterceptor)
    .build()

注意日志拦截器要脱敏:

复制代码
val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = HttpLoggingInterceptor.Level.BODY
    redactHeader("Authorization")
    redactHeader("Cookie")
}

如果你有自己的日志系统,也要统一处理:

复制代码
Authorization
accessToken
refreshToken
password
Cookie
验证码

不要一边加密存储 Token,一边在日志里把 Token 打出来。


二十、推荐的整体结构

最终结构可以这样:

复制代码
OkHttpClient
  ├── AuthInterceptor
  │      └── TokenManager.getAccessTokenFromMemory()
  │
  ├── TokenAuthenticator
  │      ├── 监听 401
  │      ├── 加锁刷新 Token
  │      ├── 保存新 Token
  │      └── 刷新失败通知登录失效
  │
  └── LoggingInterceptor
         └── 敏感 Header 脱敏


TokenManager
  ├── memoryToken
  ├── saveToken()
  ├── restoreToken()
  ├── getAccessTokenFromMemory()
  └── clearToken()


SecureTokenStore
  ├── AES-GCM 加密 Token
  ├── Android Keystore 保护 AES key
  └── DataStore / MMKV 保存密文

这套结构的重点是:

复制代码
拦截器只加 Header
Authenticator 只处理 401
TokenManager 管登录态
SecureTokenStore 管安全存储
LoginStateManager 管登录失效事件

职责清楚,后面才好维护。


二十一、常见错误总结

错误 1:业务代码到处手动加 Token

不推荐。

应该由 AuthInterceptor 统一加。


错误 2:Interceptor 里直接读 DataStore

不推荐。

建议 App 启动时恢复 Token 到内存,Interceptor 从 TokenManager 读内存。


错误 3:401 后每个请求都刷新 Token

错误。

必须加锁,同一时间只允许一个刷新请求。


错误 4:刷新成功只保存 accessToken

不完整。

如果后端返回新的 refreshToken,也要一起保存。


错误 5:refreshToken 接口也触发 Authenticator

容易死循环。

刷新接口要跳过认证刷新逻辑,或者使用独立 OkHttpClient。


错误 6:刷新失败不清 Token

错误。

刷新失败应该清空内存 Token 和本地密文 Token,并通知登录失效。


错误 7:不限制重试次数

容易无限循环。

要用 responseCount 限制重试。


错误 8:日志打印 Authorization

严重问题。

Authorization、accessToken、refreshToken 必须脱敏。


二十二、最终落地清单

这一篇可以落成一个清单:

复制代码
1. 使用 AuthInterceptor 统一添加 Authorization Header。

2. 使用 TokenAuthenticator 统一处理 HTTP 401。

3. TokenManager 统一管理 Token,不让业务层直接读写本地存储。

4. Token 本地使用 AES-GCM + Android Keystore 加密保存。

5. App 启动时从本地密文恢复 Token 到内存。

6. Interceptor 优先从内存读取 accessToken。

7. Authenticator 里限制重试次数,避免死循环。

8. 刷新 Token 时必须加锁,避免多个接口同时刷新。

9. 刷新成功后同时更新 accessToken 和 refreshToken。

10. 刷新失败后清空内存 Token 和本地密文 Token。

11. refreshToken 接口不要再次触发 Authenticator。

12. 登录失效事件统一通知,避免多个页面重复处理。

13. OkHttp 日志和本地日志必须脱敏 Authorization / Token。

14. 后端要明确区分 accessToken 过期、refreshToken 失效、踢下线、权限不足。

二十三、总结

OkHttp 登录态闭环,不是简单地给请求加一个 Header。

完整闭环应该是:

复制代码
登录成功保存 Token
  ↓
Token 加密落地
  ↓
App 启动恢复 Token 到内存
  ↓
Interceptor 自动加 Authorization
  ↓
业务接口返回 401
  ↓
Authenticator 自动刷新 Token
  ↓
刷新成功后重试原请求
  ↓
刷新失败后清 Token 并回登录页

Interceptor 和 Authenticator 的分工可以压缩成一句话:

Interceptor 负责请求前加 Token,Authenticator 负责 401 后刷新 Token。

再完整一点:

TokenManager 管登录态,SecureTokenStore 管安全存储,AuthInterceptor 管加 Header,TokenAuthenticator 管 401 刷新,LoginStateManager 管登录失效通知。

这套结构搭好之后,移动端网络库才真正具备企业级登录态管理能力。