Как сделать Private Routes с авторизацией через JWT token

от автора

1. Что такое JWT token

JWT token (он же JSON Web Token) — это строка, состоящая из трех частей: заголовок, данные и подпись.

Посетитель сайта хочет прочитать свою переписку. Он заходит на страницу с диалогами, и сайт отправляет запрос на получение контента (списка сообщений), при этом к запросу прикрепляется специальная строка — JWT токен. С помощью этой строки сервер проверяет, авторизован ли пользователь, и решает, направить ли ему в ответ контент сайта (список сообщений).

https://jwt.io/ - официальный сайт, где можно расшифровать первые 2 части токена

https://jwt.io/ — официальный сайт, где можно расшифровать первые 2 части токена

Существует два вида JWT-токенов: access-token и refresh-token, которые создаются и используются только в паре. Эту пару токенов генерирует сервер и выдает конкретному пользователю в ответ на запрос авторизации. При этом каждому из токенов назначается «срок жизни» — период времени, в течение которого токен будет распознаваться бекендом как валидный. Для этого сервер сохраняет выданную пару токенов в своей базе данных, а при новой авторизации заменяет у себя информацию о токенах и их валидности токенов.

1.1. Access-токен

Особенностью access-токена является то, что он хранится локально на frontend-части сайта в localStorage, и его необходимо каждый раз записывать в заголовки запросов на бекенд.

С помощью access-токена мы получаем доступ к функциям сайта, которые зависят от бекенда (получают данные от бекенда). Это, в основном данные для страниц, доступных авторизованным пользователям ( список сообщений, возможность оставить комментарий, удалить свой пост).

Это работает следующим образом: пользователь нажимает кнопку «загрузить историю сообщений». Сайт генерирует GET-запрос на бекенд, добавляет в headers запроса данные Access-токена. Бекенд, соответственно, считывает заголовки, проверяет валидность токена и направляет пользователю ответ со списком сообщений, который доступен только авторизованным пользователям.

1.2. Refresh-токен

Когда у access-токена заканчивается срок действия, на помощь приходит refresh-токен.

Этот токен отличается тем, что хранится в cookies, и поэтому всегда автоматически прицепляется к запросам на бекенд.

С помощью refresh-токена мы обновляем устаревший («протухший») access-токен. Для этого мы отправляем запрос на специальный endpoint бекенда (обычно «/api/refresh»), а в ответ сервер генерирует новую пару access token + refresh token.

Далее цикл повторяется: сервер сохраняет информацию о новой валидной паре access-token + refresh-token, а сайт прикрепляет новый access-token к заголовкам новых запросов.

Если до сих пор не понятно, есть очень крутой и понятный ролик на ютуб?.

2. Авторизация через JWT token (теория)

Авторизация с использованием JWT token реализуется с помощью двух основных паттернов:

  • первичная авторизация (т.е. когда пользователь первый раз зашел на сайт, либо когда пользователь последний раз авторизовался очень давно, и пара JWT-токенов устарела)

  • авторизация через обновление токена (т.е. когда пользователь авторизовался на сайте, затем закрыл вкладку, сбросив хранящееся в переменной состояние isAuth, и далее заново открыл вкладку с сайтом; но при этом пара JWT-токенов либо refresh-токен не успели устареть)

2.1. Авторизация с нуля (первичная авторизация )

  • Посетитель переходит на страницу авторизации http://my_awesome_web_app.com/login.

  • Посетитель вводит логин и пароль в форму авторизации, и нажимает кнопку «Авторизоваться» («Login»).

  • Запрос авторизации с введенными ранее логином и паролем отправляются на сервер.

  • Сервер проверяет, есть ли такая пара логин + пароль в его базе данных с пользователями.

  • Если такой пользователь не найден, сервер отправляет в ответ ошибку.

  • Если логин и пароль введены верно, сервер генерирует для пользователя пару токенов access token + refresh token, и сразу же записывает refresh токен в httpOnly cookie.

  • Далее сервер создает ответ об успешной авторизации, и записывает в body ответа сгенерированный access token. Ответ направляется пользователю.

  • Сайт принимает ответ сервера и записывает access token из ответа в свой localStorage. Также для целей доступа пользователя к приватным страничкам сайт создает и устанавливает переменную isAuth = true.

  • Далее ко всем запросам пользователя добавляется access token, и сервер дает ответ пользователю как авторизованному (например, сервер считывает из accessToken id пользователя и может дать ему список его друзей или его сообщений).

