Android 接入

原生 App 属于「公开客户端」:不携带 client_secret,必须使用 PKCE(S256)。授权页用 Custom Tabs 打开,不要用 WebView。

1. 配置回调

在应用后台把回调地址登记为其中之一(必须精确一致):

  • App Link:https://your-domain.com/oauth/callback
  • 自定义 scheme:jintuziopen://oauth/callback
<activity android:name=".OAuthCallbackActivity" android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="jintuziopen" android:host="oauth" android:path="/callback" />
  </intent-filter>
</activity>

2. 生成 PKCE 并发起授权

// Kotlin
private fun randomVerifier(): String {
    val bytes = ByteArray(32).also { SecureRandom().nextBytes(it) }
    return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}

private fun challenge(verifier: String): String {
    val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray())
    return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}

fun startOAuth(context: Context) {
    val verifier = randomVerifier()
    val state = UUID.randomUUID().toString()
    // 临时保存,回调时要用
    prefs.edit().putString("pkce_verifier", verifier).putString("oauth_state", state).apply()

    val url = Uri.parse("https://open.jintuzi.com/oauth/authorize").buildUpon()
        .appendQueryParameter("response_type", "code")
        .appendQueryParameter("client_id", "你的_app_id")
        .appendQueryParameter("redirect_uri", "jintuziopen://oauth/callback")
        .appendQueryParameter("scope", "user:avatar user:nickname user:jintuzi_no")
        .appendQueryParameter("state", state)
        .appendQueryParameter("code_challenge", challenge(verifier))
        .appendQueryParameter("code_challenge_method", "S256")
        .build()

    CustomTabsIntent.Builder()
        .setShowTitle(true)
        .build()
        .launchUrl(context, url)
}

3. 处理回调并换 token

// OAuthCallbackActivity,交由你的服务端或本地 OkHttp 换取
val code = intent.data?.getQueryParameter("code")
val state = intent.data?.getQueryParameter("state")
require(state == prefs.getString("oauth_state", null)) { "state 不匹配,可能被 CSRF" }

// 公开客户端:不带 client_secret
val body = FormBody.Builder()
    .add("grant_type", "authorization_code")
    .add("client_id", "你的_app_id")
    .add("code", code!!)
    .add("redirect_uri", "jintuziopen://oauth/callback")
    .add("code_verifier", prefs.getString("pkce_verifier", null)!!)
    .build()
// POST https://open.jintuzi.com/oauth/token

4. 注意

  • 不要把 access_token 写进日志或明文存储,建议 EncryptedSharedPreferences;
  • token 过期(7200 秒)后用 refresh_token 刷新,失败再重新授权;
  • App 上架时,隐私政策需写明会收集哪些字段、用途与第三方 SDK 情况。