Вопрос или проблема
Я использую NextJS и Next 14.2.3, а также NextAuth 4.24.7 для создания сайта онлайн-курсов на сервере Vercel.
У меня большая проблема с сессией. Она часто пропадает (после 5 – 45 минут, иногда сразу после входа в систему), хотя я установил maxAge на 1 месяц, обратите внимание, что срок действия сессии правильно отображается в инструментах разработчика.
Эта проблема никогда не возникает на localhost, только в продакшене😥
/src/app/api/auth/[…nextauth]/authOption.ts
import { connectDatabase } from '@/config/database'
import UserModel, { IUser } from '@/models/UserModel'
import bcrypt from 'bcrypt'
// Модели: Пользователь
import '@/models/UserModel'
// Провайдеры
import CredentialsProvider from 'next-auth/providers/credentials'
import GitHubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import { SessionStrategy } from 'next-auth'
const authOptions = {
secret: process.env.NEXTAUTH_SECRET!,
session: {
strategy: 'jwt' as SessionStrategy,
maxAge: 30 * 24 * 60 * 60, // 30 дней
},
// debug: process.env.NODE_ENV === 'development',
providers: [
// GOOGLE
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
// GITHUB
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
// УЧЕТНЫЕ ДАННЫЕ
CredentialsProvider({
name: 'Данные для входа',
credentials: {
usernameOrEmail: { label: 'Имя пользователя или Email', type: 'text' },
password: { label: 'Пароль', type: 'password' },
},
async authorize(credentials: Record<'usernameOrEmail' | 'password', string> | undefined) {
console.log('- Учетные данные -')
// соединение с базой данных
await connectDatabase()
// проверяем, не пустые ли учетные данные
if (!credentials?.usernameOrEmail || !credentials?.password) {
return null
}
// получаем данные из учетных данных
const { usernameOrEmail, password } = credentials
// ищем пользователя в базе данных
const user: any = await UserModel.findOne({
$or: [{ email: usernameOrEmail.toLowerCase() }, { username: usernameOrEmail }],
}).lean()
// проверяем, существует ли пользователь в базе данных
if (!user) {
throw new Error('Email или пароль неверны!')
}
// проверяем, является ли пользователь локальным
if (user.authType !== 'local') {
throw new Error('Эта учетная запись подтверждена ' + user.authType)
}
// проверяем пароль
const isValidPassword = await bcrypt.compare(password, user.password)
if (!isValidPassword) {
// выкидываем ошибку в колбэк
throw new Error('Email или пароль неверны!')
}
const { avatar: image, ...otherDetails } = user
// возвращаем в колбэк сессии
return {
...otherDetails,
image,
name: user.firstName + ' ' + user.lastName,
}
},
}),
// ...добавьте провайдеров здесь
],
callbacks: {
async jwt({ token, user, trigger, session }: any) {
console.log('- JWT -', { token, user, trigger, session })
// Новый вход
if (user) {
const userDB: IUser | null = await UserModel.findOne({
email: user.email,
}).lean()
if (userDB) {
const { password, ...userDBWithoutPassword } = userDB
token = { ...token, ...userDBWithoutPassword }
}
}
if (trigger === 'update' && token._id) {
console.log('- Обновить токен -')
const userDB: IUser | null = await UserModel.findById(token._id).lean()
if (userDB) {
// исключаем пароль
const { password, ...userDBWithoutPassword } = userDB
return { ...token, ...userDBWithoutPassword }
}
}
return token
},
async session({ session, token }: any) {
console.log('- Сессия -')
session.user = token
return session
},
async signIn({ user, account, profile }: any) {
console.log('- Вход -')
try {
// соединение с базой данных
await connectDatabase()
if (account && account.provider != 'credentials') {
if (!user || !profile) {
return false
}
// получаем данные для аутентификации
const email = user.email
const avatar = user.image
let firstName: string = ''
let lastName: string = ''
if (account.provider === 'google') {
firstName = profile.given_name
lastName = profile.family_name
} else if (account.provider === 'github') {
firstName = profile.name
lastName=""
}
// ищем пользователя в базе данных для проверки существования
const existingUser: any = await UserModel.findOne({ email }).lean()
// проверяем, существует ли пользователь
if (existingUser) {
return true
}
// создаем нового пользователя с социальной информацией (проверенный email)
await UserModel.create({
email,
avatar,
firstName,
lastName,
authType: account.provider,
verifiedEmail: true,
})
}
return true
} catch (err: any) {
console.log(err)
return false
}
},
},
}
export default authOptions
/src/app/api/auth/[…nextauth]/route.ts
import NextAuth from 'next-auth'
// Модели: Пользователь
import '@/models/UserModel'
import authOptions from './authOptions'
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
/src/app/layouts.tsx
import StoreProvider from '@/libs/StoreProvider'
import type { Metadata } from 'next'
import { getServerSession } from 'next-auth'
import NextTopLoader from 'nextjs-toploader'
import { ReactNode } from 'react'
import { Toaster } from 'react-hot-toast'
import authOptions from './api/auth/[...nextauth]/authOptions'
import './globals.scss'
export const metadata: Metadata = {
title: 'Mona Edu',
description: 'Mona Edu - Непревзойденная платформа для онлайн-обучения в Вьетнаме',
icons: {
icon: ['/favicon.ico?v=4'],
apple: ['/apple-touch-icon.png?v=4'],
shortcut: ['/apple-touch-icon.png'],
},
manifest: '/site.webmanifest',
}
export default async function RootLayout({
children,
}: Readonly<{
children: ReactNode
}>) {
const session = await getServerSession(authOptions)
return (
<html lang='vi'>
<body suppressHydrationWarning={true}>
<StoreProvider session={session}>
{/* Toast */}
<Toaster
toastOptions={{
style: {
background: '#333',
color: '#fff',
},
}}
/>
{/* Top Loader */}
<NextTopLoader
color="#F7E360"
initialPosition={0.08}
crawlSpeed={200}
height={3}
crawl={true}
showSpinner={false}
easing='ease'
speed={200}
shadow='0 0 10px #F7E360,0 0 5px #F7E360'
zIndex={1600}
showAtBottom={false}
/>
{children}
</StoreProvider>
</body>
</html>
)
}
Пожалуйста, помогите мне! 😥😥😥
Я попытался
- сократить количество обновлений сессии из next-auth
- уменьшить размер cookie сессии, в настоящее время он составляет 3 – 4 КБ, но это не является проблемой
Я определил NEXTAUTH_URL, NEXTAUTH_SECRET в .env
Но все это бессмысленно. Проблема в Vercel?
Ответ или решение
Проблема, с которой вы сталкиваетесь, может быть вызвана несколькими факторами, связанными с конфигурацией и настройками вашего приложения Next.js и сервера Vercel. Давайте рассмотрим некоторые шаги и рекомендации, которые могут помочь вам решить эту проблему.
1. Проверка конфигурации переменных окружения
Убедитесь, что переменные окружения NEXTAUTH_URL
и NEXTAUTH_SECRET
корректно настроены и доступны в вашем производственном окружении. Эти переменные критически важны для корректной работы аутентификации и сессий.
Пример настройки в файле .env
:
NEXTAUTH_URL=https://your-production-url.com
NEXTAUTH_SECRET=your_secret_key
2. Конфигурация cookie
Убедитесь, что конфигурация cookie корректна. Для использования JWT вам нужно убедиться, что настройки соответствуют тому, как ваше приложение управляет сессиями.
Добавьте параметр cookies
в настройки NextAuth:
cookies: {
sessionToken: {
name: `next-auth.session-token`,
// Если у вас есть конкретные требования к безопасности, можно настроить домен и путь.
path: '/',
sameSite: 'lax', // или 'none' для кросс-доменных запросов
secure: process.env.NODE_ENV === 'production', // использовать secure cookie в производстве
},
}
3. Настройки Vercel
Проверьте настройки сессий и таймауты на стороне сервера Vercel. Возможно, есть ограничения на хранение сессий или время их жизни. Убедитесь, что среда выполнения не ограничивает вас по времени бездействия.
4. Подсчет различных данных в токене
С уменьшением размера токена могут возникнуть проблемы. Попробуйте исключить из токена лишние данные. Например, если вы добавляете данные пользователя в токен в функции jwt
, убедитесь, что они минимально необходимы. Убедитесь, что длина токена не превышает 4-8 КБ.
5. Обработка сессии
Убедитесь, что ваш session
callback не добавляет избыточные данные и чистит токен при каждом вызове. Например:
async session({ session, token }) {
session.user = {
id: token._id,
name: token.name,
image: token.image,
}
return session
}
6. Отладка
Включите отладку в вашем коде, чтобы более точно выяснить, на каком этапе происходит сбой. Например, поэкспериментируйте с добавлением логов на разных этапах в jwt
, session
и authorize
callbacks для отслеживания получения токенов.
7. Проверка сторонних библиотек
Иногда временные проблемы с сессиями могут быть связаны с тем, как библиотека next-auth
взаимодействует с вашим приложением. Убедитесь, что вы используете последние версии всех библиотек и следите за известными проблемами на GitHub.
Резюме
Подводя итог, вам следует убедиться, что правильно установлены переменные окружения, оптимизирована конфигурация cookie, правильно настроены параметры сессии и использованы минимальные данные в токенах. Поскольку проблема возникает только в производственной среде, уделяйте внимание не только коду приложения, но и среде Vercel, которая может иметь свои особенности работы и ограничения.
Если проблема сохраняется, можно рассмотреть возможность контакта с поддержкой Vercel или сообществом next-auth
, чтобы получить дополнительные рекомендации. Надеюсь, это поможет вам решить проблему!