Android下如何获取可自动刷新AccessToken的Google Drive API OAuth2凭证?
如何获取一个可在AccessToken过期后自动刷新的OAuth2凭证对象,以访问Google Drive API?
已完成操作
我遵循Google的最新建议,在Android上通过Credential Manager完成身份验证,再通过Google Identity授权来访问Google Drive API。当前身份验证代码如下:
private fun authorizeGoogleDrive() { val requestedScopes = listOf(Scope(DriveScopes.DRIVE_READONLY)) val authorizationRequest = AuthorizationRequest.Builder() .setRequestedScopes(requestedScopes) .build() Identity.getAuthorizationClient(this) .authorize(authorizationRequest) .addOnSuccessListener { authorizationResult -> if (authorizationResult.hasResolution()) { val pendingIntent = authorizationResult.pendingIntent try { val intentSenderRequest = pendingIntent?.let { IntentSenderRequest.Builder(it.intentSender).build() } intentSenderRequest?.let { authorizeGoogleDriveResultReceiver.launch(intentSenderRequest) } } catch (e: IntentSender.SendIntentException) { Log.e(LOG, "SignInActivity/authorizeGoogleDrive Couldn't start Authorization UI: " + e.localizedMessage) } } else { buildDriveService(authorizationResult) } } } private val authorizeGoogleDriveResultReceiver = registerForActivityResult( ActivityResultContracts.StartIntentSenderForResult() ) { result -> if (result.resultCode == Activity.RESULT_OK) { val authorizationResult: AuthorizationResult = Identity.getAuthorizationClient( this ).getAuthorizationResultFromIntent(result.data) buildDriveService(authorizationResult) } else { Log.e(LOG, "SignInActivity/authorizeGoogleDriveResultReceiver result.resultCode != Activity.RESULT_OK") } } fun buildDriveService(authorizationResult: AuthorizationResult) { val accessToken = AccessToken(authorizationResult.accessToken, Date(System.currentTimeMillis() + 3600 * 1000)) val credential = GoogleCredentials.create(accessToken) driveService = Drive.Builder( GoogleNetHttpTransport.newTrustedTransport(), GsonFactory.getDefaultInstance(), HttpCredentialsAdapter(credential) ) .setApplicationName(app_name) .build() }
访问起初正常(持续约60分钟),但之后使用driveService对象时出现以下异常:
OAuth2Credentials instance does not support refreshing the access token. An instance with a new access token should be used, or a derived type that supports refreshing.
更奇怪的是:重启应用后,即使再次调用Identity.getAuthorizationClient(this).authorize(authorizationRequest),我始终获取到同一个accessToken。
根据com.google.auth.oauth2.GoogleCredentials(继承自OAuth2Credentials)的文档,该类不支持刷新accessToken。我尝试将GoogleCredentials替换为com.google.auth.oauth2.UserCredentials,希望该类支持自动刷新token,但使用其Builder时始终出现空指针异常,且该Builder需要传入refreshToken:
val userCredentials = UserCredentials.newBuilder() .setClientId(app_name) .setAccessToken(accessToken) .setRefreshToken(refreshToken) .build()
但我没有refreshToken,通过Identity.getAuthorizationClient(this).authorize(authorizationRequest)获取的authorizationResult对象中仅包含accessToken字段,没有refreshToken。
解决方案
1. 开启离线访问权限以获取RefreshToken
Google Identity的授权请求需要明确开启离线访问,才能拿到refreshToken。修改authorizeGoogleDrive方法中的请求构建逻辑:
val authorizationRequest = AuthorizationRequest.Builder() .setRequestedScopes(requestedScopes) .setAuthorizationType(AuthorizationRequest.AuthorizationType.CODE) // 切换为授权码模式,支持离线访问 .setOfflineAccess(true) // 开启离线访问,触发refreshToken返回 .build()
2. 用授权码交换包含RefreshToken的完整Token
切换到授权码模式后,authorizationResult不会直接返回accessToken,需要用授权码去交换完整的token信息(包含refreshToken)。修改授权成功后的处理逻辑:
Identity.getAuthorizationClient(this) .authorize(authorizationRequest) .addOnSuccessListener { authorizationResult -> if (authorizationResult.hasResolution()) { // 原有跳转授权UI的逻辑保持不变 } else { // 提取授权码并发起token交换请求 authorizationResult.authorizationCode?.let { code -> exchangeCodeForTokens(code) } ?: run { Log.e(LOG, "未获取到授权码") } } }
添加交换授权码的方法:
private fun exchangeCodeForTokens(code: String) { val tokenRequest = TokenRequest.newBuilder() .setClientId(YOUR_CLIENT_ID) // 替换为你的OAuth2客户端ID .setClientSecret(YOUR_CLIENT_SECRET) // 替换为你的OAuth2客户端密钥 .setCode(code) .setGrantType("authorization_code") .setRedirectUri("urn:ietf:wg:oauth:2.0:oob") // Android应用默认回调URI .build() Identity.getAuthorizationClient(this) .getToken(tokenRequest) .addOnSuccessListener { tokenResponse -> val accessToken = AccessToken(tokenResponse.accessToken, Date(System.currentTimeMillis() + tokenResponse.expiresIn * 1000)) val refreshToken = tokenResponse.refreshToken // 这里就能拿到refreshToken了 // 构建支持自动刷新的UserCredentials val userCredentials = UserCredentials.newBuilder() .setClientId(YOUR_CLIENT_ID) .setClientSecret(YOUR_CLIENT_SECRET) .setAccessToken(accessToken) .setRefreshToken(refreshToken) .build() // 构建带自动刷新能力的Drive服务 buildDriveServiceWithAutoRefresh(userCredentials) } .addOnFailureListener { e -> Log.e(LOG, "交换授权码失败: ${e.localizedMessage}") } } private fun buildDriveServiceWithAutoRefresh(credentials: UserCredentials) { driveService = Drive.Builder( GoogleNetHttpTransport.newTrustedTransport(), GsonFactory.getDefaultInstance(), HttpCredentialsAdapter(credentials) ) .setApplicationName(app_name) .build() }
3. 处理客户端密钥的安全风险
注意:在Android应用中直接硬编码客户端密钥存在泄露风险,建议通过后端服务代理token交换请求——前端将授权码发送到后端,由后端完成token交换后返回accessToken和refreshToken,避免客户端密钥暴露。
4. 解决重复获取相同AccessToken的问题
重启应用后调用authorize仍拿到旧token,是因为Credential Manager默认缓存有效凭证。如果需要强制刷新,可在构建请求时添加:
val authorizationRequest = AuthorizationRequest.Builder() // ...其他配置 .setForceRefresh(true) // 强制刷新缓存的凭证 .build()
5. 验证自动刷新逻辑
UserCredentials会在accessToken过期时自动用refreshToken获取新token,无需手动处理。可以模拟过期token测试:
// 模拟已过期的accessToken val expiredToken = AccessToken("expired_token", Date(System.currentTimeMillis() - 3600 * 1000)) val userCredentials = UserCredentials.newBuilder() .setClientId(YOUR_CLIENT_ID) .setClientSecret(YOUR_CLIENT_SECRET) .setAccessToken(expiredToken) .setRefreshToken(validRefreshToken) // 有效的refreshToken .build() // 调用Drive API时,会自动触发token刷新
内容的提问来源于stack exchange,提问作者spiderbaby

