基于Android Credential Manager的无后端Jetpack应用Google登录与Drive授权方案
无后端Android Jetpack应用Google认证与Drive授权完整实现方案(Kotlin)
一、核心依赖配置
在Module级build.gradle.kts中添加以下依赖:
dependencies { // Credential Manager 核心库 implementation "androidx.credentials:credentials:1.2.2" implementation "androidx.credentials:credentials-play-services-auth:1.2.2" // Google Identity 认证库 implementation "com.google.android.gms:play-services-auth:20.7.0" // Jetpack Security 安全存储库 implementation "androidx.security:security-crypto:1.1.0-alpha06" // Google Drive API 客户端 implementation "com.google.apis:google-api-services-drive:v3-rev20240422-2.0.0" implementation "com.google.oauth-client:google-oauth-client-android:1.34.1" }
二、Credential Manager 用户认证实现
1. 初始化Credential Manager
private val credentialManager = CredentialManager.create(context)
2. 构建Google登录请求
注意:此处需使用Web类型的Client ID(在Google Cloud Console创建),而非Android Client ID
private fun buildGoogleSignInRequest(): GetCredentialRequest { val googleIdOption = GoogleIdTokenCredentialOption.Builder() .setFilterByAuthorizedAccounts(false) .setServerClientId(context.getString(R.string.google_web_client_id)) .build() return GetCredentialRequest.Builder() .addCredentialOption(googleIdOption) .build() }
3. 发起认证并处理结果
suspend fun signInWithGoogle() { try { val result = credentialManager.getCredential( context = context, request = buildGoogleSignInRequest() ) val googleCredential = result.credential as GoogleIdTokenCredential // 获取用户基础信息:ID Token、用户名、邮箱等 val userId = googleCredential.idToken val userName = googleCredential.displayName } catch (e: GetCredentialException) { when (e) { is NoCredentialException -> {} // 无可用Google账号 is UserCanceledException -> {} // 用户取消登录 else -> {} // 其他错误,如网络异常 } } }
三、Google Drive授权与Token获取(无后端场景)
1. 无后端使用authorizationCode的安全性说明
直接在客户端换取Token存在风险,可通过以下方式降低:
- 使用Web类型的Client ID,并在Google Cloud Console中限制该Client ID的授权来源为你的Android应用的SHA-1+包名组合
- 不在客户端硬编码CLIENT_SECRET:配置Android应用OAuth 2.0客户端后,搭配Web Client ID可省略SECRET完成Token换取
- Google的authorizationCode为一次性有效,使用后立即失效,避免重复利用
2. 构建Offline Access授权请求
开启Offline Access以获取refresh token
private fun buildDriveAuthorizationRequest(): AuthorizationRequest { // 选择最小必要权限Scope,例如仅授权应用创建/打开的文件 val scopes = listOf("https://www.googleapis.com/auth/drive.file") return AuthorizationRequest.Builder( context.getString(R.string.google_web_client_id), AuthorizationRequest.ResponseType.CODE, "urn:ietf:wg:oauth:2.0:oob" // 无后端场景专用回调URI ) .setScopes(scopes.toTypedArray()) .requestOfflineAccess(context.getString(R.string.google_web_client_id)) .build() }
3. 发起授权并获取authorizationCode
private const val REQUEST_CODE_DRIVE_AUTH = 1001 suspend fun requestDriveAuthorization() { val intent = GoogleAuthUtil.getAuthorizationIntent(buildDriveAuthorizationRequest()) val result = context.startActivityForResult(intent, REQUEST_CODE_DRIVE_AUTH) if (result.resultCode == Activity.RESULT_OK) { val authorizationCode = result.data?.getStringExtra("code") authorizationCode?.let { exchangeCodeForTokens(it) } } }
4. 换取Refresh Token与Access Token
private suspend fun exchangeCodeForTokens(authorizationCode: String) { val tokenRequest = TokenRequest.Builder() .setClientId(context.getString(R.string.google_web_client_id)) .setGrantType("authorization_code") .setCode(authorizationCode) .setRedirectUri("urn:ietf:wg:oauth:2.0:oob") .build() try { val tokenResponse = GoogleAuthUtil.getToken(context, tokenRequest) val accessToken = tokenResponse.accessToken val refreshToken = tokenResponse.refreshToken val expiresIn = tokenResponse.expiresInSeconds // 存储Token到安全存储 secureTokenStorage.saveTokens(accessToken, refreshToken, expiresIn) } catch (e: GoogleAuthException) { // 处理Token换取失败,如授权码无效、网络异常 } }
四、Token续期实现
当access token过期时,使用refresh token换取新的access token
suspend fun refreshAccessToken(): String? { val savedTokens = secureTokenStorage.getSavedTokens() ?: return null val refreshToken = savedTokens.refreshToken val tokenRequest = TokenRequest.Builder() .setClientId(context.getString(R.string.google_web_client_id)) .setGrantType("refresh_token") .setRefreshToken(refreshToken) .build() return try { val tokenResponse = GoogleAuthUtil.getToken(context, tokenRequest) val newAccessToken = tokenResponse.accessToken val newExpiresIn = tokenResponse.expiresInSeconds secureTokenStorage.updateAccessToken(newAccessToken, newExpiresIn) newAccessToken } catch (e: GoogleAuthException) { // Refresh Token失效,引导用户重新授权 null } }
五、敏感信息安全存储(Jetpack Security)
使用EncryptedSharedPreferences存储Token、Client ID等敏感信息:
class SecureTokenStorage(context: Context) { private val encryptedPrefs = EncryptedSharedPreferences.create( "secure_token_store", MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(), EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) // 保存Token信息 fun saveTokens(accessToken: String, refreshToken: String, expiresIn: Long) { val expiryTime = System.currentTimeMillis() + (expiresIn * 1000) encryptedPrefs.edit() .putString("access_token", accessToken) .putString("refresh_token", refreshToken) .putLong("expiry_time", expiryTime) .apply() } // 更新Access Token及过期时间 fun updateAccessToken(newAccessToken: String, newExpiresIn: Long) { val newExpiryTime = System.currentTimeMillis() + (newExpiresIn * 1000) encryptedPrefs.edit() .putString("access_token", newAccessToken) .putLong("expiry_time", newExpiryTime) .apply() } // 获取存储的Token信息 fun getSavedTokens(): SavedTokens? { val accessToken = encryptedPrefs.getString("access_token", null) val refreshToken = encryptedPrefs.getString("refresh_token", null) val expiryTime = encryptedPrefs.getLong("expiry_time", 0) return if (accessToken != null && refreshToken != null) { SavedTokens(accessToken, refreshToken, expiryTime) } else null } data class SavedTokens( val accessToken: String, val refreshToken: String, val expiryTime: Long ) }
Client ID存储建议
将Web Client ID放在res/values/strings.xml中,避免硬编码:
<string name="google_web_client_id" translatable="false">YOUR_WEB_CLIENT_ID</string>
代码中通过context.getString(R.string.google_web_client_id)获取。
六、关键注意事项
- 权限最小化:始终使用最窄的Drive Scope,避免过度授权
- Refresh Token有效期:用户长时间未使用应用或主动撤销授权时,Refresh Token会失效,需引导用户重新授权
- 兼容性处理:Credential Manager在Android 10+表现最佳,低版本需确保Play Services已更新
- 异常全覆盖:所有认证、授权、Token操作需处理网络异常、用户取消、Token失效等场景
内容的提问来源于stack exchange,提问作者nightlytrails
相关产品推荐
相关产品推荐

