WebView SDK
Безопасная интеграция платформы «Сосед Соседу» в мобильное приложение управляющей компании
Архитектура безопасности: Bearer-токены никогда не передаются в URL. Все данные между нативным приложением и WebView шифруются AES-256-GCM. Одноразовые launch-токены TTL 10 секунд.
SDK позволяет встроить полную функциональность платформы в ваше существующее приложение УК:
- Биржа соседских услуг с аукционами и эскроу
- Чаты подъезда / дома / ЖК (FAFM chat-core)
- Верификация жильцов и профили
- Push-уведомления через FCM/APNs
- Deep links на конкретные запросы и чаты
Требования
| Параметр | Значение |
|---|---|
| Android | API 24+ (Android 7.0), WebView 80+ |
| iOS | iOS 13+, WKWebView |
| Хранилище токенов | Android Keystore / iOS Keychain (обязательно) |
| Certificate Pinning | OkHttp / TrustKit |
| App Secret | Выдаётся при подключении ЖК в ЛК УК |
| TLS | 1.2+ (1.3 рекомендуется) |
Быстрый старт
Полная интеграция состоит из 4 шагов:
- Зарегистрировать устройство — получить
access_token,refresh_token,session_key - Получить launch_token — одноразовый токен для открытия WebView (TTL 10 сек)
- Открыть WebView — передать
?lt=..., получить HttpOnly-куки от сервера - Настроить зашифрованный канал — передать
session_keyчерезpostMessage
Регистрация устройства
Вызывается один раз при первом запуске или после полного выхода пользователя.
Запрос
POST /api/sdk/register
Аутентификация: X-App-Signature
// Headers
X-App-Signature: "HMAC-SHA256(app_secret, device_fingerprint + timestamp)"
X-Device-Id: "<device_fingerprint>"
X-App-Version: "2.1.0"
Content-Type: "application/json"
// Body
{
"user_id": "uuid-из-авторизации-ук",
"device_fingerprint": "ANDROID_ID_or_IDFV",
"platform": "android" // или "ios"
}
Ответ 200
Успешная регистрация
{
"access_token": "eyJhbGci...", // JWT, TTL 15 мин
"refresh_token": "rt_abc123...", // opaque, TTL 30 дней
"session_key": "base64(AES-256)", // ключ сеанса
"expires_in": 900,
"token_type": "bearer"
}
Как вычислить X-App-Signature
Kotlin (Android)
fun computeSignature(appSecret: String, deviceFp: String, timestamp: Long): String {
val message = "$deviceFp$timestamp"
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(appSecret.toByteArray(), "HmacSHA256"))
return Base64.encodeToString(mac.doFinal(message.toByteArray()), Base64.NO_WRAP)
}
Rate limit: 1 регистрация / устройство / 5 минут. При превышении — 429 Too Many Requests.
Открытие WebView
Шаг 1 — получить launch_token
POST /api/sdk/launch
// Headers
Authorization: "Bearer <access_token>"
// Body
{
"target": "request/123", // или "chat/abc", "/"
"device_fingerprint": "ANDROID_ID_or_IDFV"
}
// Ответ
{
"launch_token": "lt_one_time_abc...",
"ttl": 10 // секунд
}
Шаг 2 — открыть WebView
Kotlin (Android)
// НЕ передавать access_token в URL!
val url = "https://sosed-sosedu.ru/sdk/launch?lt=${launchToken}"
webView.loadUrl(url)
// Обязательные настройки WebView
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = false
allowContentAccess = false
}
// Кастомный User-Agent с package name
webView.settings.userAgentString =
"SosedSoseduSDK/1.0 (com.yourcompany.app; android)"
Сервер установит HttpOnly + Secure + SameSite=Strict куки автоматически. Вам не нужно управлять сессией вручную.
launch_token удаляется после первого использования.Что проверяет сервер при GET /sdk/launch
- Валидность
launch_token(одноразовый, TTL 10 сек) - Совпадение
device_fingerprintс зарегистрированным - Заголовок
User-Agentсодержит SDK-идентификатор - Заголовок
X-Requested-With= package name вашего приложения
Обновление токенов
POST /api/sdk/refresh
// Headers
X-Device-Id: "<device_fingerprint>"
// Body
{
"refresh_token": "rt_старый..."
}
// Ответ 200
{
"access_token": "eyJ новый...",
"refresh_token": "rt_новый...", // старый стал недействителен
"expires_in": 900
}
// 401 — refresh истёк, требуется повторный логин в приложении УК
Правила обновления:
- Превентивно — каждые 10 минут (если приложение активно)
- Реактивно — при получении 401 от любого запроса из WebView
- Мьютекс — только один параллельный refresh-запрос
- Ротация — каждый refresh выдаёт новый
refresh_token
Swift (iOS) — Token Manager
actor TokenManager {
private var isRefreshing = false
private var waiters: [CheckedContinuation<String, Error>] = []
func getValidToken() async throws -> String {
if let token = Keychain.read("access_token"), !isExpired(token) {
return token
}
if isRefreshing {
return try await withCheckedThrowingContinuation { waiters.append($0) }
}
isRefreshing = true
defer { isRefreshing = false }
let newToken = try await performRefresh()
waiters.forEach { $0.resume(returning: newToken) }
waiters.removeAll()
return newToken
}
}
Зашифрованный канал (Encrypted Bridge)
Все данные между нативным приложением и WebView шифруются AES-256-GCM.
Инициализация
- WebView загружается и сигнализирует о готовности:
postMessage({type:"ready"}) - Нативное приложение передаёт
session_keyчерезpostMessage(не через URL!) - WebView сохраняет ключ в
sessionStorage(очищается при закрытии) - Все дальнейшие сообщения шифруются
Формат сообщений
JavaScript (WebView → Native)
// Отправка зашифрованного события
async function sendEvent(payload) {
const { iv, ciphertext, tag } = await encryptAESGCM(payload, sessionKey);
window.ReactNativeWebView?.postMessage(JSON.stringify({
v: 1,
nonce: crypto.randomUUID(), // защита от replay
ts: Date.now(),
iv, data: ciphertext, tag
}));
}
// Примеры событий из WebView
sendEvent({ type: "new_bid", request_id: "abc", amount: 500 });
sendEvent({ type: "deal_done", request_id: "abc" });
sendEvent({ type: "new_message", chat_id: "xyz", count: 3 });
sendEvent({ type: "error", code: "SESSION_EXPIRED" });
Kotlin (Native → WebView)
// Отправка команды в WebView
suspend fun sendCommand(webView: WebView, command: Map<String, Any>) {
val encrypted = AesGcm.encrypt(command.toJson(), sessionKey)
val payload = mapOf(
"v" to 1,
"nonce" to UUID.randomUUID().toString(),
"ts" to System.currentTimeMillis(),
"iv" to encrypted.iv,
"data" to encrypted.ciphertext,
"tag" to encrypted.tag
)
withContext(Dispatchers.Main) {
webView.evaluateJavascript(
"window.dispatchEvent(new MessageEvent('message',{data:'${payload.toJson()}'}))",
null
)
}
}
// Команды в WebView
sendCommand(webView, mapOf("type" to "navigate", "target" to "chat/xyz"))
sendCommand(webView, mapOf("type" to "refresh_token", "new_token" to newJwt))
Защита от replay-атак
Каждое сообщение содержит nonce (UUID) и ts (timestamp). Сервер отклоняет:
- Повторные
nonce(хранится в памяти 60 секунд) - Сообщения с
tsстарше 30 секунд
Матрица угроз
| Угроза | Защита |
|---|---|
| Перехват URL с токеном | launch_token — одноразовый, TTL 10 сек, не содержит access_token |
| Копирование URL в браузер | Проверка User-Agent, X-Requested-With, device_fingerprint |
| Перехват postMessage | AES-256-GCM шифрование с session_key |
| Повторное использование refresh_token | Ротация при каждом refresh, старый недействителен немедленно |
| Извлечение токенов с устройства | Android Keystore / iOS Keychain (аппаратное хранилище) |
| Подмена нативного приложения | HMAC-SHA256 с app_secret (обфусцирован в коде) |
| MITM-атака | TLS 1.3 + Certificate Pinning |
| XSS в WebView | CSP-заголовки, X-Frame-Options: SAMEORIGIN, sanitize всех входящих данных |
| Утечка access_token в логи | Токен никогда не в URL, только в Authorization header + HttpOnly куки |
| Replay-атака | nonce + ts в каждом сообщении, TTL 30 секунд |
Certificate Pinning
Android — OkHttp
val pinner = CertificatePinner.Builder()
.add("sosed-sosedu.ru", "sha256/ХЕश_СЕРТИФИКАТА_BASE64=")
.add("sosed-sosedu.ru", "sha256/BACKUP_PIN_BASE64=") // резервный пин
.build()
val client = OkHttpClient.Builder()
.certificatePinner(pinner)
.build()
iOS — TrustKit
let trustKitConfig: [String: Any] = [
kTSKSwizzleNetworkDelegates: false,
kTSKPinnedDomains: [
"sosed-sosedu.ru": [
kTSKPublicKeyHashes: [
"ХЕШ_СЕРТИФИКАТА_BASE64=",
"BACKUP_PIN_BASE64="
]
]
]
]
TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
Ротация сертификатов: Обновляйте пины через обновление приложения заблаговременно (за 30 дней до истечения). Мы уведомим вас за 60 дней до смены сертификата.
Deep Links
| URI | Открывает |
|---|---|
sosed://request/{id} | Карточку запроса на услугу |
sosed://chat/{group_id} | Чат подъезда / дома / ЖК |
sosed://profile/{user_id} | Профиль соседа |
sosed://create | Создание нового запроса |
sosed://verification | Страницу верификации |
sosed://karma/{user_id} | Профиль кармы |
Обработка deep link в нативном приложении
// При перехвате sosed:// URI
fun handleDeepLink(uri: Uri) {
val target = uri.path?.removePrefix("/") ?: "/"
val launchToken = apiClient.getLaunchToken(target)
openWebView("https://sosed-sosedu.ru/sdk/launch?lt=${launchToken}")
}
Push-уведомления
Push-payload всегда зашифрован. Расшифровка происходит в нативном приложении.
Структура зашифрованного push-payload
{
"data": {
"v": "1",
"blob": "AES-256-GCM-ciphertext-base64",
"iv": "base64",
"tag": "base64",
"url": "request/123" // target для deep link
}
}
// После расшифровки blob:
{
"type": "new_bid",
"title": "Новый отклик на ваш запрос",
"body": "Иван предложил 400₽",
"request_id": "123"
}
Типы push-событий
| type | Когда | Приоритет |
|---|---|---|
sos | SOS от соседа (дозор, экстренная ситуация) | Максимальный |
uk_broadcast | Официальное от УК | Высокий |
new_bid | Отклик на аукцион | Средний |
deal_status | Изменение статуса сделки | Средний |
new_message | Новое сообщение в чате | Обычный |
verification | Решение по верификации | Высокий |
События WebView
WebView отправляет события в нативное приложение через зашифрованный postMessage.
| type | Данные | Описание |
|---|---|---|
ready | — | WebView готов принять session_key |
new_message | chat_id, count | Непрочитанные сообщения |
new_bid | request_id, amount, provider | Новый отклик на аукцион |
deal_done | request_id, escrow_id | Сделка завершена |
navigate_back | — | Нажата кнопка «Назад» в WebView |
session_expired | — | Сессия устарела, нужен новый launch_token |
error | code, message | Ошибка в WebView |
API Reference
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/sdk/register | Регистрация устройства |
| POST | /api/sdk/launch | Получить launch_token |
| GET | /sdk/launch?lt=… | Открыть WebView (устанавливает куки) |
| POST | /api/sdk/refresh | Обновить access_token |
| POST | /api/sdk/logout | Инвалидировать все токены устройства |
Коды ошибок
| HTTP | Код | Описание |
|---|---|---|
| 401 | TOKEN_EXPIRED | access_token истёк — выполните refresh |
| 401 | REFRESH_EXPIRED | refresh_token истёк — требуется повторный логин |
| 401 | INVALID_SIGNATURE | Неверный X-App-Signature |
| 403 | DEVICE_MISMATCH | device_fingerprint не совпадает |
| 403 | LAUNCH_TOKEN_USED | launch_token уже использован или истёк |
| 403 | UA_MISMATCH | User-Agent не соответствует SDK |
| 429 | RATE_LIMITED | Превышен rate limit регистрации |
| 503 | SERVICE_UNAVAILABLE | Плановое обслуживание |
Changelog
v1.0.0 — 2026-06-23
- Первый публичный релиз SDK
- Bearer-токены с ротацией через refresh
- Одноразовые launch_token (TTL 10 сек)
- AES-256-GCM зашифрованный postMessage-канал
- Certificate Pinning для Android и iOS
- Push-payload шифрование
- Deep links: request, chat, profile, create, verification
Нужна помощь? Напишите нам на sdk@sosed-sosedu.ru или в Telegram @soseddev. Мы поддерживаем интеграцию на всех этапах.