25 β Mini App backend¶
β¬ οΈ Oldingi: 24 β Web App xavfsizligi: initData Β· π README Β· Keyingi: 26 β Kapston: Hamster uslubidagi clicker Mini App β‘οΈ
Bu bobda: Mini App'ning yuragi β backend'ni quramiz. 24-bobda
initDataimzosini qanday tekshirishni o'rgandik; endi shu tekshiruvni har bir API so'rovida avtomatik ishlatadigan to'liq server yozamiz.aiohttp(botning o'zi ishlatadigan kutubxona) bilan: (1) Web App'ning HTML/static fayllarini serve qilamiz, (2)/api/...JSON endpointlarini ochamiz, (3) auth middleware yozamiz β u har/api/*so'rovidacheck_webapp_signature(token, init_data)ni chaqirib, imzosiz yoki buzilgan so'rovni401bilan rad etadi, to'g'risida esa foydalanuvchiniinitData'dan aniqlaydi. So'ng o'yin/ilova holatini (score,level) foydalanuvchiidbo'yicha bazada (10-bob β SQLite/SQLAlchemy) saqlaymiz. Bot + Web App + backend uchburchagi ni tushunamiz: bot tugma beradi -> WebView backend'ga so'rov yuboradi -> backend Telegram'ni token bilan tekshiradi (chunki imzo kaliti tokendan chiqadi). Oxirida CORS, lokal sinov va HTTPS/ngrok tushunchasini ko'ramiz.Halol eslatma: Bu bobdagi backend β middleware, endpointlar,
initDatatekshiruvi va holat saqlash β token va internetsiz,aiohttpTestClientbilan OFFLINE haqiqatan ishga tushirib tekshirilgan (imzo soxta token bilan generatsiya qilinadi). Quyida ko'rsatilgan natijalar β real test natijalari. Faqat Mini App'ning Telegram ichida REAL ochilishi, jonli qurilmadan kelganinitDatava public HTTPS hosting (ngrok / domen) β bular jonli Telegram + internet talab qiladi, shuning uchun ular illustrativ (kod to'g'ri, lekin natija faqat jonli muhitda ko'rinadi) deb belgilangan.
25.1 β Nega Mini App'ga backend kerak?¶
Tasavvur qiling: Hamster uslubidagi "tap-to-earn" o'yin qildingiz. Foydalanuvchi ekranni bossa β balans oshadi. Agar balansni faqat brauzerda (localStorage) saqlasangiz, har qanday foydalanuvchi konsolni ochib balance = 999999999 deb yozadi β va siz hech narsa qila olmaysiz. Clientga ishonib bo'lmaydi.
Demak holatni serverda saqlash kerak. Lekin server qayerdan biladi β bu so'rovni kim yubordi? Web sahifada parol yo'q. Mana shu yerda initData ish beradi:
- Telegram Web App'ni ochganda, frontendga
window.Telegram.WebApp.initDatani beradi β bu Telegram tomonidan bot tokeni bilan imzolangan satr (24-bob). - Frontend har so'rovda shu satrni backend'ga yuboradi.
- Backend bot tokeni bilan imzoni qayta hisoblaydi va solishtiradi. Mos kelsa β so'rov haqiqatan o'sha foydalanuvchidan; mos kelmasa β soxta.
Backend ikki vazifani bajaradi: Web App fayllarini berish va himoyalangan JSON API. Tuzilma quyidagicha:
Nega
aiohttp? Chunki aiogram'ning o'ziaiohttpustiga qurilgan β u allaqachon o'rnatilgan, webhook bilan tanish (13-bob), va botni hamda backend'ni bittaasynciojarayonida ishlatish oson. (FastAPI ham mukammal tanlov β bobning oxirida farqni aytamiz. Bu yerda izchillik uchunaiohttp'da qolamiz.)
25.2 β Uchburchak arxitektura: bot, Web App, backend¶
Mini App tizimida uchta "burchak" bor va ular qanday bog'lanishini tushunish β eng muhim aqliy model.
- Bot (aiogram) β foydalanuvchiga Web App'ni ochadigan tugma beradi (
WebAppInfo(url=...), 12/24-bob). Bot tokenning egasi. - Web App (frontend) β Telegram WebView ichida ochiladigan oddiy HTML/JS sahifa. U
tg.initDatani oladi va backend'ga so'rovlar yuboradi. - Backend (aiohttp) β frontend'ni serve qiladi va JSON API ni ta'minlaydi. U ham bot tokenini biladi β chunki imzoni tekshirish kaliti aynan tokendan kelib chiqadi (
secret_key = HMAC(b"WebAppData", token)).
Kalit g'oya: bot va backend bir xil tokenni bilishadi. Bot uni Telegram'ga buyruq berish uchun, backend esa kelgan initData imzosini tekshirish uchun ishlatadi. Frontend tokenni HECH QACHON ko'rmaydi β u faqat Telegram imzolab bergan initData ni uzatadi.
Amalda token bitta
.envfaylida saqlanadi (13-bob), botni ham backend'ni ham bittaBOT_TOKENo'qiydi. Tokenni hech qachon frontend kodiga yoki gitga qo'ymang.
25.3 β Eng kichik backend: static serve¶
Avval gap-so'zsiz ishlaydigan minimal server. Web App sahifasini beradigan index.html yarataylik:
<!-- web/index.html -->
<!DOCTYPE html>
<html lang="uz">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mini App</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
<h2>Balans: <span id="score">...</span></h2>
<button id="tap">Bosing!</button>
<script src="/static/app.js"></script>
</body>
</html>
Endi shu faylni beradigan server:
# server.py β eng kichik backend
from aiohttp import web
from pathlib import Path
WEB_DIR = Path(__file__).parent / "web"
async def index(request: web.Request) -> web.Response:
return web.FileResponse(WEB_DIR / "index.html")
def make_app() -> web.Application:
app = web.Application()
app.router.add_get("/", index)
# /static/app.js, /static/style.css ... shu papkadan beriladi
app.router.add_static("/static", WEB_DIR, name="static")
return app
if __name__ == "__main__":
web.run_app(make_app(), host="127.0.0.1", port=8080)
python server.py -> http://127.0.0.1:8080 da sahifa ochiladi. Bu jonli ishga tushirish β brauzerda ko'rasiz, lekin Telegram'siz initData bo'sh bo'ladi (buni 25.5'da hal qilamiz). Hozircha API qatlamiga o'tamiz.
25.4 β JSON API endpointlar¶
Web App balansni so'raydi va oshiradi. Ikki endpoint kerak:
GET /api/stateβ joriy holatni qaytaradi ({"score": ...}).POST /api/addβ bir tap qo'shadi.
aiohttp'da JSON javob β web.json_response(...):
# server.py (davomi) β hozircha himoyasiz, keyin auth qo'shamiz
async def api_state(request: web.Request) -> web.Response:
user = request["user"] # middleware joylashtiradi (25.5)
score = await load_score(user.id) # DB'dan (25.6)
return web.json_response({"user_id": user.id, "score": score})
async def api_add(request: web.Request) -> web.Response:
user = request["user"]
score = await add_one(user.id)
return web.json_response({"user_id": user.id, "score": score})
Frontend tomon (app.js) β har fetch'da initData ni Authorization sarlavhasida yuboradi:
// web/app.js
const tg = window.Telegram.WebApp;
tg.ready();
const headers = { "Authorization": tg.initData }; // imzolangan satr
async function refresh() {
const r = await fetch("/api/state", { headers });
if (r.status === 401) { document.body.innerHTML = "Avtorizatsiya xatosi"; return; }
const data = await r.json();
document.getElementById("score").textContent = data.score;
}
document.getElementById("tap").addEventListener("click", async () => {
const r = await fetch("/api/add", { method: "POST", headers });
const data = await r.json();
document.getElementById("score").textContent = data.score;
});
refresh();
initDataniAuthorizationheaderida yuborish β keng tarqalgan konvensiya. Uni?query yoki body'da ham yuborish mumkin; muhimi β backend uni server tomonda tekshiradi. Har so'rovda yuboriladi, chunki HTTP holatsiz (stateless) β server har safar "bu kim?" degan savolga javob topishi kerak.
Endi eng muhim qism β bu so'rovlarni himoyalash.
25.5 β Auth middleware: har so'rovda initData tekshirish¶
24-bobdan eslang: aiogram.utils.web_app ikki funksiya beradi:
check_webapp_signature(token, init_data) -> boolβ imzo to'g'rimi?safe_parse_webapp_init_data(token, init_data) -> WebAppInitDataβ imzoni tekshiradi va to'g'ri bo'lsa parslangan obyektni qaytaradi, aks holdaValueError.
Bu tekshiruvni har bir /api/* so'rovida takrorlamaslik uchun middleware yozamiz (9-bobdagi g'oya, lekin bu safar bot middleware'i emas, aiohttp middleware'i). U so'rovni handler'ga yetkazishdan oldin ushlab, imzoni tekshiradi:
# server.py (davomi)
import os
from aiohttp import web
from aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_data
BOT_TOKEN = os.environ["BOT_TOKEN"] # bot bilan BIR XIL token
@web.middleware
async def auth_middleware(request: web.Request, handler):
# Faqat /api/* yo'llari himoyalanadi; / va /static ochiq qoladi.
if request.path.startswith("/api/"):
init_data = request.headers.get("Authorization", "")
if not init_data or not check_webapp_signature(BOT_TOKEN, init_data):
return web.json_response({"error": "unauthorized"}, status=401)
# Imzo to'g'ri -> foydalanuvchini ishonchli aniqlaymiz.
data = safe_parse_webapp_init_data(BOT_TOKEN, init_data)
request["user"] = data.user # WebAppUser: .id, .first_name, .username ...
return await handler(request)
Middleware'ni ilovaga ulaymiz:
def make_app() -> web.Application:
app = web.Application(middlewares=[auth_middleware])
app.router.add_get("/", index)
app.router.add_static("/static", WEB_DIR, name="static")
app.router.add_get("/api/state", api_state)
app.router.add_post("/api/add", api_add)
return app
Endi har /api/... so'rovi avtomatik tekshiriladi. Handler ichida request["user"] β bu imzo bilan tasdiqlangan, ishonchli foydalanuvchi. Frontend user.id ni o'zgartirib yubora olmaydi β chunki har qanday o'zgarish imzoni buzadi va 401 qaytadi.
Eng muhim qoida: foydalanuvchi
id'sini HECH QACHON oddiy parametr (?user_id=...) sifatida qabul qilmang. Faqatsafe_parse_webapp_init_dataqaytargandata.user.idga ishoning. Aks holda har kim boshqaning balansini o'zgartiradi.
auth_date β eskirgan initData'ni rad etish (ixtiyoriy, lekin tavsiya etiladi)¶
initData ichida auth_date (Unix vaqt) bor. Real production'da bir necha soatdan eski initData ni rad etish mantiqan to'g'ri (o'g'irlangan initData ni cheklash uchun):
import time
MAX_AGE = 24 * 3600 # 24 soat
# auth_middleware ichida, imzo tekshirilgandan keyin:
auth_age = time.time() - data.auth_date.timestamp()
if auth_age > MAX_AGE:
return web.json_response({"error": "init data expired"}, status=401)
25.6 β Holatni bazada saqlash¶
Imzo tekshirildi, user.id ishonchli. Endi o'yin holatini shu id bo'yicha saqlaymiz. 10-bobdagi SQLite naqshini ishlatamiz. Eng sodda shakli β aiosqlite:
# storage.py
import aiosqlite
DB_PATH = "miniapp.db"
async def init_db() -> None:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"CREATE TABLE IF NOT EXISTS scores ("
" user_id INTEGER PRIMARY KEY,"
" score INTEGER NOT NULL DEFAULT 0"
")"
)
await db.commit()
async def load_score(user_id: int) -> int:
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT score FROM scores WHERE user_id = ?", (user_id,)
) as cur:
row = await cur.fetchone()
return row[0] if row else 0
async def add_one(user_id: int) -> int:
async with aiosqlite.connect(DB_PATH) as db:
# UPSERT: yo'q bo'lsa 1, bor bo'lsa +1
await db.execute(
"INSERT INTO scores (user_id, score) VALUES (?, 1) "
"ON CONFLICT(user_id) DO UPDATE SET score = score + 1",
(user_id,),
)
await db.commit()
return await load_score(user_id)
Endi api_state / api_add shu funksiyalarni chaqiradi (25.4'dagi kod). Ilova ishga tushganda bazani tayyorlash uchun on_startup ulaymiz:
async def on_startup(app: web.Application) -> None:
await init_db()
# make_app ichida:
app.on_startup.append(on_startup)
Real loyihada
aiosqliteo'rniga SQLAlchemy + repository naqshi (10-bob) ni ishlatish kengayadigan tuzilma beradi. Bu yerda g'oyani aniq ko'rsatish uchun eng sodda shaklni oldik.
25.7 β Bot bilan ulanish: Web App tugmasi¶
Backend tayyor. Endi bot foydalanuvchiga uni ochadigan tugma berishi kerak. URL β backend joylashgan public HTTPS manzil (25.9):
# bot.py (qism) β aiogram 3.x
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message, InlineKeyboardButton, WebAppInfo
from aiogram.utils.keyboard import InlineKeyboardBuilder
router = Router()
WEBAPP_URL = "https://mening-domenim.example/" # backend'ning HTTPS manzili
@router.message(CommandStart())
async def start(message: Message):
kb = InlineKeyboardBuilder()
kb.button(text="O'yinni ochish", web_app=WebAppInfo(url=WEBAPP_URL))
await message.answer("Tap-to-earn o'yiniga xush kelibsiz!", reply_markup=kb.as_markup())
Foydalanuvchi tugmani bosadi -> Telegram WebView'da WEBAPP_URL ochiladi -> sahifa tg.initData ni oladi va backend'ga fetch qiladi -> backend imzoni bot tokeni bilan tekshiradi. Uchburchak yopildi.
Bot va backend bitta jarayonda ishlashi mumkin: aiogram polling'ini
asyncio.create_task(dp.start_polling(bot))bilan ishga tushirib, yonidaweb.run_app(...)o'rnigaAppRunnerishlatiladi. Lekin ko'pincha ularni alohida jarayon/konteyner sifatida ham yuritishadi β token orqali bog'lanaveradi. Bu jonli qism (token + HTTPS kerak), shuning uchun illustrativ.
25.8 β CORS: qachon kerak, qachon kerakmas¶
Agar frontend (index.html) va API bitta domen/portdan kelsa (bizdagidek β bitta aiohttp ilova hammasini beradi), CORS kerak emas: bu "same-origin", brauzer hech narsani to'smaydi.
CORS faqat frontend boshqa domendan kelsa kerak bo'ladi (masalan, static'ni Cloudflare Pages, API'ni boshqa serverda yuritsangiz). U holda aiohttp-cors ishlatiladi:
# faqat ALOHIDA domenli API kerak bo'lsa
import aiohttp_cors
cors = aiohttp_cors.setup(app, defaults={
"https://mening-frontendim.example": aiohttp_cors.ResourceOptions(
allow_headers=("Authorization",),
)
})
for route in list(app.router.routes()):
cors.add(route)
Tavsiya: boshlovchi uchun frontend va API'ni bitta aiohttp ilovasidan bering β CORS muammosi umuman tug'ilmaydi. Bu bobda biz aynan shunday qildik.
25.9 β Lokal sinov va HTTPS / ngrok¶
Telegram Web App URL'i HTTPS bo'lishi SHART (http:// qabul qilinmaydi). Lokal mashinada http://127.0.0.1:8080 ishlaydi β lekin Telegram bunga ulanolmaydi. Yechim β lokal serverni vaqtincha public HTTPS manzilga "tunnel" qilish. Eng mashhuri β ngrok:
# lokalda server 8080-portda ishlayotganida:
ngrok http 8080
# -> Forwarding https://abc123.ngrok-free.app -> http://127.0.0.1:8080
Endi https://abc123.ngrok-free.app ni WEBAPP_URL ga qo'yasiz, BotFather'da Web App URL'ini ham shu qilasiz, va telefoningizda o'yinni ochib jonli sinaysiz. (Production'da ngrok o'rniga real domen + HTTPS sertifikat β masalan Caddy/nginx + Let's Encrypt, yoki Cloudflare.)
Bu bo'lim illustrativ: ngrok va jonli Telegram sinovi internet + token talab qiladi. Quyidagi bo'limda esa hammani internetsiz, haqiqatan tekshiramiz.
25.10 β OFFLINE verifikatsiya: TestClient bilan endpointni sinash¶
Eng kuchli qism: backend'ni Telegram'siz, token'siz, internet'siz haqiqatan ishga tushirib tekshirish. aiohttp.test_utils (TestServer + TestClient) ilovani xotirada ko'taradi va unga real HTTP so'rov yuboradi.
Bitta qiyinchilik: bizda jonli initData yo'q. Lekin biz uni o'zimiz generatsiya qila olamiz β Telegram client aynan shu algoritmni bajaradi (24-bob): soxta token bilan data_check_string quramiz va HMAC bilan imzolaymiz. Shunday qilib check_webapp_signature "to'g'ri imzo" deb topadi.
# test_miniapp.py
import asyncio, hashlib, hmac, json, time
from urllib.parse import urlencode
from aiohttp import web
from aiohttp.test_utils import TestServer, TestClient
from aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_data
FAKE_TOKEN = "123456:AAH-Test_abc" # soxta token β internet kerakmas
STATE: dict[int, dict] = {} # bu testda DB o'rniga xotira
# --- Telegram client nima qilishini taqlid qilamiz: initData ni imzolaymiz ---
def make_init_data(token: str, user: dict) -> str:
fields = {
"user": json.dumps(user, separators=(",", ":")),
"auth_date": str(int(time.time())),
"query_id": "AAEtest123",
}
dcs = "\n".join(f"{k}={fields[k]}" for k in sorted(fields))
secret = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
fields["hash"] = hmac.new(secret, dcs.encode(), hashlib.sha256).hexdigest()
return urlencode(fields)
# --- backend (bobdagining aynan o'zi) ---
@web.middleware
async def auth_middleware(request, handler):
if request.path.startswith("/api/"):
init = request.headers.get("Authorization", "")
if not init or not check_webapp_signature(FAKE_TOKEN, init):
return web.json_response({"error": "unauthorized"}, status=401)
request["user"] = safe_parse_webapp_init_data(FAKE_TOKEN, init).user
return await handler(request)
async def api_state(request):
u = request["user"]; row = STATE.setdefault(u.id, {"score": 0})
return web.json_response({"user_id": u.id, "name": u.first_name, "score": row["score"]})
async def api_add(request):
u = request["user"]; row = STATE.setdefault(u.id, {"score": 0})
row["score"] += 1
return web.json_response({"user_id": u.id, "score": row["score"]})
def make_app():
app = web.Application(middlewares=[auth_middleware])
app.router.add_get("/api/state", api_state)
app.router.add_post("/api/add", api_add)
return app
async def main():
client = TestClient(TestServer(make_app()))
await client.start_server()
# 1) initData'siz -> 401
r = await client.get("/api/state")
assert r.status == 401
# 2) buzilgan initData -> 401
r = await client.get("/api/state", headers={"Authorization": "user=x&hash=dead"})
assert r.status == 401
# 3) to'g'ri initData -> 200 + holat
init = make_init_data(FAKE_TOKEN, {"id": 777, "first_name": "Oqil"})
r = await client.get("/api/state", headers={"Authorization": init})
assert r.status == 200 and (await r.json())["score"] == 0
# 4) tap qo'shish -> holat saqlanadi
await client.post("/api/add", headers={"Authorization": init})
r = await client.post("/api/add", headers={"Authorization": init})
assert (await r.json())["score"] == 2
# 5) buzib yuborilgan initData (id o'zgartirilgan, eski hash) -> 401
r = await client.get("/api/state", headers={"Authorization": init.replace("777", "999")})
assert r.status == 401
await client.close()
print("ALL ASSERTIONS PASSED")
if __name__ == "__main__":
asyncio.run(main())
Bu skriptni python test_miniapp.py bilan ishga tushirdik (aiogram 3.28 + aiohttp 3.13 + Python 3.14). Haqiqiy natija:
=== RESULTS ===
401 no-auth GET /api/state -> {'error': 'unauthorized'}
401 bad-auth GET /api/state -> {'error': 'unauthorized'}
200 valid GET /api/state -> {'user_id': 777, 'name': 'Oqil', 'score': 0}
200 valid POST /api/add -> {'user_id': 777, 'score': 1}
200 valid POST /api/add #2 -> {'user_id': 777, 'score': 2}
200 valid GET /api/state after adds -> {'user_id': 777, 'name': 'Oqil', 'score': 2}
401 tampered GET /api/state -> {'error': 'unauthorized'}
ALL ASSERTIONS PASSED
Demak: imzosiz/buzilgan/buzib-yuborilgan so'rov -> 401, to'g'ri imzolangan so'rov -> 200 va holat bazada (testda xotirada) saqlanmoqda. Bu β real test, soxta "ishladi" emas.
Diqqat: punkt (5)'da biz
777ni999ga almashtirdik, lekin eskihashqoldik β imzo endi mos kelmaydi, shuning uchun401. Bu aynan hakeruser.idni o'zgartirib boshqaning balansiga kirmoqchi bo'lgan holatni modellaydi. Backend uni rad etadi.
pytest bilan rasmiy test¶
Yuqoridagi main() o'rniga pytest-asyncio ishlatib, har holatni alohida test qilish toza:
# test_api.py
import pytest
from aiohttp.test_utils import TestServer, TestClient
@pytest.fixture
async def client():
c = TestClient(TestServer(make_app()))
await c.start_server()
yield c
await c.close()
@pytest.mark.asyncio
async def test_no_auth_returns_401(client):
r = await client.get("/api/state")
assert r.status == 401
@pytest.mark.asyncio
async def test_valid_initdata_returns_200(client):
init = make_init_data(FAKE_TOKEN, {"id": 777, "first_name": "Oqil"})
r = await client.get("/api/state", headers={"Authorization": init})
assert r.status == 200
25.11 β Xulosa¶
- Mini App backend ikki vazifa: frontend (static) serve qilish + himoyalangan JSON API.
- Uchburchak: bot tugma beradi -> Web App backend'ga so'rov yuboradi -> backend
initDataimzosini bot tokeni bilan tekshiradi. Bot va backend bir xil tokenni bilishadi. - Auth middleware har
/api/*so'rovidacheck_webapp_signatureni chaqiradi: imzosiz/buzilgan ->401; to'g'ri ->safe_parse_webapp_init_dataorqali ishonchliuserni oladi. - Foydalanuvchi
id'siga FAQATinitData'dan ishoning β hech qachon query/body parametriga emas. - Holat bazada
user.idbo'yicha saqlanadi (10-bob). - Frontend+API bitta domendan kelsa CORS kerak emas; HTTPS shart (lokalda ngrok bilan tunnel).
- Hammasi
aiohttpTestClientbilan offline, haqiqatan tekshirildi.
Keyingi bobda shu poydevorga to'liq Hamster uslubidagi clicker Mini App ni quramiz: energiya, upgrade, balans β va o'yin mantig'ini sof funksiya sifatida pytest bilan sinaymiz.
Mashqlar¶
Oson¶
make_app()gaGET /api/pingendpointini qo'shing (himoyalangan,{"pong": true}qaytaradi).TestClientbilan: initData'siz401, to'g'ri initData bilan200ekanini tekshiring.auth_middlewaredaAuthorizationo'rnigaX-Init-Datasarlavhasidan o'qishga o'zgartiring. Test headerini ham moslang.api_statejavobigausernamemaydonini qo'shing (request["user"].username).make_init_datauser'igausernameqo'shib tekshiring.make_init_datadaauth_dateni 0 (1970-yil) qilib bering. Imzo hali ham to'g'ri bo'ladimi? Nega? (Maslahat: imzoauth_dateqiymatidan ham hisoblanadi, lekin "eskirgan" tekshiruvi alohida.)- Nega
/va/static/*yo'llariauth_middlewareda tekshirilmaydi? Bir jumlada tushuntiring. web.json_response({"error": "unauthorized"}, status=401)o'rnigaweb.Response(status=401)qo'ysangiz, frontendr.json()chaqirganda nima bo'ladi?
O'rta¶
auth_middlewaregaauth_dateeskirgan bo'lsa (MAX_AGE = 3600)401qaytaradigan tekshiruv qo'shing.make_init_dataga eskiauth_date(time.time() - 7200) berib,401kelishini test qiling.api_addni "har tap +1" o'rniga "ixtiyoriy miqdor" qabul qiladigan qiling:POST /api/addbody'da{"amount": 5}. Body'ni validatsiya qiling (1..10 oralig'ida bo'lsin, aks holda400).aiosqliteversiyasini (25.6)TestClientbilan tekshiring: vaqtinchalik DB fayli yarating, ikki tap qo'shing,GET /api/statescore == 2qaytarishini tasdiqlang.- Ikki xil foydalanuvchi (
id=1vaid=2) initData'sini generatsiya qiling. Har biriPOST /api/addqilsa, balanslar alohida saqlanishini (bir-biriga aralashmasligini) test qiling. safe_parse_webapp_init_dataValueErrortashlasa nima bo'lishini tekshiring:auth_middlewaredacheck_webapp_signatureo'rniga to'g'ridan-to'g'risafe_parse_webapp_init_datanitry/except ValueErrorbilan ishlatib qayta yozing. Ikki yondashuvning farqi nimada?- CORS qachon kerak bo'lishini misol bilan tushuntiring: frontend
https://app.example, APIhttps://api.exampleda. Bitta domenli yechim CORS'ni qanday yo'q qiladi?
Qiyin¶
- Bot polling'i va aiohttp backend'ni bitta
asynciojarayonida ishga tushiradiganmain()yozing (AppRunner+asyncio.create_task(dp.start_polling(bot))). Token soxta bo'lgani uchun polling jonli ishlamaydi (illustrativ deb belgilang), lekin backendTestClientsiz hamAppRunnerbilan ko'tarilishini ko'rsating. auth_middlewareni parametrlangan qiling: himoyalanadigan prefix (/api/) ni va token'ni argument sifatida qabul qiladiganmake_auth_middleware(token, prefix="/api/")fabrikasini yozing. Ikki xil prefix bilan test qiling.- "Soxta imzo hujumi" ni modellang: haker
make_init_datani noto'g'ri token ("999:HACK") bilan generatsiya qiladi, lekin backendFAKE_TOKENbilan tekshiradi. Natija401ekanini test qiling va nega buzg'unchi to'g'ri imzo yasolmasligini izohlang.
Yechimlar
1.
async def api_ping(request): return web.json_response({"pong": True})
# make_app: app.router.add_get("/api/ping", api_ping)
# test:
r = await client.get("/api/ping"); assert r.status == 401
init = make_init_data(FAKE_TOKEN, {"id": 1, "first_name": "A"})
r = await client.get("/api/ping", headers={"Authorization": init})
assert r.status == 200 and (await r.json())["pong"] is True
2. Middleware'da request.headers.get("Authorization", "") -> request.headers.get("X-Init-Data", ""). Testda headers={"X-Init-Data": init}. Frontend app.js da ham { "X-Init-Data": tg.initData } qilinadi.
3. api_state da "username": request["user"].username. make_init_data(FAKE_TOKEN, {"id": 1, "first_name": "A", "username": "aaa"}). WebAppUser username maydonini parslab beradi.
4. Imzo TO'G'RI bo'ladi β chunki check_webapp_signature faqat HMAC mosligini tekshiradi, qiymat "mazmuni"ga qaramaydi. auth_date=0 ham to'g'ri imzolanishi mumkin. "Eskirgan" muammosi β bu alohida biznes-tekshiruv (25.5.1, mashq 7); imzo to'g'riligi bilan aralashtirmaslik kerak.
5. Chunki / va /static/* β ochiq sahifa/fayllar (HTML, JS, CSS): ularda maxfiy ma'lumot yo'q va Web App ulardan oldin initData ni hali olmagan bo'lishi mumkin; himoya faqat ma'lumot qaytaradigan API'ga kerak.
6. Frontend await r.json() chaqirganda javob bo'sh (JSON emas) bo'lgani uchun parslash xatosi (SyntaxError/ContentTypeError) tushadi. Shuning uchun xato javobini ham JSON qilib qaytarish yaxshi β frontend r.status ni tekshirib, body'ni xavfsiz o'qiydi.
7.
import time
MAX_AGE = 3600
# middleware ichida, parse'dan keyin:
data = safe_parse_webapp_init_data(BOT_TOKEN, init_data)
if time.time() - data.auth_date.timestamp() > MAX_AGE:
return web.json_response({"error": "expired"}, status=401)
request["user"] = data.user
# test: make_init_data'ni auth_date parametrli qiling:
def make_init_data(token, user, auth_date=None):
auth_date = auth_date or int(time.time())
... # fields["auth_date"] = str(auth_date)
init = make_init_data(FAKE_TOKEN, {"id": 1, "first_name": "A"}, auth_date=int(time.time()) - 7200)
r = await client.get("/api/state", headers={"Authorization": init}); assert r.status == 401
8.
async def api_add(request):
u = request["user"]
try:
body = await request.json()
except Exception:
body = {}
amount = body.get("amount", 1)
if not isinstance(amount, int) or not (1 <= amount <= 10):
return web.json_response({"error": "amount 1..10"}, status=400)
row = STATE.setdefault(u.id, {"score": 0}); row["score"] += amount
return web.json_response({"score": row["score"]})
9.
import aiosqlite, tempfile, os
# init_db/load_score/add_one (25.6) ni DB_PATH parametrli qiling.
# fixture'da: db_path = tempfile.mktemp(suffix=".db"); init_db(db_path) ni on_startup'da chaqir.
init = make_init_data(FAKE_TOKEN, {"id": 5, "first_name": "B"})
await client.post("/api/add", headers={"Authorization": init})
await client.post("/api/add", headers={"Authorization": init})
r = await client.get("/api/state", headers={"Authorization": init})
assert (await r.json())["score"] == 2
# tozalash: os.remove(db_path)
10.
i1 = make_init_data(FAKE_TOKEN, {"id": 1, "first_name": "A"})
i2 = make_init_data(FAKE_TOKEN, {"id": 2, "first_name": "B"})
await client.post("/api/add", headers={"Authorization": i1})
await client.post("/api/add", headers={"Authorization": i1})
await client.post("/api/add", headers={"Authorization": i2})
r1 = await (await client.get("/api/state", headers={"Authorization": i1})).json()
r2 = await (await client.get("/api/state", headers={"Authorization": i2})).json()
assert r1["score"] == 2 and r2["score"] == 1 # alohida saqlanadi
11.
@web.middleware
async def auth_middleware(request, handler):
if request.path.startswith("/api/"):
init = request.headers.get("Authorization", "")
try:
request["user"] = safe_parse_webapp_init_data(BOT_TOKEN, init).user
except ValueError:
return web.json_response({"error": "unauthorized"}, status=401)
return await handler(request)
check_webapp_signature faqat bool qaytaradi (tez, "bormi-yo'qmi"), safe_parse_webapp_init_data esa tekshiradi VA parslaydi (ValueError tashlaydi). Ikkinchi yondashuv ixchamroq β bitta chaqiruv ham tekshiradi ham user beradi.
12. Brauzer "same-origin policy" bo'yicha https://app.example sahifasidan https://api.example ga fetch qilishni β agar API CORS sarlavhalarini (Access-Control-Allow-Origin) qaytarmasa β to'sadi. Bitta domenli yechimda (https://app.example ham frontend ham API) so'rov o'sha origin'ga ketadi -> same-origin -> brauzer to'smaydi -> CORS keraksiz.
13.
import asyncio
from aiohttp import web
from aiogram import Bot, Dispatcher
async def main():
bot = Bot("123456:AAH-Test_abc") # soxta token -> polling jonli emas (ILLUSTRATIV)
dp = Dispatcher()
runner = web.AppRunner(make_app())
await runner.setup()
site = web.TCPSite(runner, "127.0.0.1", 8080)
await site.start() # backend REAL ko'tariladi
print("backend running on :8080")
# polling β jonli token kerak, illustrativ:
# asyncio.create_task(dp.start_polling(bot))
await asyncio.sleep(0.1) # demo uchun; real'da Event() bilan kutiladi
await runner.cleanup()
asyncio.run(main())
AppRunner bilan haqiqatan ko'tariladi (offline tekshirsa bo'ladi); start_polling esa soxta token bilan jonli ishlamaydi β shuning uchun izoh qoldirildi/illustrativ.
14.
def make_auth_middleware(token: str, prefix: str = "/api/"):
@web.middleware
async def mw(request, handler):
if request.path.startswith(prefix):
init = request.headers.get("Authorization", "")
if not init or not check_webapp_signature(token, init):
return web.json_response({"error": "unauthorized"}, status=401)
request["user"] = safe_parse_webapp_init_data(token, init).user
return await handler(request)
return mw
# make_app(prefix="/api/"): app = web.Application(middlewares=[make_auth_middleware(BOT_TOKEN, prefix)])
/api/ prefiksi bilan, boshqasini /v2/ bilan quring va mos yo'llarda 401/200 ni tekshiring.
15.
hacker = make_init_data("999:HACK", {"id": 1, "first_name": "Hacker"}) # noto'g'ri token bilan imzolangan
r = await client.get("/api/state", headers={"Authorization": hacker})
assert r.status == 401
FAKE_TOKEN bilan hisoblaydi; haker secret_key ni faqat FAKE_TOKEN dan chiqarib to'g'ri imzo yasashi mumkin edi β lekin u tokenni bilmaydi (token faqat botda va backendda). Tokensiz HMAC'ni qaytadan hisoblab bo'lmaydi, shuning uchun imzo mos kelmaydi -> 401. Aynan shu sabab initData xavfsiz: ishonch ildizi β maxfiy token.
β¬ οΈ Oldingi: 24 β Web App xavfsizligi: initData Β· π README Β· Keyingi: 26 β Kapston: Hamster uslubidagi clicker Mini App β‘οΈ