視頻地址
https://www.bilibili.com/video/BV1FE411M7pk?p=34
其他相關文檔
https://blog.csdn.net/a704397849/article/details/90216739
https://segmentfault.com/a/1190000041045953
雙token,帶比喻
https://www.zhihu.com/question/316165120?sort=created
另一篇
前奏
在安卓中一開始使用一個Token進行接口安全,但是Token假如過期時間設置的長,難免會有安全風險,假如設置的時間端,就會出現用戶沒用多久,就會使得用戶需要重新登錄
采用雙Token的方式,來使用戶無感知的刷新Token,實現真正的免登錄
設計
用戶在登錄之后返回access_token和refresh_token(這里假定他們的有效期分別是2小時和7天)
當access_token未過期時,則請求正常
當access_token過期了,此時服務端會返回過期提示給到客戶端,客戶端收到過期提示后,使用refresh_token去獲取新的access_token和refresh_token(此時他們的有效期就又變為2小時和7天,舊的自然失效)
當refresh_token也過期了,使用它去獲取新access_token時服務端就會返回過期提示,那么此時就應該讓用戶重新登錄了
每次使用access_token時,都會更新refresh_token的有效期
流程圖如下:
以上設計的一個好處就是只要用戶在7天內有操作,那么就可以不用重新登錄,而如果用戶在7天內沒有任何操作,那么則需要用戶重新登錄
使用長短周期token的好處就是短周期token可以防止被截獲后無休止使用,長周期token又可以保證用戶不用一直登錄,畢竟登錄用的是賬號和密碼,這種數據在網絡傳輸中越少越好。
之所以在使用refresh_token時都返回新的refresh_token,而不是延長舊的refresh_token的有效期,主要是為了安全,避免refresh_token被非法截獲后一直可用
假如每次都生成新的refresh_token,那么即使舊的refresh_token被截獲,在用戶合法刷新后就會生成新的refresh_token,導致被截獲的那個舊refresh_token失效,從而提升安全性
代碼如下,注釋很詳細:
/**
* @author zyl
* @date 2020/7/24 3:54 PM
*/
internal class TokenInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
// 添加默認的Token請求頭
request.addHeader("Authorization", TokenMmkv.accessToken.toString())
val response = chain.proceed(request.build())
if (request.build().url.toString().contains("tokenRefresh")) {
// 直接返回response,忽略攔截器,否則會無限攔截
return response
}
val mediaType = response.body?.contentType()
val content = response.body?.string()
if (isTokenExpired(content ?: "")) {
val newToken = getNewToken()
when (newToken?.code) {
0 -> {
LogUtils.d("獲取新的token和refreshToken")
TokenMmkv.accessToken = newToken.data.token
TokenMmkv.refreshToken = newToken.data.refreshToken
}
300 -> {
LogUtils.d("RefreshToken過期了,跳到登錄界面")
MMKV.defaultMMKV().remove("PersonId")
MMKV.defaultMMKV().remove("EnterpriseId")
MMKV.defaultMMKV().remove("isApprove")
// 移除之前保存的token值
TokenMmkv.removeToken()
// refreshToken 過期,跳到登錄界面
ARouter.getInstance().build("/person/main/activity").navigation()
// 這邊必須制造一個假的返回值,否值會有問題,假如直接返回response,則會報錯
val baseBean = BaseBean(RefreshToken("", ""), 300, "登錄過期,請重新登錄!")
return response.newBuilder().body(ResponseBody.create("application/json".toMediaType(), GsonUtils.toJson(baseBean))).build()
}
}
// 如果token過期 再去重新請求token 然后設置token的請求頭 重新發起請求 用戶無感
// 使用新的Token,創建新的請求
val newRequest = chain.request()
.newBuilder()
.addHeader("Authorization", TokenMmkv.accessToken.toString())
.build()
return chain.proceed(newRequest)
}
return response
.newBuilder()
.body(ResponseBody.create(mediaType, content ?: ""))
.build()
}
// 通過一個特定的接口獲取新的token,此處要用到同步的retrofit請求,這邊需要過濾掉Token攔截,防止無限循環的被攔截
private fun getNewToken(): BaseBean<RefreshToken>? {
// 通過一個特定的接口獲取新的token,此處要用到同步的retrofit請求
// 要用retrofit的同步方式
val call = Api.apiInstance.refreshToken(RequestTokenBody(TokenMmkv.refreshToken))
var newToken: BaseBean<RefreshToken>? = null
return try {
newToken = call.execute().body()
newToken
} catch (e: IOException) {
e.printStackTrace()
null
}
}
/**
* 根據Response,判斷Token
* @return
*/
private fun isTokenExpired(resultStr: String): Boolean {
val (_, code) = GsonUtils.fromJson(resultStr, BaseBean::class.java)
if (code == 300) {
LogUtils.d("Token過期了,用RefreshToken獲取新token")
return true
}
return false
}
}
