You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 00:20:13