Настройка системи автентифікації з оновленням токенів з нуля за допомогою Next.js та Node.js

pic

згенеровано ШІ

Привіт, хочу розібрати одну з популярних тем, яка залишає багато запитань у багатьох — як налаштувати авторизацію користувача через зовнішній сервер за допомогою 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)

Leave a Reply

Your email address will not be published. Required fields are marked *