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

如何在Android中使用OpenID Connect实现登录页面认证

Android应用中OpenID Connect登录认证的具体实现步骤

我之前在Android项目里折腾过OpenID Connect的登录,太懂这种查了一堆资料还是摸不着头绪的感觉!下面给你一步步拆解实战过的实现步骤,跟着走基本能搞定:

一、准备工作:获取身份提供商(IDP)的配置信息

首先你得从你的OpenID Connect身份提供商(比如Auth0、Okta,或者自己搭建的Keycloak)拿到这些核心信息:

  • Client ID:你的应用在IDP上注册的唯一标识
  • 授权端点(Authorization Endpoint):用户跳转去登录授权的URL
  • 令牌端点(Token Endpoint):用来交换获取访问令牌、ID令牌的URL
  • 重定向URI(Redirect URI):授权完成后IDP跳转回你的应用的地址,格式一般是scheme://host,比如com.yourapp.oidc://callback
  • 可选:注销端点(End Session Endpoint):用来实现登出的URL

二、添加依赖库

推荐使用AppAuth,这是Android官方推荐的用于OAuth 2.0和OpenID Connect的开源库,稳定且文档完善。在你的app/build.gradle(Module级别)里添加依赖:

dependencies {
    // AppAuth核心库
    implementation 'net.openid:appauth:0.11.1'
    // 可选:用于验证JWT(ID Token)的库
    implementation 'com.nimbusds:nimbus-jose-jwt:9.25.6'
}

三、配置AndroidManifest.xml

  1. 添加网络权限(必须,因为要和IDP通信):
<uses-permission android:name="android.permission.INTERNET" />
  1. 配置处理回调的Activity(或者直接用AppAuth的默认回调Activity):
    如果用自定义Activity,需要添加intent-filter来匹配重定向URI:
<activity android:name=".OidcCallbackActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- 这里的data要和你设置的重定向URI匹配 -->
        <data
            android:scheme="com.yourapp.oidc"
            android:host="callback" />
    </intent-filter>
</activity>

注意:如果你的IDP是第三方应用(比如Google登录),Android 11及以上需要在Manifest里添加包可见性配置,避免无法跳转:

<queries>
    <package android:name="com.google.android.gms" />
</queries>

四、构建并发起授权请求

在你的登录按钮点击事件里,构建授权请求并启动授权流程:

import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationService
import net.openid.appauth.ResponseTypeValues
import android.app.ActivityResultLauncher
import android.content.Intent
import android.widget.Toast

// 1. 初始化AuthorizationService
val authService = AuthorizationService(this)

// 2. 构建授权请求
val authRequest = AuthorizationRequest.Builder(
    // IDP的授权端点(可以用Uri.parse转换为Uri对象)
    Uri.parse("https://your-idp-domain.com/auth"),
    // 你的Client ID
    "your-client-id",
    // 响应类型,OpenID Connect必须包含openid
    ResponseTypeValues.CODE,
    // 重定向URI
    Uri.parse("com.yourapp.oidc://callback")
)
// 添加需要的权限范围,比如获取用户信息、邮箱等
.setScopes("openid", "profile", "email")
.build()

// 3. 启动授权页面(用AndroidX的registerForActivityResult更推荐)
val authLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
    androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == RESULT_OK) {
        result.data?.let { intent ->
            // 处理授权响应,见下一步
            handleAuthorizationResponse(intent, authService)
        }
    } else {
        // 用户取消了授权,处理错误逻辑
        Toast.makeText(this, "授权取消", Toast.LENGTH_SHORT).show()
    }
}

// 启动授权Intent
val authIntent = authService.getAuthorizationRequestIntent(authRequest)
authLauncher.launch(authIntent)

五、处理授权响应并获取令牌

授权完成后,在回调里处理响应,交换获取访问令牌(Access Token)和ID令牌(ID Token):

private fun handleAuthorizationResponse(intent: Intent, authService: AuthorizationService) {
    val authResponse = net.openid.appauth.AuthorizationResponse.fromIntent(intent)
    val authException = net.openid.appauth.AuthorizationException.fromIntent(intent)

    if (authResponse != null && authException == null) {
        // 构建令牌请求
        val tokenRequest = authResponse.createTokenExchangeRequest()
        // 发起令牌请求
        authService.performTokenRequest(tokenRequest) { tokenResponse, tokenException ->
            if (tokenResponse != null && tokenException == null) {
                // 成功拿到令牌!
                val accessToken = tokenResponse.accessToken
                val idToken = tokenResponse.idToken
                val refreshToken = tokenResponse.refreshToken

                // 这里必须验证ID Token的合法性!不能直接信任
                validateIdToken(idToken)

                // 持久化令牌(推荐用Android Keystore加密存储,或者加密的SharedPreferences)
                saveTokens(accessToken, idToken, refreshToken)

                // 跳转到主页面,完成登录
                startActivity(Intent(this, MainActivity::class.java))
                finish()
            } else {
                // 令牌请求失败,处理错误
                Toast.makeText(this, "登录失败:${tokenException?.message}", Toast.LENGTH_SHORT).show()
            }
        }
    } else {
        // 授权失败,处理错误
        Toast.makeText(this, "授权失败:${authException?.message}", Toast.LENGTH_SHORT).show()
    }
}

