راهنما
استفاده از API قیمت در اپ اندروید
کلید مخصوص اپ اندروید، محدود به نام بسته و اثر انگشت گواهی امضا؛ با Interceptor آماده برای OkHttp و Retrofit، کش و ارسال زنده.
دو راه، بسته به حساسیت
| روش | مناسب برای | امنیت |
|---|---|---|
| اپ ← سرور شما ← API زویکس | اپهای پرمخاطب، کلید پلن گران، داده حساس | بالاترین: کلید هرگز در APK نیست؛ کلید سرور را به IP قفل کنید |
| اپ مستقیم با کلید «اپ اندروید» | اپهای ساده، ویجت صفحه اصلی، ماشینحساب طلا | خوب: کلید فقط برای اپ شما (نام بسته + گواهی امضا) کار میکند |
۱. ساخت کلید اندروید
- در داشبورد ← کلیدهای API ← کلید جدید، نوع «اپ اندروید» را انتخاب کنید.
- هر خط یک اپ:
نامبسته;SHA-256، مثلir.example.goldprice;AB:CD:…:89. فقط نام بسته (بدون گواهی) هم پذیرفته میشود ولی ضعیفتر است. - اگر اپ را با Play App Signing منتشر میکنید، SHA-256 «گواهی امضای اپ» را از Play Console (Setup ← App signing) بردارید، نه گواهی آپلود. برای نسخه debug یک خط جدا اضافه کنید.
اثر انگشت SHA-256 گواهی
# امضای debug (برای کلید آزمایشی)
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep SHA256
# امضای release خودتان
keytool -list -v -keystore release.jks -alias my-alias | grep SHA256
# با Gradle: همه امضاها
./gradlew signingReport۲. هدرهای هر درخواست
| هدر | مقدار |
|---|---|
X-API-Key | کلید اندروید شما |
X-Android-Package | نام بسته اپ (context.packageName) |
X-Android-Cert | SHA-256 گواهی امضای اپ؛ با یا بدون «:»، حروف بزرگ یا کوچک |
اگر هدرها نیایند یا با اپ ثبتشده نخوانند، پاسخ 403 app_not_allowed است.
۳. Interceptor آماده برای OkHttp
این کلاس نام بسته و گواهی را در زمان اجرا از خود اپ میخواند؛ پس نسخه دستکاریشده و دوبارهامضاشده اپ، گواهی دیگری میفرستد و رد میشود.
ZevixInterceptor.kt
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import okhttp3.Interceptor
import okhttp3.Response
import java.security.MessageDigest
/** کلید API و شناسه اپ (نام بسته و SHA-256 گواهی امضا) را به هر درخواست اضافه میکند. */
class ZevixInterceptor(context: Context, private val apiKey: String) : Interceptor {
private val pkg = context.packageName
private val cert = signingCertSha256(context)
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(
chain.request().newBuilder()
.header("X-API-Key", apiKey)
.header("X-Android-Package", pkg)
.header("X-Android-Cert", cert)
.build()
)
private fun signingCertSha256(context: Context): String {
val pm = context.packageManager
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pm.getPackageInfo(pkg, PackageManager.GET_SIGNING_CERTIFICATES)
.signingInfo!!.apkContentsSigners
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES).signatures!!
}
val digest = MessageDigest.getInstance("SHA-256").digest(signatures[0].toByteArray())
return digest.joinToString(":") { "%02X".format(it) }
}
}Retrofit
// یکبار، مثلاً در Application یا ماژول Hilt
val client = OkHttpClient.Builder()
.addInterceptor(ZevixInterceptor(appContext, BuildConfig.ZEVIX_API_KEY))
.cache(Cache(File(appContext.cacheDir, "zevix"), 1L * 1024 * 1024))
.build()
interface ZevixApi {
@GET("v1/prices")
suspend fun prices(@Query("symbols") symbols: String): PricesResponse
}
val api = Retrofit.Builder()
.baseUrl("https://data.zevix.ir/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ZevixApi::class.java)
// در ViewModel
val board = api.prices("gold_18,coin_emami,usd").data۴. ارسال زنده در اپ
برای تابلوی قیمت زنده بهجای درخواست هر چند ثانیه، یک اتصال /v1/stream باز کنید (پلنهای دارای ارسال زنده). وقتی اپ به پسزمینه رفت اتصال را ببندید و هنگام بازگشت دوباره باز کنید.
Server-Sent Events
// ارسال زنده با okhttp-sse (implementation("com.squareup.okhttp3:okhttp-sse:4.12.0"))
val request = Request.Builder().url("https://data.zevix.ir/v1/stream?symbols=gold_18,usd").build()
EventSources.createFactory(client).newEventSource(request, object : EventSourceListener() {
override fun onEvent(source: EventSource, id: String?, type: String?, data: String) {
if (type == "prices") updateBoard(JSONObject(data).getJSONArray("quotes"))
}
override fun onFailure(source: EventSource, t: Throwable?, response: okhttp3.Response?) {
// قطع شبکه: چند ثانیه بعد دوباره وصل شوید
}
})محدودیت امنیتی را بشناسید
هر کلیدی که داخل APK باشد با مهندسی معکوس قابل استخراج است و هدرها را میشود جعل کرد. محدودیت اپ جلوی استفاده سرسری از کلید را میگیرد، نه یک مهاجم مصمم. برای پلنهای گران یا اپ پرمخاطب، API را از سرور خودتان صدا بزنید و قیمت را برای کاربران اپ کش کنید.
نکتهها
- هر اپ (و هر محیط debug/release) کلید جدا داشته باشد تا مصرفش جدا دیده شود و در صورت لو رفتن فقط همان باطل شود.
- پاسخ را چند ثانیه کش کنید؛ هزاران کاربر همزمان با درخواست هر ثانیه، سقف پلن را پر میکنند.
- خطای
429را با صبر به اندازه هدرRetry-Afterمدیریت کنید و402 subscription_expiredرا به کاربر «سرویس موقتاً در دسترس نیست» نشان دهید.