Галоўная / Артыкулы / Зупніце вытэканне поль базы дадзеных: шар доступу да дадзеных для прыемакоў Next.js

Зупніце вытэканне поль базы дадзеных: шар доступу да дадзеных для прыемакоў Next.js

Дазвольце дакладнаць, як слой доступу да данных централізуе пераконтанні абонентаў і фільтрацыю поль у серверных компанентах Next.js, так што чуліватыя столбцы ніколі не падаюць у браузер прачынай.

1597 слоў

Компаненты сервера дазволяюць запрашваць даны з базы дадзеных працоўна з самай компаненты. Але ёсць проблема: любой об’ект, які вы перадаеце кліентскай компаненты, серыязуецца і адправляецца ў браузер, уключаючы поля, якія вы ніколі не хацелі паказваць. Слой доступу да дадзеных (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 коштаў аднае чытанне сесіі за кожны запит.
  • Ў функцыях DAL выкорыстоўваюце Promise.all для незалежных запыткаў.
  • Імпортуйце модуль server-only у кожны файл DAL, каб неправільны імпорт спануўваў процес будавання, а не перагляд безпекі.
  • Компаненты сервера спрыяюць простам доступу да базы дадзеных; шар доступу да дадзеных стараваецца, каб гэтая простата не ператворылася на вытаканне дадзеных.

    Спаднія матэрыялы