六、验证ID Token的合法性(关键!)

ID Token是JWT格式,必须验证它的签名、有效期、受众(Audience)等,防止伪造。用nimbus-jose-jwt库的示例:

import com.nimbusds.jwt.JWT
import com.nimbusds.jwt.JWTParser
import com.nimbusds.jwt.SignedJWT
import com.nimbusds.jose.JWSVerifier
import com.nimbusds.jose.crypto.RSASSAVerifier
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext
import java.util.Date

private fun validateIdToken(idToken: String?) {
    idToken ?: throw IllegalArgumentException("ID Token不能为空")
    try {
        val jwt = JWTParser.parse(idToken) as SignedJWT
        // 从IDP的JWKS端点获取公钥(需要提前配置或动态获取)
        val jwkSource: JWKSource<SecurityContext> = // 这里实现从IDP获取公钥的逻辑
        val verifier: JWSVerifier = RSASSAVerifier(jwkSource.getJWK(jwt.header.keyID, null))
        
        // 验证签名
        if (!jwt.verify(verifier)) {
            throw SecurityException("ID Token签名验证失败")
        }
        
        // 验证有效期
        val claims = jwt.jwtClaimsSet
        val now = Date().time / 1000
        if (claims.expirationTime.time / 1000 < now) {
            throw SecurityException("ID Token已过期")
        }
        
        // 验证受众(必须包含你的Client ID)
        if (!claims.audience.contains("your-client-id")) {
            throw SecurityException("ID Token受众不匹配")
        }
        
        // 验证 issuer(必须和IDP的域名一致)
        if (claims.issuer != "https://your-idp-domain.com") {
            throw SecurityException("ID Token发行方不匹配")
        }
        
        // 验证通过,安全使用用户信息
        val userId = claims.subject
        val userName = claims.getStringClaim("name")
        val userEmail = claims.getStringClaim("email")
    } catch (e: Exception) {
        throw SecurityException("ID Token验证失败:${e.message}")
    }
}

七、令牌持久化与刷新

  • 持久化:不要明文存储令牌,推荐用Android Keystore生成密钥来加密令牌,或者使用Jetpack Security库来加密SharedPreferences。
  • 刷新令牌:当Access Token过期时,用Refresh Token去Token端点获取新的Access Token,避免用户重复登录:
import net.openid.appauth.TokenRequest
import net.openid.appauth.GrantTypeValues

private fun refreshToken(refreshToken: String, authService: AuthorizationService) {
    val tokenRequest = TokenRequest.Builder(
        Uri.parse("https://your-idp-domain.com/token"),
        "your-client-id"
    )
    .setGrantType(GrantTypeValues.REFRESH_TOKEN)
    .setRefreshToken(refreshToken)
    .build()
    
    authService.performTokenRequest(tokenRequest) { tokenResponse, tokenException ->
        if (tokenResponse != null) {
            // 更新存储的Access Token和Refresh Token(如果返回了新的Refresh Token)
            saveTokens(tokenResponse.accessToken, tokenResponse.idToken, tokenResponse.refreshToken ?: refreshToken)
        }
    }
}

八、注销登录

  • 本地注销:清除存储的所有令牌,跳转到登录页面。
  • IDP注销:如果需要单点注销,跳转到IDP的注销端点,带上ID Token和重定向URI:
fun logout() {
    val idToken = getSavedIdToken()
    val logoutUri = Uri.parse("https://your-idp-domain.com/logout")
        .buildUpon()
        .appendQueryParameter("id_token_hint", idToken)
        .appendQueryParameter("post_logout_redirect_uri", "com.yourapp.oidc://logout-callback")
        .build()
    
    // 用浏览器打开注销页面
    val intent = Intent(Intent.ACTION_VIEW, logoutUri)
    startActivity(intent)
    
    // 清除本地令牌
    clearSavedTokens()
    startActivity(Intent(this, LoginActivity::class.java))
    finish()
}

内容的提问来源于stack exchange,提问作者dam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 09:02:31