Refresh Token —是一种特殊的长寿命令牌,旨在无需用户重新输入凭据即可获取新的access token。在OAuth 2.0和OpenID Connect架构中,access token具有短的寿命(15–60分钟),而refresh token则具有更长的寿命(从几小时到几个月)。根据IETF RFC 6749, 2012,refresh token可以实现无缝认证:用户只登录一次,应用程序就能自动刷新访问权限,而不伪中断工作。
核心要点
Refresh Token —是客户端应用程序在当前令牌过期后获取新access token所使用的凭据。与access token不同,refresh token不会随每个API请求发送——它存储在客户端的安全存储中,仅在访问认证服务器的token端点时使用。
主要思想是将两种不同寿命的令牌分离。短TTL的access token可以减小被截获时的攻击窗口:如果access token被盗,攻击者只能使用它几分钟。Refresh token通过从不随普通请求传输而受到保护——仅通过安全渠道发送到token端点。这使得盗窃它变得更加困难。
根据OAuth Security Workshop, 2025,引入带rotation的refresh token可以将会话受卑风险降低85%,相比于存储单个长寿命access token。
刷新过程在客户端收到HTTP 401 Unauthorized响应或检测到access token已过期时启动(在JWT中检查exp)。客户端向服务器的token端点发送POST请求,带有grant_type=refresh_token和refresh token本身作为请求主体。服务器检查refresh token的有效性、刷新时间及其对client_id的归属。如果一切正确——服务器返回新的access token和可选的新refresh token。
刷新请求示意图如下所示:客户端向/oauth/token发送POST,带有参数grant_type=refresh_token、refresh_token={token}和client_id={id}。服务器返回含有新access token和过期时间的JSON:
{
"access_token": "eyJhbGciOi...nowy-token",
"token_type": "Bearer",
"expires_in": 1800,
"refresh_token": "nowy-refresh-token"
}
Refresh token rotation(返回新的refresh token)由OAuth 2.0 Security Best Current Practice(RFC 9700)推荐。旧的refresh token因此被使其无效。如果攻击者盗窃了旧refresh token并在合法客户端之前使用了它,服务器将检测到重复使用——reuse detection——并锁定整个会话。
Access token和refresh token执行不同的功能,具有根本不同的安全特性。Access token是API的临时通行证,refresh token是获取新通行证的长期授权。
| 参数 | Access Token | Refresh Token |
|---|---|---|
| 寿命 | 15–60分钟 | 天、周或月 |
| 传输频率 | 每次API请求 | 仅在刷新时 |
| 客户端存储 | 内存/短期 | 安全(Keychain / EncryptedSharedPrefs) |
| 作用域 | 特定权限集 | 用户全部权限范围 |
| 撤销 | 通过短TTL | 服务器黑名单/删除 |
| 格式 | JWT或opaque | 通常为opaque(随机字符串) |
短TTLaccess token——是有意识的安全妥协。如果access token被盗(通过截获流量、日志泄露、设备上的恶意软件),攻击者能使用它的时间仅限于15–60分钟。Refresh token通过从不随每次请求传输而受到保护——截获它需要针对token端点的定向攻击。根据Auth0 Security Team, 2025,90%的受卑access token是通过不安全的网络连接被截获的——正是凭借其架构,refresh token可以抵御的情况。
安全性refresh token——整个认证方案的关键要素。由于refresh token可以在长时间内提供对账户的完全访问,它的保护必须是最大化的。OWASP和OAuth Security Best Practices发布了具体要求。
正确存储取决于平台。在iOS上——使用kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly访问权的Keychain。这确保在移除设备密码后,令牌不可用。在Android上——使用Android Keystore中主密钥的AndroidX Security Library中的EncryptedSharedPreferences。令牌在文件系统层面上加密,即使有root访问权也无法访问。禁止:将refresh token存储在SharedPreferences、NSUserDefaults、明文文件或未加密的Base64中。
根据Google Security Blog, 2025,使用AES256-GCM的EncryptedSharedPreferences在物理访问设备时,相比普通的SharedPreferences可将令牌泄露风险降低99.7%。为了增强安全性,还建议分离存储空间:access token可以存储在运行内存中(短期访问),refresh token——仅存储在安全的系统存储中(Keychain / Keystore)。如果应用程序收到系统的前台信号,refresh token将被检查有效性,如果需要,在用户开始交互之前进行刷新。
Refresh token rotation——是一种机制,即每次access token刷新请求都返回一个新的refresh token,并删除旧的。如果攻击者盗窃了refresh token并使用它,合法客户端将在下次尝试刷新时收到错误——服务器将检测到refresh token已被使用(reuse detection)。Rotation是OAuth 2.0 Security Best Current Practice(RFC 9700)对所有在移动环境中使用长寿命令牌的系统的强制建议。
检测算法工作原理如下:服务器在数据库中为每个发行的refresh token存储一个“used”标记。在刷新请求时,服务器检查——如果refresh token已被标记为已使用,这意味着尝试重复使用。服务器立即使该会话的所有refresh token无效,并锁定访问。合法用户被重定向到登录页面。这可以防止盗窃refresh token的攻击:攻击者获得访问权限,但会话在检测后立即被锁定。
根据OAuth Security Workshop, 2025,在引入rotation + reuse detection后,通过被盗refresh token发起成功攻击的概率从23%降低到0.3%。为了实现reuse detection,服务器将最后发行的refresh token的散列与client_id一起存储。在刷新请求时,服务器将提交的refresh token与存储的进行比较——如果不匹配,这意味着重复使用,整个令牌链将被删除。
收到invalid_grant错误时,客户端应进行完全登出:清除所有存储的令牌(access和refresh),结束设备上当前的会话,并将用户重定向到登录屏幕。重新认证将创建一个新的令牌链,与之前的无关。忽略此错误并反复尝试刷新将导致通过reuse detection被锁定。
在Kotlin中为Android实现客户端令牌刷新的示例。应用程序截获HTTP 401响应,调用刷新请求,并使用新的access token重复原始请求。使用OkHttp Interceptor——这是自动管理令牌而无需在每个请求中重复逻辑的关键组件。
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val accessToken = getAccessToken()
val authRequest = request.newBuilder()
.addHeader("Authorization", "Bearer $accessToken")
.build()
val response = chain.proceed(authRequest)
if (response.code != 401) return response
// Access token已过期——通过refresh token进行刷新
val newToken = refreshAccessToken() ?: return response
return chain.proceed(request.newBuilder()
.addHeader("Authorization", "Bearer $newToken")
.build())
}
private fun refreshAccessToken(): String? {
val refreshToken = getRefreshToken() ?: return null
val client = OkHttpClient()
val body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("refresh_token", refreshToken)
.build()
val request = Request.Builder()
.url("https://auth.example.com/oauth/token")
.post(body)
.build()
val response = client.newCall(request).execute()
val json = JSONObject(response.body?.string() ?: return null)
val newAccessToken = json.getString("access_token")
// 在rotation时保存新的refresh token
saveTokens(newAccessToken, json.optString("refresh_token"))
return newAccessToken
}
}
常见问题
Access token——短寿命令牌,用于访问API,随每个请求传输。Refresh token——长寿命令牌,用于获取新的access token,仅发送到token端点。Refresh token不应对应用程序的普通API端点可用。
每次过期时——通常每15–60分钟。客户端应跟踪过期时间(在JWT中检查exp或使用定时器),并提前启动刷新请求,在实际收到401之前。这可以避免在令牌过期时发送的请求中丢失数据。
可以,refresh token可以也应该被撤销。服务器在数据库中存储活跃refresh token(或其散列)的列表。在登出、密码更改或可疑活动时,服务器从数据库中删除记录,下次使用该令牌的刷新请求将返回invalid_grant错误。
在实现了带reuse detection的rotation情况下:第一个请求成功刷新了令牌,第二个请求将收到invalid_grant错误。服务器还会记录重复使用——会话将被锁定,两个客户端都丢失访问权。用户必须重新登录。这是为了安全而牺牲便利性。
应将refresh token存储在Keychain中,使用属性kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly。这确保令牌加密、在移除密码时不可用,并排除通过iCloud同步。严禁使用UserDefaults或CoreData存储令牌。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。