Прежде всего, что такое SSR (рендеринг на стороне сервера)?
«Рендеринг на стороне сервера с помощью JavaScript библиотек, таких как React — это когда сервер возвращает готовую к рендерингу HTML страницу и JS скрипты, необходимые для того, чтобы сделать страницу интерактивной.» — источник
SSR имеет множество преимуществ, таких как улучшение SEO, улучшение производительности сайта, улучшение TTI, FCP и многое другое.
Во время SSR вы можете запросить свои или сторонние API, чтобы получить необходимые вам данные, а затем вы можете внедрить эти данные в ваши компоненты.
Этот процесс довольно прост, пока вы не дойдете до момента, когда вам нужно показать некоторые данные, основанные на запросе пользователя, запрашивающего страницу.
В этой статье я попытаюсь объяснить процесс аутентификации пользователя при использовании SSR с NextJS, Prisma и cookies.
Общий поток
Давайте разберем, что происходит, когда кто-то заходит на вашу страницу www.yourdomain.com
в браузере.
- Браузер посылает GET запрос на этот URL
- Сервер получает запрос
- Запрос перенаправляется на определенный обработчик страницы с помощью nextjs (в данном случае это будет файл pages/index.js)
- Обработчик страницы выполняет getServerSideProps, вставляет данные в перенаправленную страницу и возвращает HTML-вывод.
Мы должны понимать, что браузер автоматически отправляет этот первый запрос в потоке. Не существует способа выполнить какой-либо javascript до этого запроса или изменить какие-либо заголовки, отправленные браузером.
Поэтому нам необходимо использовать cookies в качестве механизма передачи маркера доступа для пользователя, так как они автоматически прикрепляются к каждому запросу, отправленному браузером.
Пример использования
В качестве примера для данной статьи мы рассмотрим ленту новостей с двумя типами пользователей: гостевые пользователи и зарегистрированные пользователи. Зарегистрированные пользователи делятся на две группы: подписанные и не подписанные.
У нас будет три страницы:
- Домашняя страница — Здесь будут перечислены все сообщения.
- Страница входа — Это страница, на которой пользователь может войти в систему.
- Страница новостей — Это страница, где пользователи могут увидеть более подробную информацию о конкретном сообщении.
На бэкенде у нас будут следующие маршруты
- GET /api/posts — Это маршрут, который вернет все посты.
- POST /api/users/login — Это маршрут, который создаст JWT-токен после успешного входа в систему.
- DELETE /api/users/logout — Это маршрут, который очистит cookie пользователя.
Пачкаем руки 🙂
В качестве клиента базы данных мы будем использовать prisma.
В качестве провайдера БД будем использовать SQLite, так как его проще всего запустить в работу.
Данные о зарегистрированных пользователях будут сохраняться в таблице User
, а данные о постах — в таблице Post
.
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid())
email String? @unique
password String
subscribed Boolean? @default(false)
}
model Post {
id String @id @default(uuid())
title String
content String
premium Boolean? @default(false)
}
In prisma/seeds there is a script that can seed the test data for you
Разбивка маршрута входа в систему:
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// only allow POST requests
handleRequestMethod(req, `POST`)
// get email and password from the request body
const { email, password } = req.body
// find the user with the email
const user = await prisma.user.findFirst({ where: { email } })
// compare the password with the hashed password
const isValid = await bcrypt.compare(password, user.password)
if (!user || !isValid) {
return res.status(401).json({ error: `Invalid credentials` })
}
// create jwt token
const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, {
expiresIn: `7d`,
})
// attach created token as cookie to the response header
return res
.setHeader(`Set-Cookie`, serialize(`token`, token, { path: `/` }))
.status(200)
.json(user)
}
Создадим вспомогательную функцию в папке lib.
Эта функция будет проверять токен пользователя и возвращать объект пользователя из DB, если токен действителен, или выдавать ошибку, если токен недействителен.
export const authenticate = (req) => {
// get token from cookies
const token = req.cookies.token
return new Promise((resolve, reject) => {
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
reject(new Error(`Unauthorized`))
} else {
prisma.user
.findUnique({
where: { id: decoded.sub },
select: {
id: true,
email: true,
subscribed: true,
},
})
.then((user) => {
if (user) {
return resolve(user)
} else {
reject(new Error(`Unauthorized`))
}
})
.catch(reject)
}
})
})
}
Теперь перейдем на главную страницу.
Внутри src/pages/index.tsx
у нас есть наш компонент React Home
и функция getServerSideProps
. getServerSideProps
выполняется каждый раз, когда кто-то делает запрос к этой странице, и результат передается компоненту Home
через props.
В нем мы пытаемся аутентифицировать текущего пользователя из запроса, просматривая JWT-токен из cookies.
Если нам удается аутентифицировать пользователя, мы сначала проверяем, подписан ли он или нет, чтобы знать, должен ли он иметь возможность видеть все сообщения или только бесплатные.
Если нам не удается аутентифицировать пользователя, мы возвращаем бесплатные посты для гостевых пользователей.
Также, вместе с данными о постах, getServerSideProps возвращает объект пользователя, чтобы мы могли отобразить пользовательский интерфейс, специфичный для этого пользователя, например, показать его email/статус подписки.)
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
let posts = []
let authUser = null
try {
const user = await authenticate(req)
authUser = user
if (user.subscribed) {
posts = await getAllPosts()
} else {
posts = await getFreePosts()
}
} catch (e) {
posts = await getFreePosts()
}
return {
props: {
posts,
user: authUser,
},
}
}
Логика аналогична для страницы Posts.
Просматривая JWT-токен из cookies, аутентифицируем пользователя и запрашиваем БД для получения конкретного поста.
Как выйти из системы?
Чтобы выйти из системы, нужно просто отправить запрос DELETE на конечную точку /api/users/logout и после этого обновить страницу. Cookie будет удалена, и сервер снова выполнит обработчик страницы, теперь уже без cookie.
Реализация маршрута выхода из системы:
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
handleRequestMethod(req, `DELETE`)
return res
.setHeader(`Set-Cookie`, serialize(`token`, ``, { path: `/`, maxAge: 0 }))
.status(200)
.json({ message: `Logged out` })
}
Обратите внимание, что этот код не готов к производству. Если вы ищете более готовое решение, вам стоит обратить внимание на NextAuth.
Исходный код этого проекта доступен на Github
Первоначально опубликовано на: https://www.danilothedev.com/