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 情况。