Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Himoyalangan marshrut — yo'lsiz layout
- 2.1 Qorovul komponenti
- 2.2 ?keyin= — qaytish manzili
- 2.3 Kirgan foydalanuvchi /kirish ni ochsa
- 3. Miltillash: "hali bilmayman" holati
- 3.1 Muammo
- 3.2 <Navigate> va bo'sh birinchi kadr
- 4. Rollar: kim nimani ko'radi
- 4.1 Rol — qo'shimcha shart
- 4.2 Menyuni yashirish — himoya emas
- 5. Loader'da ham tekshirish
- 5.1 Loader qorovuldan oldin ishlaydi
- 5.2 Davosi: loader o'zi /kirish ga yo'naltiradi — replace() bilan
- 5.3 Nega router middleware emas?
- 6. Vazifalar qadami: /kirish va himoyalangan layout (holat 07)
- 6.1 Nima o'zgaradi
- 6.2 Marshrutlar
- 6.3 Tekshiruvlar
- 7. Hujumchi nigohi
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qayerga tushadi?
- 2-mashq (o'rta): qorovulga ?keyin=
- 3-mashq (qiyin): admin sahifasi
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
React Router himoyalangan marshrutlar: Navigate, ?keyin= qaytish, rollar va loader'da tekshirish
Qisqacha: Himoyalangan sahifalar bitta yo'lsiz layout marshrut ichiga olinadi. Uning komponenti foydalanuvchi kirmagan bo'lsa
<Navigate to="/kirish?keyin=/joriy/yol" replace />qaytaradi, kirgan bo'lsa —<Outlet />. Kirish sahifasi muvaffaqiyatdan keyin?keyin=dagi manzilga qaytaradi — faqat/bilan boshlanadigan ichki yo'l bo'lsa (ochiq yo'naltirish hujumi). Loader komponentdan oldin ishlaydi, shuning uchun u ham o'zi tekshiradi. Rollar ham xuddi shunday: menyu rolga qarab, sahifa — 403. Bularning hammasi faqat ko'rinish: haqiqiy himoya — serverda.
Bu darsda
- Himoyalangan marshrutlarni bitta yo'lsiz layout bilan o'raysiz va kirmagan foydalanuvchini
/kirishga yuborasiz. ?keyin=bilan kirgandan keyin o'sha sahifaga qaytarasiz va ochiq yo'naltirish (open redirect) hujumini to'sasiz.- Sessiya tekshirilayotganda "miltillash" (kirish formasi bir zum ko'rinib yo'qolishi) ning oldini olasiz.
- Rolga qarab menyuni va sahifani cheklaysiz (403).
- Nega loader'da ham tekshirish kerakligini o'lchov bilan ko'rasiz va
vazifalarga/kirishsahifasini qo'shasiz.
Oldin bilishingiz kerak: React Router III: dasturiy navigatsiya, data mode — loader va action, React'da auth holati.
1. Nega bu kerak?
18-qismning oxirgi darsida vazifalar ga soxta kirish qo'shildi. U butun ilovani o'rardi: <Himoyalangan> kirmagan foydalanuvchiga ilova o'rniga kirish formasini ko'rsatardi. Router kelgach, uchta muammo chiqdi:
- Manzil yolg'on gapiradi.
/statistikaochildi — manzil/statistika, ekranda esa kirish formasi. Kirish sahifasining o'z manzili yo'q, unga havola berib bo'lmaydi. - Qaytish yo'q. Do'stingiz
/vazifa/2havolasini yubordi. Siz kirdingiz — va ro'yxatga tushdingiz. Qaysi vazifa edi? Havola esdan chiqdi. - Loader himoyani chetlab o'tadi. Vazifa sahifasining loader'i komponentdan oldin ishlaydi. Kirmagan foydalanuvchi uchun ham serverga so'rov ketadi (kanon buni o'lchadi — «Loader'da ham tekshirish» bo'limida).
Hayotiy o'xshatish — bino qorovuli. Yaxshi qorovul ruxsatnomasiz odamni eshikdan qaytaradi, lekin "qaysi xonaga kelgandingiz?" deb yozib qo'yadi: ruxsatnoma olgach, odam to'g'ri o'sha xonaga boradi. Har xona eshigida esa (loader) yana bir qulf bor — kimdir derazadan kirib qolmasin.
Diqqat: Bu dars — ko'rinish himoyasi. Brauzerdagi kodni har kim o'qiydi va o'zgartiradi: DevTools'da
localStoragega ism yozib qo'yish kifoya. Ma'lumotni haqiqatan himoya qiladigan joy — server, u har so'rovda sessiyani tekshiradi (Session-based auth, 28-qism). Bugungi kod foydalanuvchiga qulaylik beradi va keraksiz so'rovlarni to'xtatadi, xolos.
2. Himoyalangan marshrut — yo'lsiz layout
2.1 Qorovul komponenti
03-darsdagi yo'lsiz layout — path siz marshrut. U manzilga hech narsa qo'shmaydi, faqat bolalarini o'raydi. Qorovul uchun ideal joy: himoyalangan sahifalar uning bolalari bo'ladi.
Ingliz tilidagi maqolalarda bu komponent ProtectedRoute yoki RequireAuth deb ataladi. Natija oynasida auth — oddiy Context, ism useState da:
import { createContext, use, useState } from "react";
import {
createMemoryRouter,
Link,
Navigate,
Outlet,
RouterProvider,
useLocation,
useNavigate,
} from "react-router";
const AuthContext = createContext(null);
function Shell() {
const { pathname } = useLocation();
return (
<div>
<p>Manzil: {pathname}</p>
<Outlet />
</div>
);
}
// Qorovul: kirmagan — /kirish ga, kirgan — bola sahifa
function RequireAuth() {
const { user } = use(AuthContext);
if (user === null) {
return <Navigate to="/kirish" replace />;
}
return <Outlet />;
}
function Login() {
const { setUser } = use(AuthContext);
const navigate = useNavigate();
function logIn() {
setUser("Malika");
navigate("/kabinet", { replace: true });
}
return <button onClick={logIn}>Kirish</button>;
}
const router = createMemoryRouter([
{
path: "/",
element: <Shell />,
children: [
{ index: true, element: <Link to="/kabinet">Kabinet</Link> },
{ path: "kirish", element: <Login /> },
{
// path yo'q — yo'lsiz layout
element: <RequireAuth />,
children: [
{ path: "kabinet", element: <p>Kabinet: buyurtmalar</p> },
],
},
],
},
]);
export default function App() {
const [user, setUser] = useState(null);
return (
<AuthContext value={{ user, setUser }}>
<RouterProvider router={router} />
</AuthContext>
);
}Ekranda:
Manzil: /kabinet
Kabinet: buyurtmalarOqim: «Kabinet» → qorovul "kirmagan" deb /kirish ga yubordi → «Kirish» → kabinet. Ikki joyda replace: yo'naltirishda va kirishdan keyin. Navigatsiya darsidagi qoida — yo'naltirish tarixni o'stirmasin. Aks holda «Orqaga» foydalanuvchini qorovulga qaytarib, u yana /kirish ga otadi.
Endi yangi himoyalangan sahifa qo'shish — bitta qator: RequireAuth ning children iga. Har sahifaga alohida if (user === null) yozilmaydi va hech biri esdan chiqmaydi.
Maslahat:
<AuthContext value={…}>— React 19 da Context'ning o'zi provider (Context). Auth provider router'dan tashqarida: kirish holati sahifa almashganda yo'qolmasin.
2.2 ?keyin= — qaytish manzili
Oqimda kamchilik bor: kirgandan keyin har doim /kabinet ga. Foydalanuvchi /buyurtma/12 ni ochmoqchi bo'lgan bo'lsa-chi? Qorovul qaytish manzilini URL'ga yozib qo'yadi: /kirish?keyin=%2Fbuyurtma%2F12. Inglizcha loyihalarda ?from=, ?redirect=, ?next= deyiladi. Kirish sahifasi uni useSearchParams bilan o'qiydi:
import { createContext, use, useState } from "react";
import {
createMemoryRouter,
Link,
Navigate,
Outlet,
RouterProvider,
useLocation,
useNavigate,
useSearchParams,
} from "react-router";
const AuthContext = createContext(null);
function Shell() {
const { pathname, search } = useLocation();
return (
<div>
<p>Manzil: {pathname + search}</p>
<Outlet />
</div>
);
}
function RequireAuth() {
const { user } = use(AuthContext);
const { pathname, search } = useLocation();
if (user === null) {
const back = new URLSearchParams({ keyin: pathname + search });
return <Navigate to={`/kirish?${back}`} replace />;
}
return <Outlet />;
}
function Login() {
const { setUser } = use(AuthContext);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
function logIn() {
setUser("Malika");
navigate(searchParams.get("keyin") ?? "/", { replace: true });
}
return <button onClick={logIn}>Kirish</button>;
}
const router = createMemoryRouter([
{
path: "/",
element: <Shell />,
children: [
{
index: true,
element: <Link to="/buyurtma/12">12-buyurtma</Link>,
},
{ path: "kirish", element: <Login /> },
{
element: <RequireAuth />,
children: [
{
path: "buyurtma/:id",
element: <p>Buyurtma: osh, choy</p>,
},
],
},
],
},
]);
export default function App() {
const [user, setUser] = useState(null);
return (
<AuthContext value={{ user, setUser }}>
<RouterProvider router={router} />
</AuthContext>
);
}Ekranda:
Manzil: /buyurtma/12
Buyurtma: osh, choyOraliq nuqta (ekran("keyin=")) kirish sahifasidagi manzilni tekshirdi: /kirish?keyin=%2Fbuyurtma%2F12. %2F — / ning URL ichidagi kodi: URLSearchParams qiymatni o'zi kodlaydi (URL va URLSearchParams). Kirishdan keyin — aynan /buyurtma/12.
Butun oqim bitta rasmda — vazifalar da shunday bo'ladi:
flowchart TD
A["/vazifa/2 ochildi"] --> B{"kirganmi?"}
B -- ha --> C["vazifa sahifasi"]
B -- yo'q --> D["/kirish?keyin=%2Fvazifa%2F2<br/>(replace)"]
D --> E["ism kiritildi"]
E --> F{"keyin — ichki yo'lmi?"}
F -- ha --> G["/vazifa/2 (replace)"]
F -- yo'q --> H["/ (bosh sahifa)"]Ikki romb — ikki tekshiruv. Birinchisi — qorovul (va loader). Ikkinchisi — ?keyin= qiymati.
Bu kod ishlaydi, lekin unda xavfli teshik bor: searchParams.get("keyin") tekshirilmasdan navigate ga berildi. Nima bo'lishi mumkinligi — «Hujumchi nigohi» bo'limida.
Tekshirib ko'ring: Nega qaytish manzili
location.stateda emas, URL'da (?keyin=)? Oldingi darsdan bilasiz —navigategastateberish mumkin.
Javob
state mo'rt (navigatsiya darsi): yangi tabda, nusxalangan havolada va ba'zi qayta yuklashlarda yo'qoladi. Kirish sahifasini foydalanuvchi parolni tiklash xatidan, boshqa tabdan ham ochishi mumkin. ?keyin= esa manzilning bir qismi — sahifa yangilansa ham, havola boshqa joyda ochilsa ham saqlanadi. Bir narsa uchun to'lov: qiymatni har kim o'zgartira oladi, shuning uchun tekshiriladi.
2.3 Kirgan foydalanuvchi /kirish ni ochsa
Teskari holat: Malika allaqachon kirgan, lekin xatcho'pdan /kirish ni ochdi. Unga forma ko'rsatish ma'nosiz — ?keyin= dagi (yoki bosh) sahifaga yuboriladi. vazifalar dagi KirishSahifasi aynan shunday (kanondan aynan):
// KirishSahifasi.tsx — "/kirish" (19/#07). Kirgan bo'lsa (shu tabda
// forma orqali yoki BOSHQA tabda) — ?keyin= dagi sahifaga, qaytish
// tarixda qolmasin (replace)
export function KirishSahifasi() {
const { foydalanuvchi } = useAuth();
const [parametrlar] = useSearchParams();
if (foydalanuvchi !== null) {
return (
<Navigate to={keyingiManzil(parametrlar.get("keyin"))} replace />
);
}
return (
<>
<title>Kirish — Vazifalar</title>
<KirishFormasi />
</>
);
}Qiziq tomoni: forma kirgandan keyin o'zi navigate chaqirmaydi. Auth holati o'zgardi → KirishSahifasi qayta chizildi → foydalanuvchi !== null → <Navigate>. Boshqa tabda kirilsa ham shu yo'l ishlaydi: BroadcastChannel xabari keldi, holat o'zgardi, sahifa o'zi yo'naltirdi. Bitta joyda bitta qoida.
3. Miltillash: "hali bilmayman" holati
3.1 Muammo
Haqiqiy ilovada "kirganmi?" degan savolga darhol javob yo'q. Sessiya serverda: sahifa ochilganda GET /api/men so'raladi, javob 200 ms da keladi. Shu 200 ms da user — null. Qorovul "kirmagan" deb /kirish ga yuboradi, keyin javob keladi — "kirgan ekan". Natija: kirish formasi bir zum ko'rinadi va yo'qoladi (yoki foydalanuvchi umuman kirish sahifasida qolib ketadi). Bu miltillash (flicker).
Davosi — uchinchi holat. null ikki ma'noni bildirmasin: "kirmagan" va "hali bilmayman" alohida bo'lsin:
import { createContext, use, useEffect, useState } from "react";
import {
createMemoryRouter,
Navigate,
Outlet,
RouterProvider,
} from "react-router";
const AuthContext = createContext(null);
// Soxta server: sessiya bor, javob 200 ms da
const fetchMe = () =>
new Promise((resolve) => setTimeout(() => resolve("Malika"), 200));
function RequireAuth() {
const { status } = use(AuthContext);
if (status === "tekshirilmoqda") {
return <p role="status">Tekshirilmoqda…</p>;
}
if (status === "mehmon") {
return <Navigate to="/kirish" replace />;
}
return <Outlet />;
}
const router = createMemoryRouter(
[
{ path: "/kirish", element: <p>Kirish formasi</p> },
{
element: <RequireAuth />,
children: [{ path: "/kabinet", element: <p>Kabinet</p> }],
},
],
{ initialEntries: ["/kabinet"] },
);
export default function App() {
const [status, setStatus] = useState("tekshirilmoqda");
useEffect(() => {
fetchMe().then((name) => setStatus(name ? "kirgan" : "mehmon"));
}, []);
return (
<AuthContext value={{ status }}>
<RouterProvider router={router} />
</AuthContext>
);
}Ekranda:
Kabinet200 ms davomida — "Tekshirilmoqda…", keyin to'g'ridan kabinet. Kirish formasi umuman ko'rinmadi. Tur nuqtai nazaridan bu discriminated union: "tekshirilmoqda" | "mehmon" | "kirgan". Har holat aniq, null ning ikki ma'nosi yo'q.
vazifalar da bu muammo yo'q: soxta auth ismni localStorage dan sinxron o'qiydi, birinchi chizishdayoq javob tayyor. Lekin haqiqiy auth (28-qism) bilan shu uch holat kerak bo'ladi.
3.2 <Navigate> va bo'sh birinchi kadr
Miltillashning ikkinchi turini kanon o'lchab topdi. Navigatsiya darsidagi ogohlantirishni eslang: <Navigate> manzilni effektda almashtiradi. Ya'ni birinchi kadrda u hech narsa chizmaydi, manzil keyin almashadi, kirish sahifasi esa undan keyin chiziladi. Kanon o'lchovi: sahifa yuklanganda kirish formasi yo'q edi — bir kadr bo'sh ekran.
Kanon yechimi — yo'naltirish bilan birga formani shu kadrning o'zida chizish (kanondan aynan):
export function Himoyalangan() {
const { foydalanuvchi } = useAuth();
const { pathname, search } = useLocation();
// Layout (Qobiq) konteksti shu marshrut orqali bolalarga o'tadi:
// Outlet konteksti faqat bevosita bolaga yetadi
const kontekst = useOutletContext();
if (foydalanuvchi === null) {
// replace: Orqaga himoyalangan sahifaga qaytib, yana shu yerga
// otilmasin. <Navigate> manzilni EFFEKTDA almashtiradi —
// birinchi kadr bo'sh qolardi (o'lchandi: sahifa yuklanganda
// forma yo'q edi); forma shu kadrdayoq ko'rinsin
return (
<>
<Navigate to={kirishManzili(pathname + search)} replace />
<KirishFormasi />
</>
);
}
return <Outlet context={kontekst} />;
}Birinchi kadrda: forma ko'rinadi, manzil hali /statistika. Effekt ishlaydi: manzil /kirish?keyin=… ga almashadi, endi KirishSahifasi o'sha formani chizadi. Foydalanuvchi uchun ekran o'zgarmaydi — bo'sh kadr yo'q.
Ikki mayda, lekin muhim detal:
useOutletContext()—Qobiqning konteksti (sarlavhaRef) qorovul orqali bolalarga qayta uzatiladi. 03-darsdagi tuzoq: Outlet konteksti faqat bevosita bolaga yetadi.KirishFormasida fokus endiuseLayoutEffectbilan (useLayoutEffect): birinchi chizishdayoq, bo'yashdan oldin fokus ism maydonida. Bobur (faqat klaviatura) sahifa ochilishi bilan yozishni boshlay oladi.
4. Rollar: kim nimani ko'radi
4.1 Rol — qo'shimcha shart
«Bahor» da uch xil odam: mehmon (kirmagan), oshpaz va admin (Jasur aka). Oshpaz buyurtmalarni ko'radi, narxlarni faqat admin o'zgartiradi. Rolga asoslangan kirish (role-based access control, RBAC) — sahifa va tugmalar foydalanuvchining roliga qarab ochiladi.
Qorovul ikkinchi shartni oladi: "kirganmi?" dan keyin "roli mosmi?". Mos kelmasa — /kirish emas, 403 ("ruxsat yo'q"): foydalanuvchi kirgan, qayta kirish yordam bermaydi. Menyu ham rolga qarab:
import { createContext, use, useState } from "react";
import {
createMemoryRouter,
Link,
Navigate,
Outlet,
RouterProvider,
} from "react-router";
const AuthContext = createContext(null);
const USERS = {
ali: { name: "Ali", roles: ["oshpaz"] },
jasur: { name: "Jasur aka", roles: ["oshpaz", "admin"] },
};
const can = (user, role) => user?.roles.includes(role) ?? false;
function RequireRole({ role }) {
const { user } = use(AuthContext);
if (user === null) return <Navigate to="/kirish" replace />;
if (!can(user, role)) return <p>403: bu sahifa {role} uchun</p>;
return <Outlet />;
}
function Shell() {
const { user, setUser } = use(AuthContext);
return (
<div>
<button onClick={() => setUser(USERS.ali)}>
Ali (oshpaz)
</button>{" "}
<button onClick={() => setUser(USERS.jasur)}>
Jasur aka (admin)
</button>
<nav>
<Link to="/buyurtmalar">Buyurtmalar</Link>
{/* menyuda — faqat ruxsat borlari */}
{can(user, "admin") && <Link to="/narxlar"> Narxlar</Link>}
</nav>
<Outlet />
</div>
);
}
const router = createMemoryRouter([
{
path: "/",
element: <Shell />,
children: [
{ path: "kirish", element: <p>Kirish formasi</p> },
{
element: <RequireRole role="oshpaz" />,
children: [
{ path: "buyurtmalar", element: <p>Buyurtmalar: 3 ta</p> },
],
},
{
element: <RequireRole role="admin" />,
children: [
{ path: "narxlar", element: <p>Narxlar: osh 35 000</p> },
],
},
],
},
]);
export default function App() {
const [user, setUser] = useState(null);
return (
<AuthContext value={{ user, setUser }}>
<RouterProvider router={router} />
</AuthContext>
);
}Ekranda:
Ali (oshpaz) Jasur aka (admin)
Buyurtmalar Narxlar
Narxlar: osh 35 000Harakat qatori: Ali tanlandi — menyuda faqat «Buyurtmalar», u ochildi (ekran qadami tekshirdi). «Narxlar» ni Ali bosa olmaydi: havolaning o'zi yo'q. Jasur aka tanlangach «Narxlar» paydo bo'ldi va sahifa ochildi. Admin uchun rol tekshiruvi (RBAC) serverda ham takrorlanadi.
4.2 Menyuni yashirish — himoya emas
Ali menyuda «Narxlar» ni ko'rmaydi. Lekin manzil satriga /narxlar ni o'zi yozishi mumkin. Shuning uchun ikkala qatlam kerak:
| Qatlam | Nima qiladi | Nimadan himoya qilmaydi |
|---|---|---|
menyu (can(user, "admin") &&) |
ortiqcha tugma ko'rsatmaydi | qo'lda yozilgan manzildan |
marshrut (RequireRole) |
sahifani ochmaydi (403) | brauzer kodini o'zgartirishdan |
| server | ma'lumotni bermaydi, o'zgartirishga yo'l qo'ymaydi | — bu oxirgi qatlam |
Rol tekshiruvini bitta funksiyaga (can) yig'ish — keyin "admin oshpaz ishini ham qila oladi" kabi qoidani bitta joyda o'zgartirasiz. Katta ilovalarda rol o'rniga ruxsatlar (permissions) tekshiriladi: can(user, "narx:tahrir"). Rol — ruxsatlar to'plami.
Diqqat:
vazifalarkanonida rollar yo'q: soxta auth'da faqat ism bor, rolni beradigan server yo'q. Rolni brauzerda o'zimiz "o'ylab topsak", bu o'rganish uchun ham noto'g'ri namuna bo'lardi. Rollar haqiqiy auth bilan birga — 28-qismda. Yuqoridagi naqsh — o'shanda ishlatiladigan tuzilma.
5. Loader'da ham tekshirish
5.1 Loader qorovuldan oldin ishlaydi
Data mode darsidan bilasiz: router sahifani chizishdan oldin loader'larni chaqiradi. Qorovul esa komponent — u loader tugagandan keyin chiziladi. Demak, kirmagan foydalanuvchi /vazifa/1 ni ochsa, loader serverga so'rov yuboradi, keyin qorovul uni /kirish ga otadi. Ko'ramiz:
import {
createMemoryRouter,
Navigate,
Outlet,
RouterProvider,
useLoaderData,
} from "react-router";
const user = null; // kirmagan
function RequireAuth() {
if (user === null) return <Navigate to="/kirish" replace />;
return <Outlet />;
}
function Task() {
return <p>{useLoaderData()}</p>;
}
const router = createMemoryRouter(
[
{ path: "/kirish", element: <p>Kirish formasi</p> },
{
element: <RequireAuth />,
children: [
{
path: "/vazifa/:id",
element: <Task />,
loader: ({ params }) => {
console.log("serverga so'rov: vazifa", params.id);
return "Non olish";
},
},
],
},
],
{ initialEntries: ["/vazifa/1"] },
);
export default function App() {
return <RouterProvider router={router} />;
}Konsolda:
serverga so'rov: vazifa 1Ekranda:
Kirish formasiFoydalanuvchi kirish sahifasini ko'rdi — lekin so'rov ketib bo'ldi. Haqiqiy serverda u 401 qaytaradi (zarar yo'q), lekin trafik behuda, server logida xatolar, ba'zan konsolda qizil qator. Kanon e2e tekshiruvi aynan shuni o'lchaydi: kirmagan /vazifa/1 — serverga so'rovlar ro'yxati bo'sh bo'lishi kerak.
5.2 Davosi: loader o'zi /kirish ga yo'naltiradi — replace() bilan
Loader hook ishlata olmaydi (u komponent emas), lekin auth holati React'dan tashqarida ham bor — localStorage da. Loader uni o'qiydi va yo'naltirish javobini otadi (throw). Qaysi javobni? Data mode darsida ikkalasini ko'rgansiz: redirect() va replace(). Farqi — tarixda: redirect() yangi yozuv qo'shadi (push), replace() joriy yozuvni almashtiradi.
Kanon avval redirect() yozgan edi. Kanon tekshiruvi buni haqiqiy Chrome'da o'lchadi: kirmagan foydalanuvchi /vazifa/1 ni yangi tabda to'g'ridan ochadi, /kirish chiqadi, keyin brauzerning «Orqaga» tugmasi. Tab about:blank dan boshlanadi (history.length 1). Natija (natijalar/07-kirish-replace.txt):
| Qadam | redirect() (push) |
replace() |
|---|---|---|
/vazifa/1 ochildi → loader |
/kirish?keyin=%2Fvazifa%2F1, tarix 3 |
/kirish?keyin=%2Fvazifa%2F1, tarix 2 |
| «Orqaga» 1 | /vazifa/1 → loader → yana /kirish |
about:blank — tabdan chiqdi |
| «Orqaga» 2, 3, 4 | har safar yana /kirish |
— |
Nima bo'ldi? React Router birinchi yuklanishdagi loader yo'naltirishini ham push qiladi. Tarixda /vazifa/1 yozuvi qoladi. «Orqaga» uni ochadi, loader yana /kirish ga otadi — foydalanuvchi tabdan «Orqaga» bilan chiqa olmaydi. replace() bilan tarix toza: «Orqaga» oldingi saytga olib ketadi. Qorovuldagi <Navigate replace> (/statistika) ham aynan shunday ishlaydi (o'sha o'lchov). Qoida bitta: yo'naltirish — replace, u komponentda bo'ladimi yoki loader'da.
Kanon kodi (aynan; izoh qatorlari qisqa bo'lingan):
// 19/#07: loader komponentdan (Himoyalangan) OLDIN ishlaydi —
// kirmagan foydalanuvchi uchun ham serverga so'rov ketardi
// (o'lchandi). Shuning uchun o'zi ham tekshiradi va /kirish ga
// yo'naltiradi. replace(), redirect() emas: redirect yozuv
// QO'SHADI — «Orqaga» himoyalangan manzilga, loader yana /kirish
// ga — tabdan chiqib bo'lmasdi (o'lchandi)
function kirganmi(request: Request): void {
if (foydalanuvchiniOqi(foydalanuvchiMatni()) === null) {
const { pathname, search } = new URL(request.url);
throw replace(kirishManzili(pathname + search));
}
}
export async function vazifaLoader({
params,
request,
}: LoaderFunctionArgs): Promise<VazifaMalumoti> {
kirganmi(request);
const raqam = raqaminiOl(params.id);
// …request.url — loader'ga kelgan to'liq manzil, undan ?keyin= uchun yo'l olinadi. foydalanuvchiniOqi — 18-qismdagi o'sha funksiya: localStorage dagi matnni tekshirib o'qiydi. kirishManzili — qorovul bilan umumiy funksiya: ikkala joy bir xil ?keyin= yasaydi.
5.3 Nega router middleware emas?
React Router 8 da middleware bor — marshrutga middleware: [fn] beriladi. U shu marshrut va bolalarining hamma loader'laridan oldin ishlaydi. Himoya uchun ideal ko'rinadi: bitta funksiya, hamma loader himoyalangan.
// O'LCHANDI, KANONGA KIRMADI (natijalar/07-middleware.txt)
const authMiddleware = () => {
if (foydalanuvchiniOqi(foydalanuvchiMatni()) === null) {
throw redirect("/kirish");
}
};
// yo'lsiz layout: { middleware: [authMiddleware], children: [...] }Kanon sinab ko'rdi (natijalar/07-middleware.txt, aynan):
kirgan=true /: initialized darhol=false, joy=/
10 ms dan keyin: initialized=true, joy=/Middleware bo'lsa, ro'yxat sahifasi (/, loader'i yo'q) ham darhol tayyor emas: initialized=false. Router middleware'ni asinxron kutadi, birinchi kadrda esa hydrateFallback ("Yuklanmoqda…") chiqadi. Data mode darsida ro'yxat sahifasidan loader'ni aynan shu sababli olib tashlagan edik — u keshdan darhol chizilsin (CLS 0). Kanon qarori: himoya — komponent qorovul (birinchi kadr sinxron) + loader'ga ega sahifada loader ichida tekshiruv.
Bu middleware'ni kamsitish emas. Server render'li ilovada (framework mode, 06-dars) middleware — himoyaning eng toza joyi: u serverda ishlaydi va birinchi kadr muammosi yo'q.
6. Vazifalar qadami: /kirish va himoyalangan layout (holat 07)
6.1 Nima o'zgaradi
git switch -c feat/himoyalangan-marshrutlarmain.tsxdan<Himoyalangan>o'rami olib tashlanadi — himoya endi marshrutlarda.marshrutlar.tsx:/kirish— ochiq sahifa; qolgan hammasi — yo'lsiz layoutHimoyalanganichida.Himoyalangan— «<Navigate>va bo'sh birinchi kadr» bo'limidagi kod;KirishSahifasi— «Kirgan foydalanuvchi/kirishni ochsa» bo'limidagi.keyingiManzil()—?keyin=tekshiruvi («Hujumchi nigohi»).vazifaLoader—kirganmi(request).Qobiq— kirmagan bo'lsa bo'limlar menyusi va skip link chizilmaydi.
6.2 Marshrutlar
marshrutlar.tsx (kanon, qisqartirilgan):
children: [
- // index — ota manzilining o'zi ("/")
- { index: true, element: <RoyxatSahifasi /> },
- …
+ // 19/#07: ochiq sahifa — kirish
+ { path: "kirish", element: <KirishSahifasi /> },
+ {
+ // Yo'lsiz (pathless) layout marshrut: manzilga hech narsa
+ // qo'shmaydi, faqat bolalarini o'raydi — kirmagan bo'lsa
+ // /kirish
+ element: <Himoyalangan />,
+ children: [
+ // index — ota manzilining o'zi ("/")
+ { index: true, element: <RoyxatSahifasi /> },
+ { path: "statistika", element: <StatistikaSahifasi /> },
+ { path: "sozlamalar", element: <SozlamalarSahifasi /> },
+ { path: "vazifa/:id", … },
+ ],
+ },
],Qobiq hamon eng tashqarida: /kirish ham uning ichida (foydalanuvchi menyusi, sarlavha). Lekin Qobiq endi useAuth() ni o'qiydi va kirmagan bo'lsa bo'limlar menyusini chizmaydi — kirish sahifasida «Statistika» havolasi ma'nosiz.
Qobiq.tsx dagi o'zgarish mohiyati (kanon):
+ // 19/#07: kirmagan — bo'limlar menyusi yo'q (hammasi
+ // himoyalangan)
+ const kirgan = useAuth().foydalanuvchi !== null;
...
- <nav className="bolimlar" aria-label="Bo'limlar">
+ {kirgan && (
+ <nav className="bolimlar" aria-label="Bo'limlar">6.3 Tekshiruvlar
git add -A
git commitCommit (kanon): feat: /kirish, Himoyalangan layout marshrut, ?keyin= (open redirect himoyasi).
npm test— node:test 183 (+1:keyingiManzil, olti yomon qiymat bilan), Vitest 56 (+1: loader kirmaganda/kirishga).lint,format:check,tip,knip—exit 0.- Brauzer: 159/159, 72/72, 19-qism — 46/46. Yangi sakkiztasi (aynan):
✅ kirmagan: /statistika → /kirish?keyin=..., menyu yo'q, fokus ismda: ["/kirish?keyin=%2Fstatistika%3Fx%3D1",0,"kirish-ism"]
✅ kirdi: o'sha sahifaga qaytdi (replace — tarix o'smadi): ["/statistika?x=1",0]
✅ kirgan /kirish ni ochsa — ro'yxatga: "/"
✅ kirmagan /vazifa/1: loader serverga so'ramadi, /kirish ga: [[],"/kirish?keyin=%2Fvazifa%2F1"]
✅ open redirect yo'q: ?keyin=//yomon.example → ilova ichida «/»: [true,"/"]
✅ himoya: CSP buzilishi yo'q: []
✅ kirmagan /vazifa/1 yangi tabda: tarix 2 yozuv, «Orqaga» — tabdan chiqdi (/kirish ga qaytmadi): [2,"about:blank"]
✅ konsolda xato yo'q: []Birinchi qatorda uchta da'vo birga: manzil ?keyin= bilan (%3F — ?, %3D — =, so'rov qatori ham saqlangan), bo'limlar menyusi 0 ta, fokus ism maydonida. Ikkinchisi — kirgach /statistika?x=1 ga qaytdi, tarix uzunligi o'zgarmadi. To'rtinchisi — «Loader'da ham tekshirish» bo'limining isboti: serverga so'rovlar ro'yxati bo'sh ([]). Yettinchisi — replace() ning isboti: yangi tabda tarix 2 yozuv (about:blank va /kirish), «Orqaga» — about:blank. redirect() bilan shu tekshiruv [3,"/kirish?keyin=%2Fvazifa%2F1"] bilan yiqiladi. Diff (holat 05 dan): 12 fayl, +230 −70.
7. Hujumchi nigohi
Ochiq yo'naltirish (open redirect). «?keyin= — qaytish manzili» bo'limidagi kodni hujumchi ko'zi bilan o'qing. Kirish sahifasi URL'dagi qiymatni tekshirmasdan navigate ga beradi. Hujumchi Telegram'da havola tarqatadi:
https://sizning-sayt.example/kirish?keyin=https://bahor-login.example/parol
Domen — sizniki, odam ishonadi. Kiradi — va brauzer uni begona saytga olib ketadi. U yerda sizning saytingizga o'xshash sahifa "sessiya tugadi, parolni qayta kiriting" deydi. navigate("https://…") to'liq manzilni ham qabul qiladi, //bahor-login.example (protokolsiz) ham — brauzer uni boshqa domen deb tushunadi.
Himoya — "faqat shu ilova ichidagi yo'l" qoidasi (kanondan aynan):
function keyingiManzil(keyin) {
if (
keyin === null ||
!keyin.startsWith("/") ||
/^\/[/\\]/.test(keyin)
) {
return "/";
}
return keyin;
}
for (const keyin of [
"/vazifa/2?x=1",
null,
"https://yomon.example",
"//yomon.example/x",
"/\\yomon.example",
"javascript:alert(1)",
]) {
console.log(JSON.stringify(keyin), "→", keyingiManzil(keyin));
}Konsolda:
"/vazifa/2?x=1" → /vazifa/2?x=1
null → /
"https://yomon.example" → /
"//yomon.example/x" → /
"/\\yomon.example" → /
"javascript:alert(1)" → /(Kanonda funksiya TypeScript'da, keyin: string | null bilan; bu yerda tiplarsiz.) Uch qoida:
/bilan boshlanmasa (https://…,javascript:…) — rad.//bilan boshlansa — rad: bu "protokolga nisbiy" manzil, boshqa domen./\bilan boshlansa — rad: brauzerlar\ni/deb o'qiydi,/\yomon.exampleham boshqa domen.
Shubhali hamma narsa — /. Foydalanuvchi bosh sahifaga tushadi, xavf yo'q. Kanonda bu olti qiymat — node:test da, brauzer e2e'da ham ?keyin=//yomon.example sinaladi.
Maslahat: Ruxsat ro'yxati (allowlist) — "nima mumkin" ni aniq aytish — har doim taqiq ro'yxatidan (blocklist) ishonchliroq.
keyingiManzil"httpsbo'lmasin" demaydi, "faqat/xko'rinishi" deydi: hujumchi o'ylab topadigan yangi hiyla (HTTPS:,%2F%2F) ham avtomatik rad etiladi.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| kirgandan keyin «Orqaga» — yana kirish sahifasi | yo'naltirish replace siz |
<Navigate replace>, navigate(…, { replace: true }) |
| kirish formasi bir zum miltillaydi | "hali bilmayman" ham null |
uch holat: tekshirilmoqda, mehmon, kirgan |
| kirgandan keyin har doim bosh sahifa | qaytish manzili saqlanmagan | ?keyin= |
| foydalanuvchi begona saytga ketdi | ?keyin= tekshirilmagan |
keyingiManzil — faqat ichki yo'l |
| kirmagan foydalanuvchi uchun API so'rovi | loader qorovuldan oldin | loader'da throw replace(…) |
yangi tabda «Orqaga» — yana va yana /kirish |
loader'da redirect() (tarixga push) |
replace() — joriy yozuvni almashtiradi |
himoyalangan sahifada sarlavhaRef — undefined |
qorovul kontekstni uzatmadi | <Outlet context={useOutletContext()} /> |
| admin sahifasi qo'lda yozilgan manzil bilan ochildi | faqat menyu yashirilgan | marshrutda ham RequireRole |
9. Mashqlar
Mashqlar kurs/mashqlar/19/07-himoya/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qayerga tushadi?
keyingiManzil (Hujumchi nigohi bo'limidagi) qaytaradigan qiymatni yozing:
keyingiManzil("/statistika")→keyingiManzil("statistika")→keyingiManzil("//example.com")→keyingiManzil("/vazifa/2?x=1")→
Yechim
1 va 4 — / bilan boshlanadi, keyingi belgi / yoki \ emas: o'zi qaytadi (so'rov qatori ham). 2 — / siz nisbiy yo'l: rad, /. Nisbiy yo'l joriy manzilga qo'shilib, kutilmagan sahifaga olib borishi mumkin edi. 3 — protokolga nisbiy manzil, boshqa domen: rad.
2-mashq (o'rta): qorovulga ?keyin=
Quyidagi qorovul kirmagan foydalanuvchini /kirish ga yuboradi, lekin qaytish manzilini saqlamaydi. Kirgandan keyin har doim / ga tushiladi. Tuzating:
- Qorovul
?keyin=ga joriypathnameni yozsin (URLSearchParamsbilan). Loginkirgandan keyin?keyin=dagi manzilga o'tsin —safeNextfunksiyasi orqali (u tayyor,keyingiManzilning qisqa nusxasi).
Ishora: «?keyin= — qaytish manzili» bo'limi.
import { createContext, use, useState } from "react";
import {
createMemoryRouter,
Navigate,
Outlet,
RouterProvider,
useLocation,
useNavigate,
useSearchParams,
} from "react-router";
const AuthContext = createContext(null);
const safeNext = (v) =>
v !== null && v.startsWith("/") && !/^\/[/\\]/.test(v) ? v : "/";
function Shell() {
const { pathname } = useLocation();
return (
<div>
<p>Manzil: {pathname}</p>
<Outlet />
</div>
);
}
function RequireAuth() {
const { user } = use(AuthContext);
if (user === null) return <Navigate to="/kirish" replace />;
return <Outlet />;
}
function Login() {
const { setUser } = use(AuthContext);
const navigate = useNavigate();
return (
<button
onClick={() => {
setUser("Bobur");
navigate("/", { replace: true });
}}
>
Kirish
</button>
);
}
const router = createMemoryRouter(
[
{
element: <Shell />,
children: [
{ path: "/kirish", element: <Login /> },
{
element: <RequireAuth />,
children: [
{ path: "/", element: <p>Bosh sahifa</p> },
{ path: "/savat", element: <p>Savat: 2 ta osh</p> },
],
},
],
},
],
{ initialEntries: ["/savat"] },
);
export default function App() {
const [user, setUser] = useState(null);
return (
<AuthContext value={{ user, setUser }}>
<RouterProvider router={router} />
</AuthContext>
);
}Ekranda:
Manzil: /
Bosh sahifaYechim
import { createContext, use, useState } from "react";
import {
createMemoryRouter,
Navigate,
Outlet,
RouterProvider,
useLocation,
useNavigate,
useSearchParams,
} from "react-router";
const AuthContext = createContext(null);
const safeNext = (v) =>
v !== null && v.startsWith("/") && !/^\/[/\\]/.test(v) ? v : "/";
function Shell() {
const { pathname } = useLocation();
return (
<div>
<p>Manzil: {pathname}</p>
<Outlet />
</div>
);
}
function RequireAuth() {
const { user } = use(AuthContext);
const { pathname } = useLocation();
if (user === null) {
const back = new URLSearchParams({ keyin: pathname });
return <Navigate to={`/kirish?${back}`} replace />;
}
return <Outlet />;
}
function Login() {
const { setUser } = use(AuthContext);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
return (
<button
onClick={() => {
setUser("Bobur");
navigate(safeNext(searchParams.get("keyin")), {
replace: true,
});
}}
>
Kirish
</button>
);
}
const router = createMemoryRouter(
[
{
element: <Shell />,
children: [
{ path: "/kirish", element: <Login /> },
{
element: <RequireAuth />,
children: [
{ path: "/", element: <p>Bosh sahifa</p> },
{ path: "/savat", element: <p>Savat: 2 ta osh</p> },
],
},
],
},
],
{ initialEntries: ["/savat"] },
);
export default function App() {
const [user, setUser] = useState(null);
return (
<AuthContext value={{ user, setUser }}>
<RouterProvider router={router} />
</AuthContext>
);
}Ekranda:
Manzil: /savat
Savat: 2 ta oshIlova /savat dan boshlandi, qorovul /kirish?keyin=%2Fsavat ga yubordi (Shell faqat pathname ni yozadi, shuning uchun oraliq ekranda Manzil: /kirish). Kirgandan keyin — savat. safeNext siz searchParams.get("keyin") ni to'g'ridan berish — «Hujumchi nigohi» dagi teshik.
3-mashq (qiyin): admin sahifasi
«Bahor» ga ikki foydalanuvchi va ikki sahifa:
USERS: Malika —["kassir"], Jasur aka —["kassir", "admin"]. Ikki tugma ular sifatida "kiradi"./kassa—kassiruchun,/hisobot—adminuchun. Ruxsat bo'lmasa —<p>403: ruxsat yo'q</p>.- Menyuda «Hisobot» havolasi faqat admin'ga.
Ishora: «Rol — qo'shimcha shart» bo'limidagi can va RequireRole. Boshlang'ich kodda faqat ma'lumot.
import { createContext, use, useState } from "react";
import {
createMemoryRouter,
Link,
Navigate,
Outlet,
RouterProvider,
} from "react-router";
const AuthContext = createContext(null);
const USERS = {
malika: { name: "Malika", roles: ["kassir"] },
jasur: { name: "Jasur aka", roles: ["kassir", "admin"] },
};
// can, RequireRole, Shell, router, App — shu yergaYechim
import { createContext, use, useState } from "react";
import {
createMemoryRouter,
Link,
Navigate,
Outlet,
RouterProvider,
} from "react-router";
const AuthContext = createContext(null);
const USERS = {
malika: { name: "Malika", roles: ["kassir"] },
jasur: { name: "Jasur aka", roles: ["kassir", "admin"] },
};
const can = (user, role) => user?.roles.includes(role) ?? false;
function RequireRole({ role }) {
const { user } = use(AuthContext);
if (user === null) return <Navigate to="/" replace />;
if (!can(user, role)) return <p>403: ruxsat yo'q</p>;
return <Outlet />;
}
function Shell() {
const { user, setUser } = use(AuthContext);
return (
<div>
<button onClick={() => setUser(USERS.malika)}>
Malika
</button>{" "}
<button onClick={() => setUser(USERS.jasur)}>Jasur aka</button>
<p>Foydalanuvchi: {user?.name ?? "mehmon"}</p>
<nav>
<Link to="/kassa">Kassa</Link>
{can(user, "admin") && <Link to="/hisobot"> Hisobot</Link>}
</nav>
<Outlet />
</div>
);
}
const router = createMemoryRouter([
{
path: "/",
element: <Shell />,
children: [
{
element: <RequireRole role="kassir" />,
children: [{ path: "kassa", element: <p>Kassa: 12 chek</p> }],
},
{
element: <RequireRole role="admin" />,
children: [
{
path: "hisobot",
element: <p>Hisobot: 1 250 000 so'm</p>,
},
],
},
],
},
]);
export default function App() {
const [user, setUser] = useState(null);
return (
<AuthContext value={{ user, setUser }}>
<RouterProvider router={router} />
</AuthContext>
);
}Ekranda:
Malika Jasur aka
Foydalanuvchi: Jasur aka
Kassa Hisobot
Hisobot: 1 250 000 so'mMalika kassani ko'rdi, menyuda «Hisobot» yo'q edi. Jasur aka tanlangach havola paydo bo'ldi va hisobot ochildi. Sinab ko'ring: initialEntries: ["/hisobot"] qo'shib, Malika tugmasini bosing — "403: ruxsat yo'q". Menyuda yashirilgan sahifa qo'lda yozilgan manzil bilan ham ochilmaydi. can — bitta joyda: "admin hamma narsani qila oladi" qoidasini qo'shish uchun faqat uni o'zgartirasiz.
4-mashq: Vazifalar qadami
feat/himoyalangan-marshrutlar branch'ida «Vazifalar qadami» bo'limidagi oltita o'zgarishni qiling, keyingiManzil ga testlar (olti yomon qiymat) yozing. Keyin commit.
Yechim
Tekshiruv ro'yxati (npm run dev):
- «Chiqish» → manzil
/kirish, bo'limlar menyusi yo'q, fokus ism maydonida. - Chiqqan holda
/statistika?x=1ni oching →/kirish?keyin=%2Fstatistika%3Fx%3D1. Ism yozib kiring →/statistika?x=1. «Orqaga» kirish sahifasiga qaytarmaydi. - Kirgan holda
/kirish→ ro'yxat. - Chiqqan holda
/vazifa/1→ DevTools Network'da/vazifalarga so'rov yo'q. /kirish?keyin=//example.com→ kirgandan keyin ilovada,/da.- Ikkita tab: birida «Chiqish» — ikkinchisi ham
/kirish?keyin=…ga o'tadi. npm test— node 183, Vitest 56.
10. Real ishda
- Har SPA'da —
ProtectedRoute/RequireAuthva?redirect=deyarli har loyihada bor. Intervyuda "login'dan keyin foydalanuvchini qanday qaytarasiz va bu qanday xavfli bo'lishi mumkin?" degan savol ko'p beriladi. - Admin panellar — rollar va ruxsatlar (
can("buyurtma:ochir")), menyu va tugmalar ruxsatga qarab. CASL kabi kutubxonalar shu qoidalarni bir joyda saqlaydi. - Next.js — himoya
middleware.tsda yoki server komponentda, serverda (Auth.js darsida). Bugungi uch qatlam (menyu, marshrut, server) o'sha yerda ham. - Xavfsizlik auditlari — open redirect OWASP ro'yxatidagi klassik zaiflik; bug bounty dasturlarida tez-tez topiladi.
Xulosa
- Himoyalangan sahifalar — yo'lsiz layout (
element: <RequireAuth />,pathyo'q) ichida; yangi sahifa — bitta qator. - Kirmagan —
<Navigate to="/kirish?keyin=…" replace />; kirgach —?keyin=dagi manzilga,replacebilan. ?keyin=— foydalanuvchi kiritgan ma'lumot: faqat/bilan boshlanadigan,//va/\siz ichki yo'l (ochiq yo'naltirish).- Miltillashga qarshi — uch holat (
tekshirilmoqda/mehmon/kirgan); bo'sh kadrga qarshi —<Navigate>bilan birga forma. - Rollar: menyu (qulaylik) + marshrut (403) + server (haqiqiy himoya).
- Loader qorovuldan oldin ishlaydi — u o'zi
throw replace(…)qiladi (redirect()emas: yangi tabda «Orqaga» yana/kirishga tushardi — o'lchandi). Middleware o'lchandi: ro'yxatning birinchi kadrini kechiktiradi — kanonga kirmadi.
Keyingi dars: TanStack Router: to'liq type-safe routing — React Router'ga muqobil: yo'llar va parametrlar kompilyatsiya paytida tekshiriladi. vazifalar da o'lchab solishtiramiz.
Manbalar
- React Router: Navigate, useSearchParams, redirect — reactrouter.com/api/… (2026-10 holatiga, v8.4.0)
- React Router: Middleware — reactrouter.com/how-to/middleware
- OWASP: Unvalidated Redirects and Forwards Cheat Sheet — cheatsheetseries.owasp.org
- MDN: URLSearchParams — developer.mozilla.org
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!