Зупніце вытэканне поль базы дадзеных: шар доступу да дадзеных для прыемакоў Next.js
Дазвольце дакладнаць, як слой доступу да данных централізуе пераконтанні абонентаў і фільтрацыю поль у серверных компанентах Next.js, так што чуліватыя столбцы ніколі не падаюць у браузер прачынай.
Компаненты сервера дазволяюць запрашваць даны з базы дадзеных працоўна з самай компаненты. Але ёсць проблема: любой об’ект, які вы перадаеце кліентскай компаненты, серыязуецца і адправляецца ў браузер, уключаючы поля, якія вы ніколі не хацелі паказваць. Слой доступу да дадзеных (Data Access Layer, DAL) знаходзіцца межы компанентамі і базай дадзеных, таму аутэнтыкацыя, автарызацыя і фільтрацыя полей выконваюцца ў аднам месцы. Хутчэй будзе розказана, як структураваць такі слой, што ў яго мае быць і калі варта дагадваць дапаможныя файлы.
Што такое слой доступу да дадзеных
DAL — это спецыяльны папка, якая зберагае кожную запытку да базы дадзеных. Компаненты ніколі не вызываюць Prisma безпосередна; яны вызываюць такія функцыі, як getProfile, якія вяршаюць, што можа бачыць вызываючы код і якія поля будуць вярнуты. Можна супрацаваць з яйным як з пунктам контролю на шляху да інтерфейса: яны пераканальваюць, хто запытаўся, і адключаюць усё, чаго экран не патрэбуе. У дакументах Next.js пра безпеку дадзеных рэкамендуецца такі падход для новых проектаў.
Тыповая структура размешчае аблігаторы аутэнтыкацыі праза модулямі запытаў для кожнага домэна:
src/
data/
auth.ts # Authentication helpers
user.ts # User queries
posts.ts # Post queries
Правіло, якое дазволяе гэта працаваць, ёсць простым: ніч гэтакога, што знаходзіцца за межами data/, не імпортуе кліента базы дадзеных.
Як неарабізаваныя запыткі вывалююць дадзеныя
Безпосередняй запыт вяртае цэлую строчку. У прыводзі нижчэй компанент запрашвае дадзеныя аб корыстніку і перадае рэзультат безпосередна да карточкі:
// Without DAL - directly in a component
const user = await prisma.user.findUnique({ where: { id } })
return <ProfileCard user={user} />
Этот об’ект мае ў сабе кожны столбец: хэш пароля, адресу электранайта, номер тэлефона, внутраніяі ідэнтыфікаторы. Якщо ProfileCard ўжо ёсць Кліентскі компонент, весь гэты об’ект серыязуецца ў пакет даных сторункі, і кожны, у каго ачынутая вікно сеті, можа яго прачытаць, нават якщо карточка адображае толькі імя.
Рашэння — вярнуць спецыяльна створаны об’ект заместо суровага записа. Гэтая функцыя DAL выбірае тры поля, якія насправды викорыстоўваюцца у адзірку профіля:
// data/user.ts
export async function getProfile(id: string) {
const user = await prisma.user.findUnique({ where: { id } })
return {
name: user.name,
avatar: user.avatar,
bio: user.bio
}
}
Компонент вызывае getProfile(id) і атрымлівае захіщаны об’ект; хэш пароля застаецца на сервере. Таксама можна фільтрацыяваць у запытэ з select Prisma, і трэба караць ситуацыю, калі findUnique вяртае null, адколькі тады user.name выклекае аднойчыну.
Чаму безпека на рэвеніі компанентоў не працуе
Большая небяпека ў расліваючымся кодбэсе — гэта тое, што кожны компанент рэалізуе свае сабе правіла доступу, і яны пачынаюць адстаўаць адзін ад другога. Уявіце двух разработчыкаў над адной працою. Першы стварае сторунку з профілем на адной з функцый DAL, якая павінна выканаць пераканання ў аутентыкацыі (этот фрагмент павторае вышэйшую функцыю з фільтрацыяй і ўжо не мае пераканання; захіщаная версія прадстаўлена ў наступным раздзеле):
// data/user.ts
export async function getProfile(id: string) {
const user = await prisma.user.findUnique({ where: { id } })
return {
name: user.name,
avatar: user.avatar,
bio: user.bio
}
}
Другі разработчык стварае сторунку налашоўкаў і выканае запыты да Prisma безпосередна, чытаючы ідэнтыфікатор пользователя з URL і ніколі не пераканваючыся, хто ўвайшаў:
// pages/settings/page.tsx - Developer B wrote this
export default async function SettingsPage({ searchParams }) {
// No auth check!
const user = await prisma.user.findUnique({
where: { id: searchParams.id }
})
// Returns everything, including sensitive fields
return <Settings user={user} />
}
Зараз є два багі: няма аутентыкацыі, таму хто-небудзь, хто можа зменіць параметр id, можа завантажыць настройкі іншай особы, а ўсе даныя, уключаючы чутлівыя поля, падаюць у інтэрфейс. (Незважаючы на прыметку pages/, гэта сторанка App Router пад даменам app/). Калі логіка безпекі розтакана па разныя сторанкі, дапамога адной забытай пераконтроўкі ёсць достатня, і практыка аудыту должна пераглядзець кожны компонент, які выкарыстоўвае базу дадзеных.
Размешчэнне аутентыкацыі і фільтрацыі ў аднам месца
За дапамогою DAL кожны вызывальнік праходзіць чераз тыя ж пераконтроўкі. getCurrentUser чытае сесію і вяртае корыстніка або null; requireAuth перенаправляе на сторанку входу, калі ніхто не ўвайшаў. Обея функцыі абгорнутыя ў cache з React, таму паўтарныя вызывы ў межах адной запросу викорыстоўваюць першы рэзультат:
// data/auth.ts
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await getSession()
if (!session) return null
return session.user
})
export const requireAuth = cache(async () => {
const user = await getCurrentUser()
if (!user) redirect('/login')
return user
})
Не паведамлены элементы імпорту: getSession выкалічваецца з бібліятэкі аутантыкацыі, а redirect — з next/navigation.
getProfile тепер выкалічваецца толькі для заўважанага корыстніка і адкрывае электранадзор толькі власніку прафіля:
// data/user.ts
import 'server-only'
import { requireAuth } from './auth'
export async function getProfile(id: string) {
const viewer = await requireAuth()
const user = await prisma.user.findUnique({ where: { id } })
return {
name: user.name,
avatar: user.avatar,
email: viewer.id === user.id ? user.email : null
}
}
Кожны корыстнік атрымвае можлівасць аутантыкацыі і фільтрацыі безкоштовна; другі разработчык не можа прыхіліцца ад пераканальнага календара, які ўбудованы ў функцыю. Згадаць, што requireAuth адпавядае толькі на запыт "Чы ў каго-небудзь ёсць адпаведны прыступ?". Магчымасць гэтага корыстніка рэдагаваць пасляведамленне — це аўтарызацыя, і яна таксама павінна знаходзіцца ў DAL. Чытайце болей дакладна пра гэты разлік у стацыі пра тое, дзе атрыбуецца аутантыкацыя і аўтарызацыя.
Сторанкі становяцца простымі. Яны запрашают данні через DAL і адрасаваюць ўвесь матэрыял:
// Any component - simple and secure
export default async function ProfilePage({ params }) {
const profile = await getProfile(params.id)
return <Profile profile={profile} />
}
Компанента не мае коду безпекі, і аудыт павярэдзяе перагляд data/, а не сотні компанет. У новейшых версіях Next.js params ёстся Promise, таму спачатку трэба await яго.
Зялэнне даных паралельна з функцыяй DAL
Дашборды частаўкае аднойчыны незалежныя запиты аднаго за іншым:
// Sequential fetching - slow
export default async function Dashboard() {
const user = await prisma.user.findUnique({ where: { id } })
const posts = await prisma.post.findMany({ where: { authorId: id } })
const stats = await prisma.stats.findFirst({ where: { userId: id } })
return <DashboardUI user={user} posts={posts} stats={stats} />
}
Кожны await чакае на пылейшы, таму тры запиты по 100 мс займаюць апошнія прыблізна 300 мс. Паколькі яны незалежныя, гэтая функцыя DAL запускае іх аднойчыны за дапамою Promise.all і прысвячае рэзультаты неабходным полям:
// data/dashboard.ts
export async function getDashboardData(userId: string) {
const [user, posts, stats] = await Promise.all([
prisma.user.findUnique({ where: { id: userId } }),
prisma.post.findMany({ where: { authorId: userId } }),
prisma.stats.findFirst({ where: { userId } })
])
return {
user: { name: user.name, avatar: user.avatar },
posts: posts.map(p => ({ id: p.id, title: p.title })),
stats: { views: stats.views, followers: stats.followers }
}
}
Абсалютны часы знижаюцца да рэгістру самага медленнага запытку, адпраўна 100 мс. Карыстаючыся Promise.all, падбіраецца эфект, але функцыя для обробкі дадзеных прабачае цэйтовую сумяшанасць і дазволяе выконваць фільтрацыю разам з запошчаннем дадзеных. Залежныя запыткі все рава мусяць чакаць. Таксама, калі requireAuth выкарыстоўвае cache(), многі компаненты можаць вызываць функцыі DAL, якія пераканаліся ў аднойчынных правах, калі сесія чытваецца толькі аднойчы на кожны запыт.
Захаванне шару толькі для сервера
Кожны файл у DAL трэба пачынаць з такога імпорту:
import 'server-only'
Пакет server-only не дазволяе стварэнню проекту, калі модуль, які яго імпортуе, опынаецца ў коде кліента; таму функцыя DAL, якая па чыяй-небудзь памылцы імпортуецца ў кліентскі компанент, стварае ясную памылку ў перадпродажным режыме. Гэта правіло стае часткай правілаў, якія аплываюць за дапамогою інструментаў.
Што належыць унутрь DAL
Кожная функцыя DAL должна спецыязавацца на чатыры задачы:
- Автанацыя: паказаць, чы ёсць увійшоўны корыстнік, выкарыстоўваючы запам’ятаваныя дапаможнікі, каб пераказ чыстаўся адна раз у кожны запит.
- Автарызацыя: паказаць, чы гэты корыстнік мае право на доступ да конкретнага запису, напрыклад, чы ён можа пераглядаць прафіль або рэдагаваць пастку.
- Фільтрацыя: вярнуць толькі тые поля, якія адображае экран. Якщо вы не впевнены, чы патрэбна якае-небудзь поле, залишыце яго без змян.
- Секрэты: толькі DAL должна чытаць чутлівую настройку, такую як
DATABASE_URL, чым деталі з’яўлення захоўваюцца парадульна ад коду компаненты.
Дзеянні сервера должны выкарыстоўваць тыя ж функціі, таму запісы праходзяць тыя ж перакананні, што і чытанні; дывіцеся автарызацію ў кожным дзеянні сервера.
Калі DAL ўжо неабходны
Ўжывайце яго для всьога, што карыстуецца данымі корыстніка, аутентыкацыяй чы роўнасцю інформацыі з высокай ступенем конфідэнцыйнасці; у прыметным дапрыемку з аблікамі гэта базовая практыка, а не дадатковая. Адмовіцеся ад яго толькі для прынцыпатных праектаў чы статычных сайтаў без даных корыстніка, і нават там вярненне явных об’ектаў — это дешавы звычак, які варта падтрымваць.
Ключовыя выводы
- Што бы ні дасягало кліентскага компонента, гэта дасягае браузера, таму ніколи не перадавайце сырыя рядкі базы даных через гэтыя межы.
- Перадавайце кожны запит через папку
data/, якая карыстуецца аутентыкацыяй, автарызацыяй і выборам полей.
cache бібліятэкі React, каб кожны запыт DAL коштаў аднае чытанне сесіі за кожны запит.Promise.all для незалежных запыткаў.server-only у кожны файл DAL, каб неправільны імпорт спануўваў процес будавання, а не перагляд безпекі.Компаненты сервера спрыяюць простам доступу да базы дадзеных; шар доступу да дадзеных стараваецца, каб гэтая простата не ператворылася на вытаканне дадзеных.
Спаднія матэрыялы
- Раздзелэнне шароў домэна, дадзеных і UI у кодбазе Next.js App Router — аналіз прыкладу Pokédex, які паказвае, як раздзеліць прыложэнне Next.js App Router на шары домэна, дадзеных і прыводу з адпаведным выкарыстоўваннем Prisma, Zod, аутэнтыкаціі через кукі і кэшавання.
- 20 адвансаванных шаблонаў Next.js 16 для архітектуры прыкладных програм высокага рангу — Аптакст пра дыяжэнне сервера як асновы, кэшаванне, стрімінг, PPR, паралельныя та перехопляючыя маршруты, а таксама іншыя шаблоны для стварэння масштабавальных прыкладных програм на Next.js 16.
- Аналіз пацелкі неудачы: Next.js, Remix і застарэлыя даны автарызаціі — Даследжэнне прыкладу таго, як баг з застарэлымі правамі вплывае на выбор между Next.js і Remix, а таксама як ацэніць фреймворкі за канстантныя змены, трымачы кэша і можлівасці дыбагавання.