双Token验证机制

855 阅读1分钟

原理: 客户端会持有两个Token令牌:accessToken和refreshToken,accessToken用于接口访问控制,每次访问接口都会携带验证,并定期刷新;refreshToken用于登录超时控制,同时用于accessToken超时验证,当accessToken刷新时,携带refreshToken校验其合法性;同样refreshToken也有时效性,超时需要重新登录。双Token机制避免了频繁登录验证,提高用户体验。
难点: 高并发情况下的token同步刷新问题。
以retrofit为例,通过添加拦截器实现双Token验证功能。

/**
 * Token拦截器
 */
class TokenInterceptor: Interceptor {
    private val tagHeader = "TokenInterceptor-"
    private val refreshTokenUrlPathHeader = "${RemoteClient.devUrl}/refreshToken"
    private fun accessToken() =
        SharedPreferenceUtil.findSharedPreference(SharedPreferenceUtil.KEY_ACCESS_TOKEN, "")

    override fun intercept(chain: Chain): Response {
        val tag = Constants.LOG_NET_HEADER.plus(tagHeader).plus("intercept:")
        val curAccessToken = accessToken()
        chain.proceed(newRequest(chain, curAccessToken))
            .apply {
                if (!isSuccessful) {
                    body()?.string()?.let {
                        Gson().fromJson(it, TokenTimeoutModel::class.java)
                            .let { tokenResult ->
                                if (tokenResult.code == CODE_201) {
                                    //token超时
                                    Log.e(tag, "accessToken timeout")
                                    //newAccessToken双重校验锁
                                    var newAccessToken = accessToken()
                                    if (newAccessToken == curAccessToken) {
                                        synchronized(this@TokenInterceptor) {
                                            newAccessToken = accessToken()
                                            if (newAccessToken != curAccessToken) {
                                                //accessToken已经刷新,续接请求
                                                Log.d(tag, "accessToken已经刷新,续接请求")
                                                this.close()
                                                return chain.proceed(
                                                    newRequest(
                                                        chain,
                                                        newAccessToken
                                                    )
                                                )
                                            } else {
                                                //刷新accessToken
                                                val userName: String =
                                                    SharedPreferenceUtil.findSharedPreference(
                                                        SharedPreferenceUtil.KEY_ACCOUNT_NAME,
                                                        ""
                                                    )
                                                val refreshToken: String =
                                                    SharedPreferenceUtil.findSharedPreference(
                                                        SharedPreferenceUtil.KEY_REFRESH_TOKEN,
                                                        ""
                                                    )
                                                userName.let {
                                                    Log.d(tag, "userName: $userName")
                                                    if (userName.isNotEmpty()) {
                                                        refreshToken.let { refreshToken ->
                                                            if (refreshToken.isNotEmpty()) {
                                                                Log.e(tag, "refreshToken start")
                                                                this.close()
                                                                chain.request()
                                                                    .newBuilder()
                                                                    .method("GET", null)
                                                                    .url("$refreshTokenUrlPathHeader/${userName}/${refreshToken}")
                                                                    .build()
                                                                    .let { newRequest ->
                                                                        chain.proceed(newRequest)
                                                                            .let { refreshTokenResponse ->
                                                                                if (refreshTokenResponse.code() == 200) {
                                                                                    Gson().fromJson(
                                                                                        refreshTokenResponse.body()
                                                                                            ?.string(),
                                                                                        TokenModel::class.java
                                                                                    )
                                                                                        .let { tokenModel ->
                                                                                            if (tokenModel.code == CODE_201) {
                                                                                                //跳转登录页面,重新登录
                                                                                                Log.e(
                                                                                                    tag,
                                                                                                    "refreshToken过期,重新登录"
                                                                                                )
                                                                                                throw RefreshTokenTimeoutException()
                                                                                            } else {
                                                                                                Log.e(
                                                                                                    tag,
                                                                                                    "使用新的AccessToken访问接口: ${tokenModel.result.accessToken}"
                                                                                                )
                                                                                                //使用新的accessToken访问接口,本地缓存新的token
                                                                                                localCache(
                                                                                                    tokenModel.result.accessToken
                                                                                                )
                                                                                                return chain.proceed(
                                                                                                    newRequest(
                                                                                                        chain,
                                                                                                        tokenModel.result.accessToken
                                                                                                    )
                                                                                                )
                                                                                            }
                                                                                        }
                                                                                } else {
                                                                                    return refreshTokenResponse
                                                                                }
                                                                            }
                                                                    }
                                                            } else {
                                                                //跳转登录页面,重新登录
                                                                Log.e(tag, "refreshToken没存储,重新登录")
                                                                throw RefreshTokenTimeoutException()
                                                            }
                                                        }
                                                    } else {
                                                        //跳转登录页面,重新登录
                                                        Log.e(tag, "userName没存储,重新登录")
                                                        //用户名没存储,重新登录
                                                        throw RefreshTokenTimeoutException()
                                                    }
                                                }
                                            }
                                        }
                                    } else {
                                        //accessToken已经刷新,续接请求
                                        Log.d(tag, "accessToken已经刷新,续接请求")
                                        this.close()
                                        return chain.proceed(newRequest(chain, newAccessToken))
                                    }
                                } else {
                                    //token没有超时
                                    return this.newBuilder()
                                        .body(ResponseBody.create(body()?.contentType(), it))
                                        .build()
                                }
                            }
                    } ?: return this
                } else {
                    return this
                }
            }
    }
}