1. Что такое JWT token
JWT token - это строка, состоящая из трех частей: заголовок, данные и подпись.
Посетитель сайта хочет прочитать свою переписку. Он заходит на страницу с диалогами, и сайт отправляет запрос на получение контента (списка сообщений), при этом к запросу прикрепляется специальная строка - JWT токен. С помощью этой строки сервер проверяет, авторизован ли пользователь, и решает, направить ли ему в ответ контент (список сообщений).
Существует два вида 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;