Первая авторизация на сайте

Первая авторизация на сайте

2.2. Авторизация через обновление токена

  • Посетитель переходит на любую страницу сайта http://my_awesome_web_app.com/news.

  • Если для просмотра страницы http://my_awesome_web_app.com/news не требуется авторизация, сайт показывает эту страничку.

  • Если для просмотра страницы http://my_awesome_web_app.com/my_messages нужна авторизация, сайт показывает нам «крутилку», и в это время пытается обновить авторизацию на основе ранее полученных токенов.

  • Сайт пробует считать access токен из localStorage, и направить запрос на получение данных пользователя (аватарки, имени пользователя и т.п.).

    • Если access token окажется валидным, то с сервера в ответ придут данные пользователя, и сайт может установить переменную isAuth = true. Далее, соответственно, пользователь перенаправляется на страничку, которую изначально хотел посмотреть.

    • Если access token окажется невалидным, то с сервера в ответ придет ошибка. Эту ошибку нужно перехватить и попробовать обновить авторизацию через refresh token. То есть в тот момент, когда пришел ответ с ошибкой, перехватить ошибку и направить запрос на адрес «/api/refresh». К этому запросу автоматически прицепляется cookies с refresh-токеном.

      • При успешном обновлении авторизации сервер выдает пользователю новую пару access token + refresh token, и далее пользователь сможет сделать успешный запрос за получением данных пользователя, и, соответственно, статуса isAuth = true на сайте. Далее, соответственно, пользователь перенаправляется на страничку, которую изначально хотел посмотреть.

      • При неудачной попытке обновления авторизации, устанавливается переменная isAuth = false, и пользователь перенаправляется на страницу авторизации, либо на домашнюю страницу сайта.

  • В результате, пользователь получает желаемую страничку, либо перенаправляется на страницу авторизации.

3. Что требуется для JWT авторизации на frontend

3.1. Создать проект

Введите в консоли: npx create-react-app jwt-auth-app

Если не знаете, как создать новый проект на React:

3.2. Настроить axios

Введите в консоли: npm install axios

В папке src/ создадим файл api.config.js, в котором пропишем для axios добавление accessToken к запросам и обновление токенов при невалидном accessToken.

