Аутентификация — это одна из тех вещей, которые зачастую требуют от нас гораздо больше усилий, чем нам хотелось бы.
Чтобы реализовать аутентификацию, приходится заново разбираться в темах, о которых вы не вспоминали с тех пор, как в последний раз делали ее для вашего приложения. Ведь эта область очень быстро развивается, а это означает, что за прошедшее с тех пор время появилась целая куча всего нового: новые угрозы, новые решения и обновления ранее используемых вами инструментов, из-за которых вам придется часами копаться в документации и ваших прошлых проектах.
В этом руководстве мы рассмотрим другой подход к аутентификации (а также управлению доступом, SSO и т.д.) в React-приложениях.
Вместо того чтобы использовать статическую библиотеку, которую нужно постоянно обновлять и заново вникать в нее всякий раз, когда вы хотите реализовать где-нибудь аутентификацию, мы будем использовать сервис, который обновляется автоматически и является упрощенной альтернативой Auth0, Okta и их аналогов.
Github-репозиторий примера: github.com/userfront/react-example
Аутентификация в React
Обычно аутентификации в React-приложении выглядит следующим образом: приложение делает запрос к серверу аутентификации, который возвращает токен доступа (access token), затем этот токен сохраняется в браузере и может быть использован при последующих запросах к вашему серверу (или другим серверам, если это необходимо).
Будь то стандартная аутентификация по логину (электронной почте) и паролю или через магические ссылки (magic links) и технологии единого входа (SSO) такие как Google, Azure и Facebook, мы хотим, чтобы наше React-приложение отправляло первоначальный запрос на сервер аутентификации и чтобы этот сервер, в свою очередь, генерировал токен.
Задачи React
- Отправка первоначального запроса на сервер аутентификации
- Получение и хранение токена доступа
- Отправка токена доступа на ваш сервер при каждом последующем запросе.
JWT (JSON Web Tokens)
JSON Web Tokens (JWT) — это компактные и безопасные токены, которые можно использовать для аутентификации и управления доступом в React-приложениях.
Каждый JWT содержит в себе простой JSON-объект, называемый «полезной нагрузкой» (payload), и подписывается специальным образом, чтобы ваш сервер мог удостовериться в подлинности его полезной нагрузки.
Важно отметить, что эта полезная нагрузка JWT может быть прочитана кем угодно: и вашим React-приложением, и любыми третьими лицами, которые видят токен.
Токен доступа JWT выглядит следующим образом:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjRjMDUxZTg3LTU2ZWEtNGUzNC05ZmE3LThkYWNkOThkOWUzMyJ9.eyJtb2RlIjoidGVzdCIsInRlbmFudElkIjoiZGVtbzEyMzQiLCJ1c2VySWQiOjEsInVzZXJVdWlkIjoiNGU2ODgwZTMtZDU0YS00NTNlLTgwMDQtMmJmMGNjNWM1MmY2IiwiaXNDb25maXJtZWQiOnRydWUsImF1dGhvcml6YXRpb24iOnsiZGVtbzEyMzQiOnsicm9sZXMiOlsiY29udHJpYnV0b3IiLCJ2aWV3ZXIiLCJhZG1pbiJdfX0sImlzcyI6InVzZXJmcm9udCIsInNlc3Npb25JZCI6IjNlYTUxOTNjLWE3ZWEtNGM1Zi05ZmRlLTZhZjk0ZTgyNGQ3NCIsImlhdCI6MTY1ODUxNzc2OSwiZXhwIjoxNjU5MTIyNTY5fQ.LB7IyP09G6f64Ho0CdjA8xDe-s7KAjJGPe5lC9GRwSrWHZSnvmoMb1HTRkrO1oabDcpMOvDmyjjkK0Nb-0RoK-bKX3i5PPtvKGu18VKgShxgg0bXYsA-HRrh8bsOExXhKO_I7gS7GVGCMDlSlbGFDRVdtqSqv1rfKj0pRP9PScFjuB9bbdGlmJXbQSw8V_zRxPYEUOnoTqW7-vfyn3DONbo0cElm1mRVMZzK1W8-Kpg6MRTt7nlMj60ysoBktM4w6KdOlDTmlyLzy6kjkqSylT_pDk0ALWW2tCRV8qZcrzJhWu-g8x6MIEUCo8TArELdl6aUGLdItVi-WmQCZNwajQ
Полезной нагрузкой в этом JWT является средняя часть (отделенная точками):
eyJtb2RlIjoidGVzdCIsInRlbmFudElkIjoiZGVtbzEyMzQiLCJ1c2VySWQiOjEsInVzZXJVdWlkIjoiNGU2ODgwZTMtZDU0YS00NTNlLTgwMDQtMmJmMGNjNWM1MmY2IiwiaXNDb25maXJtZWQiOnRydWUsImF1dGhvcml6YXRpb24iOnsiZGVtbzEyMzQiOnsicm9sZXMiOlsiY29udHJpYnV0b3IiLCJ2aWV3ZXIiLCJhZG1pbiJdfX0sImlzcyI6InVzZXJmcm9udCIsInNlc3Npb25JZCI6IjNlYTUxOTNjLWE3ZWEtNGM1Zi05ZmRlLTZhZjk0ZTgyNGQ3NCIsImlhdCI6MTY1ODUxNzc2OSwiZXhwIjoxNjU5MTIyNTY5fQ
JSON-объект в полезной нагрузке JWT зашифрован в base64:
JSON.parse(atob("eyJtb2RlIjoidGVzdCIsInRlbmFudElkIjoiZGVtbzEyMzQiLCJ1c2VySWQiOjEsInVzZXJVdWlkIjoiNGU2ODgwZTMtZDU0YS00NTNlLTgwMDQtMmJmMGNjNWM1MmY2IiwiaXNDb25maXJtZWQiOnRydWUsImF1dGhvcml6YXRpb24iOnsiZGVtbzEyMzQiOnsicm9sZXMiOlsiY29udHJpYnV0b3IiLCJ2aWV3ZXIiLCJhZG1pbiJdfX0sImlzcyI6InVzZXJmcm9udCIsInNlc3Npb25JZCI6IjNlYTUxOTNjLWE3ZWEtNGM1Zi05ZmRlLTZhZjk0ZTgyNGQ3NCIsImlhdCI6MTY1ODUxNzc2OSwiZXhwIjoxNjU5MTIyNTY5fQ"));
/**
* {
* "userId": 1,
* "userUuid": "4e6880e3-d54a-453e-8004-2bf0cc5c52f6",
* "isEmailConfirmed": true,
* ...
* }
*/
Токены доступа JWT
Благодаря современной криптосистеме RSA сервер аутентификации способен генерировать защищенные JWT, которые любая другая машина может прочитать и проверить с помощью публичного (открытого) ключа.
Ваш бэкенд-сервер может подтвердить подлинность токена доступа JWT, сверив его с публичным ключом.
Это позволяет бэкенд-серверу отклонять любые токены доступа JWT, которые не были сгенерированы сервером аутентификации (или срок действия которых истек).
JWT в React-приложении:
- Ваше React-приложение запрашивает токен доступа JWT всякий раз, когда пользователь хочет войти в систему.
- Сервер аутентификации генерирует токен доступа JWT на основе приватного (закрытого ключа) и затем отправляет его обратно в React-приложение.
- React-приложение хранит этот токен доступа JWT и отправляет его на бэкенд-сервер всякий раз, когда пользователю необходимо сделать запрос.
- Ваш бэкенд-сервер проверяет токен доступа JWT с помощью публичного ключа, а затем считывает полезную нагрузку, чтобы определить, какой пользователь выполняет запрос.
Примечание:
Все эти шаги на бумаге выглядят достаточно просто, но каждый из них имеет свой набор «подводных камней», на которые вы обязательно наткнетесь, когда вам нужно будет реализовать их на практике и обеспечить безопасность. Особенно когда с течением времени появляются новые источники угроз и требуется исправление или поддержка новых платформ — вам всегда будет нужно совершать какие-нибудь действия, направленные на обеспечение безопасности.
Userfront устраняет всю эту мороку с авторизацией в React-приложениях
Userfront — это фреймворк, который позволяет вам абстрагироваться от сложностей авторизации. Это полнофункциональное решение, которое автоматически обновляется.
Это значительно упрощает работу с аутентификацией в React-приложениях и, что, пожалуй, наиболее важно, берет на себя заботу об актуальности всех протоколов аутентификации.
Настройка аутентификации в React
Теперь мы рассмотрим процесс создания всех основных аспектов аутентификации в React-приложении.
Для создания приложения мы будем использовать Create React App, а для маршрутизации на стороне клиента — React Router.
GitHub репозиторий примера: github.com/userfront/react-example
Оборот токенов доступа JWT
На высоком уровне ответственность React по аутентификации заключается в следующем:
- Отправка первоначального запроса в Userfront для получения токена доступа JWT. Это делают формы регистрации и логина в систему.
- Отправка токена доступа JWT на ваш сервер при каждом последующем запросе.
Установка Create React App
Чтобы начать работу с React, нам нужно установить Create React App и React Router.
npx create-react-app my-app
cd my-app
npm install react-router-dom --save
npm start
Теперь наше React-приложение доступно по адресу localhost:3000.
Как сказано на превью, мы можем начать с редактирования файла
src/App.js
.Превью
Маршрутизация
Мы создадим небольшое приложение с маршрутизацией. Этого нам будет достаточно для начала добавления аутентификации.
Маршрут | Описание |
/ |
Главная страница |
/login |
Страница входа в систему |
/reset |
Страница сброса пароля |
/dashboard |
Информационная панель, только для вошедших в систему пользователей |
Замените содержимое файла src/App.js на следующий код (который был взят из руководства React Router):
// src/App.js
import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/reset">Reset</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/reset" element={<PasswordReset />} />
<Route path="/dashboard" element={<DefaultLayout />} />
</Routes>
</div>
</Router>
);
}
function Home() {
return <h2>Home</h2>;
}
function Login() {
return <h2>Login</h2>;
}
function PasswordReset() {
return <h2>Password Reset</h2>;
}
function Dashboard() {
return <h2>Dashboard</h2>;
}
Мы создали маршруты и готовы заняться непосредственно аутентификацией.
Превью
Регистрация, логин и сброс пароля
Начнем мы с того, что добавим на главную страницу форму для регистрации.
Вы можете найти инструкцию созданию формы регистрации в разделе Toolkit на вашей панели управления.
Она будет выглядеть следующим образом:
Userfront Toolkit
Оставьте Toolkit открытым в другой вкладке браузера, чтобы позже добавить формы входа в систему и сброса пароля.
Следуя инструкциям, установите Userfront Toolkit для React с помощью следующей команды:
npm install @userfront/toolkit --save
npm start
Затем добавьте форму регистрации на главную страницу, импортировав и инициализировав Userfront, и обновив функцию Home(), чтобы она отображала форму:
// src/App.js
import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import Userfront, { SignupForm } from "@userfront/toolkit/react";
Userfront.init("demo1234");
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/reset">Reset</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/reset" element={<PasswordReset />} />
<Route path="/dashboard" element={<DefaultLayout />} />
</Routes>
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
<SignupForm />
</div>
);
}
function Login() {
return <h2>Login</h2>;
}
function PasswordReset() {
return <h2>Password Reset</h2>;
}
function Dashboard() {
return <h2>Dashboard</h2>;
}
Теперь форма регистрации размещена на главной странице. Давайте попробуем зарегистрировать нового пользователя.
Превью
Тестовый режим
По умолчанию форма находится в «Тестовом режиме» (Test mode), позволяющем создавать учетные данные пользователей в тестовой среде, которую можно отдельно просмотреть на панели управления Userfront.
Формы логина и сброса пароля мы можем добавить тем же способом, что и форму регистрации. Чтобы позволить пользователю выйти из системы, мы можем вызвать встроенный метод
Userfront.logout
.Нам нужно сделать следующие изменения в файле src/App.js:
- Добавить в метод Login() возврат формы входа в систему.
- Добавить в метод PasswordReset() возврат формы сброса пароля.
- Добавить в метод Dashboard() отображение данных пользователя и кнопки выхода из системы.
Обратите внимание на
toolId
для каждой формы в вашем Toolkit. Приведенные в данном примере кода toolId
не подойдут для вашего приложения.// src/App.js
import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import Userfront, {
SignupForm,
LoginForm,
PasswordResetForm
} from "@userfront/toolkit/react";
Userfront.init("demo1234");
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/reset">Reset</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/reset" element={<PasswordReset />} />
<Route path="/dashboard" element={<DefaultLayout />} />
</Routes>
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
<SignupForm />
</div>
);
}
function Login() {
return (
<div>
<h2>Login</h2>
<LoginForm />
</div>
);
}
function PasswordReset() {
return (
<div>
<h2>Password Reset</h2>
<PasswordResetForm />
</div>
);
}
function Dashboard() {
const userData = JSON.stringify(Userfront.user, null, 2);
return (
<div>
<h2>Dashboard</h2>
<pre>{userData}</pre>
<button onClick={Userfront.logout}>Logout</button>
</div>
);
}
На этом этапе регистрация, вход в систему и сброс пароля должны уже быть вполне работоспособны. Следует отметить, что форма входа на странице
/login
будет автоматически перенаправлять на страницу /dashboard
, если вы уже вошли в систему.Теперь пользователи могут регистрироваться, логиниться, разлогиниваться и сбрасывать пароль.
Превью
Защищенный маршрут в React
Мы не хотим, чтобы пользователи могли просматривать информационную панель, если они не вошли в систему. Это называется защитой маршрута.
Если пользователь не вошел в систему, но пытается посетить /dashboard, мы можем перенаправить его на экран входа в систему.
Чтобы это реализовать мы можем обернуть компонент <DefaultLayout /> компонентом , который проверяет, вошел ли пользователь в систему. Если пользователь залогинился, его токен доступа будет доступен в виде Userfront.tokens.accessToken, поэтому нам достаточно просто проверить его наличие.
Для перенаправления браузера в случае отсутствия токена доступа компонент RequireAuth использует Navigate и useLocation из React Router.
// src/App.js
import React from "react";
import {
BrowserRouter as Router,
Routes,
Route,
Link,
Navigate,
useLocation,
} from "react-router-dom";
import Userfront, {
SignupForm,
LoginForm,
PasswordResetForm
} from "@userfront/toolkit/react";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/reset">Reset</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/reset" element={<PasswordReset />} />
<Route
path="/dashboard"
element={
<RequireAuth>
<DefaultLayout />
</RequireAuth>
}
/>
</Routes>
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
<SignupForm />
</div>
);
}
function Login() {
return (
<div>
<h2>Login</h2>
<LoginForm />
</div>
);
}
function PasswordReset() {
return (
<div>
<h2>Password Reset</h2>
<PasswordResetForm />
</div>
);
}
function Dashboard() {
const userData = JSON.stringify(Userfront.user, null, 2);
return (
<div>
<h2>Dashboard</h2>
<pre>{userData}</pre>
<button onClick={Userfront.logout}>Logout</button>
</div>
);
}
function RequireAuth({ children }) {
let location = useLocation();
if (!Userfront.tokens.accessToken) {
// Redirect to the /login page
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
Таким образом, если пользователь вошел в систему, он может просматривать информационную панель. А если пользователь не залогинился, он будет перенаправлен на страницу входа в систему.
Теперь у нас есть веб-приложение с готовыми функциями регистрации, входа в систему, выхода из нее, сброса пароля и защищенным маршрутом.
Превью
Аутентификация через API в React
Как мы уже видели выше, на фронтенде при входе пользователя в систему создается токен доступа
Userfront.tokens.accessToken
. Это токен доступа JWT, который также можно использовать на бэкенде для защиты конечных точек API.Библиотек для чтения и проверки JWT достаточно большое количество практически на каждом языке. В списке ниже приведены некоторые популярные библиотеки для работы с JWT.
JWT-библиотеки для:
Ваше React-приложение может передавать токен доступа JWT в виде Bearer-токена внутри заголовка
Authorization
. Например:async function getInfo() {
const res = await window.fetch("/your-endpoint", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Userfront.tokens.accessToken}`,
},
});
console.log(res);
}
getInfo();
Перед тем как обработать такой запрос, бэкенд должен сначала считать токен доступа JWT из заголовка
Authorization
и проверить его корректность с помощью публичного ключа JWT, который можно найти в панели управления Userfront.Ниже приведен пример Node.js middleware для чтения и проверки токена доступа JWT:
// Пример Node.js (Express.js)
const jwt = require("jsonwebtoken");
function authenticateToken(req, res, next) {
// Считываем токен доступа JWT из заголовка запроса
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (token == null) return res.sendStatus(401); // Возвращаем 401 при отсутствии токена
// Проверяем токен с помощью публичного ключа Userfront
jwt.verify(token, process.env.USERFRONT_PUBLIC_KEY, (err, auth) => {
if (err) return res.sendStatus(403); // Возвращаем 403 при непройденной верификации
req.auth = auth;
next();
});
}
При таком подходе все недействительные или отсутствующие токены будут отклонены сервером. Вы также можете ссылаться на содержимое токена в обработчиках маршрутов, используя объект
req.auth
:console.log(req.auth);
// =>
{
mode: 'test',
tenantId: 'demo1234',
userId: 1,
userUuid: 'ab53dbdc-bb1a-4d4d-9edf-683a6ca3f609',
isEmailConfirmed: false,
authorization: {
demo1234: {
roles: ["admin"]
},
},
sessionId: '35d0bf4a-912c-4429-9886-cd65a4844a4f',
iat: 1614114057,
exp: 1616706057
}
На основе этой информации можно выполнить дополнительные проверки или использовать
userId
или userUuid
для поиска данных, связанных с пользователем.Например, если необходимо сделать маршрут доступным только пользователям-администраторам, можно проверять объект
authorization
из валидного токена доступа:// Пример Node.js (Express.js)
app.get("/users", (req, res) => {
const authorization = req.auth.authorization["demo1234"] || {};
if (authorization.roles.includes("admin")) {
// Разрешаем доступ
} else {
// Запрещаем доступ
}
});
React SSO (технологии единого входа)
Также мы можем добавить в React-приложение социальных поставщиков идентификационных данных, таких как Google, Facebook и LinkedIn, или бизнес поставщиков идентификации, таких как Azure AD, Office365 и т.д.
Для этого необходимо создать приложение с поставщиком идентификации (например, Google), а затем добавить учетные данные SSO в панель управления Userfront. В результате мы получаем модифицированную систему логина.
Для реализации Single Sign On с помощью этого подхода не требуется никакого дополнительного кода: можно добавлять и удалять поставщиеов, не изменяя формы или способ обработки токенов доступа JWT.
Заключение
Добавление аутентификации и контроля доступа в React-приложение не обязательно должно быть сложной задачей. И этап настройки, и, что еще важнее, сопровождение в течение долгого времени — все это можно переложить на современные платформы, такие как Userfront.
JSON Web Tokens позволяет четко отделить слой генерации токенов от остальной части приложения, что делает его более понятным и модульным. Такая архитектура также позволяет сосредоточить усилия на самом приложении, где вы, скорее всего, создадите гораздо больше пользы для себя и своих клиентов.
Более подробную информацию о добавлении аутентификации в React-приложение можно найти в руководстве Userfront, где описано все: от настройки форм аутентификации до документации по API, примеры репозиториев, работа с различными языками и фреймворками и т.д.
GreaterGlider
Спасибо за столь подробную статью, но сейчас, в 2023, я бы взял next-auth для решения задачи аутентификации и вообще next.js в котором дается решение многих рутинных задач из коробки.