згенеровано ШІ
Привіт, хочу розібрати одну з популярних тем, яка залишає багато запитань у багатьох — як налаштувати авторизацію користувача через зовнішній сервер за допомогою NextJS. У цій статті ми розглянемо, як використовувати структуру Pages Router в NextJS, а також як організувати авторизацію (Authorization) та аутентифікацію (Authentication) через NodeJS/Express та Cookies. І все це без використання сторонніх бібліотек для авторизації.
(У цій статті не буде розглянуто, що таке cookie, jwt або axios interceptors, а лише описані використовувані методи та їхні причини. Стаття не призначена для початківців.)
Використовувані технології
Frontend: NextJS, Axios
Backend: NodeJS, Express, JWT
Backend (NodeJS/ExpressJS) операції
Припустимо, ви вже налаштували ваш Express сервер. Спершу визначимо middleware, а потім створимо необхідні API маршрути — три маршрути: login, refresh-token і logout.
Middleware
Цей middleware ми можемо додавати до тих маршрутів, для яких хочемо обмежити доступ за авторизацією. Він перевіряє cookies у запиті і здійснює необхідні дії.
export interface CustomRequest extends Request {
user?: JwtUser;
cookies: { [key: string]: string };
}
export const AuthMiddleware = async (
req: CustomRequest,
res: Response,
next: NextFunction
) => {
const token = req.cookies?.accessToken;
if (!token) {
return res
.status(401)
.json({ error: "Доступ заборонено. Токен не знайдено." });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtUser;
req.user = decoded;
return next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res
.status(401)
.json({ error: "Термін дії токену закінчився." });
}
}
};
Lütfen tekrar giriş yapın." });
}
return res.status(400).json({ error: "Geçersiz token." });
}
};
API для входу
const login = async (req: Request, res: Response) => {
const { email, password } = req.body;
try {
const { user } = await _login(email, password);
if (!user) throw new Error("Користувача не знайдено.");
const accessToken = generateToken({ id: user.id, email: user.email });
const refreshToken = generateToken(
{ id: user.id, email: user.email },
"refresh"
);
res.cookie("accessToken", accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "none",
maxAge: 15 * 60 * 1000, // 15 хвилин
});
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "none",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 днів
});
res.status(200).json({
message: "Увійшли успішно!",
user: {
id: user.id,
email: user.email,
name: user.name,
},
});
} catch (error: any) {
return res.status(500).json({
error: error.message,
});
}
};
Ми повертаємо дані користувача та токени в cookies після успішного входу.
API для Refresh-Token
const refreshToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(400).json({ error: "Refresh token є обов'язковим." });
}
const { id, email } = verifyToken(refreshToken);
const accessToken = generateToken({ id, email });
const newRefreshToken = generateToken({ id, email }, "refresh");
res.cookie("accessToken", accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "none",
maxAge: 15 * 60 * 1000, // 15 хвилин
});
res.cookie("refreshToken", newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "none",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 днів
});
res.status(200).json({
message: "Токен оновлено."
});
};
API для виходу
export const logout = async (req: Request, res: Response) => {
res.clearCookie("refreshToken", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "none",
});
res.clearCookie("accessToken", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "none",
});
res.status(200).json({ message: "Вихід успішний!" });
};
Операції на фронтенді (NextJS)
Як ви знаєте, NextJS має вбудований сервер на основі NodeJS, що означає, що NextJS проекти вже містять шар BFF (Backend For Frontend). У цьому шарі ми можемо налаштувати middleware для контролю авторизації користувача на стороні сервера.
NextJS Server Authorization
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const accessToken = request.cookies.get("accessToken");
const refreshToken = request.cookies.get("refreshToken");
if (!accessToken && !refreshToken) {
const loginUrl = new URL("/auth", request.url);
loginUrl.searchParams.set("redirect", request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/:path*"], // Захищені маршрути.
};
Тут ви можете управляти оновленням токенів, перевірками токенів (шифрування/дешифрування), але я надаю перевагу зберіганню структури refresh-token на стороні клієнта.
NextJS Client Authorization
Основні дії виконуються тут, ми будемо управляти потоком токенів. Почнемо з першого етапу сценарію — запиту на вхід.
Açıklama satırlarıyla birlikte aşağıdaki kodu inceleyebilirsiniz.
type SignInProps = {
email: string;
password: string;
redirect?: boolean;
}
const signIn = async ({
email,
password,
redirect,
}: SignInProps) => {
try {
const {data} = await instance.post("/auth/login", {
email,
password,
});
// Тут ми робимо запит на вхід
// У відповіді cookies встановлюються в браузері.
// Дані, що повертаються, можна зберегти в store.
// У прикладі використано Redux-Toolkit.
// Ви можете використовувати будь-який інший метод
dispatch(setUser(data?.user));
router.push("/dashboard");
} catch (err) {
console.log(err);
}
};
Налаштуємо axios interceptors для обробки потоку refresh-token.
const instance: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
withCredentials: true, // Потрібно для відправки cookies.
});
// response interceptor перевіряє дані відповіді
// і спочатку зберігає оригінальний запит, потім перевіряє статус або відповідь
// При статусі 401 (Unauthorized) виконується запит на refresh-token.
// Якщо refreshToken є і є дійсним, новий accessToken буде встановлений на стороні NodeJS.
instance.interceptors.response.use(
(response: AxiosResponse) => response,
async (error) => {
const originalRequest = error.config as AxiosRequestConfig & {
_retry?: boolean;
};
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Виконуємо запит на новий accessToken.
await instance.post(
"/auth/refresh-token",
{},
{ withCredentials: true }
);
// Повторно відправляємо оригінальний запит.
return instance(originalRequest);
} catch (refreshError) {
console.error("Оновлення токену не вдалося:", refreshError);
// Якщо оновлення токену не вдалося,
// перенаправляємо користувача на сторінку авторизації.
window.location.href = "/auth";
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
Після налаштування interceptor та cookies, давайте тепер використовувати інформацію про користувача в проекті. Для цього можна написати React Hook. Нижче наведено приклад такого hook:
export const useUser = () => {
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
const router = useRouter();
const getUser = useSelector((state: RootState) => state.user);
useEffect(() => {
if (!getUser) {
const fetchUser = async () => {
try {
const { data } = await instance.get("/user/me");
dispatch(setUser(data?.user));
} catch (err: any) {
// Не завжди правильно перенаправляти на logout без перевірки помилки.
// Враховуйте такі ситуації.
// Axios interceptor вже обробляє інші випадки,
// але ми також можемо обробити це окремо.
toast.warning(err.response.data.error, {
onClose: async () => {
await instance.get("/auth/logout");
router.push("/auth");
},
});
} finally {
setLoading(false);
}
};
fetchUser();
} else {
setLoading(false);
}
}, [dispatch, getUser]);
return { getUser, loading };
};
Цей hook можна значно покращити за допомогою додаткових бібліотек, таких як Tanstack Query, але головне — зрозуміти концепцію.
Після того, як ми створили наш hook, можемо почати його використовувати. Залежно від вимог вашого проекту або його структури, ви можете впровадити клієнтську авторизацію. Ось приклад, як я використовував його в RootLayout, який охоплює увесь проект.
export default function RootLayout({
children,
css,
}: {
children: React.ReactNode;
css?: string;
}) {
const { getUser: session, loading } = useUser();
if (!session || loading) {
return (
Loading...
);
}
return (
<>
{children}
); } ``` Як більш елегантний спосіб для управління авторизацією на основі компонентів, можна використовувати HOC (High Order Component). HOC не є темою цієї статті, але нижче наведено короткий приклад його використання. ``` import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { useUser } from "@/hooks/useUser"; export default function isAuth(Component: any) { return function IsAuth(props: any) { const { getUser: session, loading } = useUser(); const { push } = useRouter(); const auth = !!session || loading; // Перенаправлення як спосіб useEffect(() => { if (!auth) { push("/auth"); } }, [auth]); // Сторінка Unauthorized як спосіб // if (!auth) { // return ( //
// Unauthorized //
// ); // } return ; }; } ``` Використання ``` import isAuth from "@/hoc/isAuth"; function ProfilePage() { return (
Профіль користувача
Ласкаво просимо!
); } export default isAuth(ProfilePage); ``` Дякую за увагу, якщо ви помітили якісь неточності або є питання, будь ласка, поділіться ними. Бажаю всім успіхів у роботі.
Перекладено з: [Next.js ve Node.js İle Refresh-Token Sıfırdan Kimlik Doğrulama Altyapısı](https://medium.com/@violetbee0/next-js-ve-node-js-i%CC%87le-refresh-token-s%C4%B1f%C4%B1rdan-kimlik-do%C4%9Frulama-altyap%C4%B1s%C4%B1-70861f79f73f)