Код для добавления перехватчиков к axios-запросам:
import axios from "axios";  export const instance = axios.create({   // к запросу будет приуепляться cookies   withCredentials: true,   baseURL: "https://jsonplaceholder.typicode.com/", });   // создаем перехватчик запросов // который к каждому запросу добавляет accessToken из localStorage instance.interceptors.request.use(   (config) => {     config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`     return config   } )   // создаем перехватчик ответов // который в случае невалидного accessToken попытается его обновить // и переотправить запрос с обновленным accessToken instance.interceptors.response.use(   // в случае валидного accessToken ничего не делаем:   (config) => {     return config;   },   // в случае просроченного accessToken пытаемся его обновить:   async (error) => {    // предотвращаем зацикленный запрос, добавляя свойство _isRetry     const originalRequest = {...error.config};    originalRequest._isRetry = true;      if (       // проверим, что ошибка именно из-за невалидного accessToken       error.response.status === 401 &&        // проверим, что запрос не повторный       error.config &&       !error.config._isRetry     ) {       try {         // запрос на обновление токенов         const resp = await instance.get("/api/refresh");         // сохраняем новый accessToken в localStorage         localStorage.setItem("token", resp.data.accessToken);         // переотправляем запрос с обновленным accessToken         return instance.request(originalRequest);       } catch (error) {         console.log("AUTH ERROR");       }     }     // на случай, если возникла другая ошибка (не связанная с авторизацией)     // пробросим эту ошибку      throw error;   } );

  • Введите в консоли: npm install axios

В папке src/ создадим файл api.auth.js, в котором пропишем запросы на backend:

import { instance } from "./api.config.js";  export default const AuthService {      login (email, password) {         return instance.post("/api/login", {email, password})     }          refreshToken() {         return instance.get("/api/refresh");     }          logout() {         return instance.post("/api/logout")     } }

4. Создаем Private Routes

На многих сайтах существует два вида страниц: с общим доступом и приватные.

Страницы с общим доступом может открыть любой посетитель сайта (например, новости, информация о компании, а также страница авторизации).

Приватные страницы доступны только авторизованным пользователям, иначе они автоматически отправляют неавторизованного посетителя на страницу авторизации.

Для того, чтобы сайт знал, авторизован ли пользователь, необходимо хранить эту информацию в двух глобальных переменных: isAuth, isAuthInProgress.

4.1. Установим React Router

Введите в консоли: npm install react-router react-router-dom

4.2. Создадим переменные isAuth, isAuthInProgress

Эти переменные можно создать и хранить в state-менеджере (redux, mobX), можно создать специальный AuthContext, также можно хранить их в useState, либо создать хук, который будет обновлять авторизацию и возвращать объект с указанными переменными.

Разберем это на примере хранения переменных в mobX.

Введите в консоли: npm install mobx mobx-react-lite

В папке src/ создадим файл store.js, в котором пропишем указанные переменные.

Код файла store.js:
import { makeAutoObservable } from "mobx"; import AuthService from "./api.auth.js";  class AuthStore {      isAuth = false;   isAuthInProgress = false;      constructor() {     makeAutoObservable(this, {}, { autoBind: true });   }    async login(email, password) {     this.isAuthInProgress = true;     try {       const resp = await AuthService.login(email, password);       localStorage.setItem("token", resp.data.accessToken);       this.isAuth = true;       } catch (err) {       console.log("login error");      } finally {       this.isAuthInProgress = false;     }    }    async checkAuth() {     this.isAuthInProgress = true;     try {       const resp = await AuthService.refresh();       localStorage.setItem("token", resp.data.accessToken);       this.isAuth = true;       } catch (err) {       console.log("login error");      } finally {       this.isAuthInProgress = false;     }    }    async logout() {     this.isAuthInProgress = true;     try {       await AuthService.logout();       this.isAuth = false;       localStorage.removeItem("token");     } catch (err) {       console.log("logout error");     } finally {       this.isAuthInProgress = false;     }    }    }  export default new AuthStore();

4.3. Создадим HOC PrivateRoute

В папке src/ создадим файл privateRoute.jsx, в котором пропишем логику privateRoute:

  • если процесс авторизации не завершен, то ждем, когда сервер пришлет ответ об удачной/неудачной попытке авторизации

  • если пришел ответ от сервера, и авторизация успешна, тогда показываем пользователю запрошенную приватную страничку

  • если пришел ответ от сервера, но авторизация неуспешна, то перенаправляем пользователя на страницу авторизации

Код файла privateRoute.jsx:
import { Navigate, Outlet, Route } from "react-router-dom"; import authStore from "./store.js"; import { observer } from "mobx-react-lite";  const PrivateRoute = (props) => {    if (authStore.isLoadingAuth) {     return <div>Checking auth...</div>;   }   if (authStore.isAuth) {      return <Outlet/>   } else {     return <Navigate to="/login" />;   } };    export default observer(PrivateRoute); 

<Outlet/> используется для отображения children (вместо props.children)

В папке src/ редактируем файл App.jsx, в котором добавим приватные роуты.

App.jsx:
import React, { useEffect } from "react"; import { Route, Routes, BrowserRouter } from "react-router-dom"; import { observer } from "mobx-react-lite"; import AuthStore from "./store.js"; import PrivateRoute from "./privateRoute.js";   import LoginPage from "./pages/loginPage"; import UsersPage from "./pages/usersPage";  const App = observer(() => {      useEffect(() => {      AuthStore.checkAuth();   }, []);     return (     <BrowserRouter>         <Routes>     //страница, для посещения которой авторизация не требуется             <Route path="/login" element={<LoginPage />} />      //страницы, для посещения которых требуется авторизация             <Route path="/users" element={<PrivateRoute  />}>                 <Route path="" element={<UsersPage />} />                 <Route path=":id" element={<UserPage />} />             </Route>              <Route path="*" element={<div>404... not found </div>} />         </Routes>     </BrowserRouter>    ); });  export default App;

Успех!

Вот и готова первая JWT-авторизация на фронтенде. Всем работающего кода, даже когда у тебя лапки ?


ссылка на оригинал статьи https://habr.com/ru/articles/732432/