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

JWT token - это строка, состоящая из трех частей: заголовок, данные и подпись.

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

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

Существует два вида JWT токенов: access token и refresh token, которые создаются и используются только в паре.

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

С помощью refresh токена мы обновляем устаревший ("протухший") access токен. При этом сервер генерирует для клиента новую пару access token + refresh token.

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

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

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

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

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

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

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

  • Cf

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

dsadads

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

fdsfdsfdfds

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);

4.4. Добавим Private Routes в проект

В папке 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 "./../entities/user/user.store";
import PrivateRoute from "../components/privateRouteHOC/privateRouteHOC";
 
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;

Комментарии (8)