Универсальное руководство о том, как создать часть аутентификации в вашем приложении React.

Платформа авторизации OAuth 2.0

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

Жетоны доступа

Токены доступа используются при аутентификации на основе токенов, чтобы разрешить доступ к API. Токены доступа получаются после того, как пользователи успешно аутентифицируются и авторизуются. Затем эти токены передаются в качестве учетных данных при вызове API. Маркер доступа сообщает API, что его носитель авторизован для доступа к API и выполнения определенных действий, если они попадают в область действия, предоставленную во время авторизации.

Обновить токены

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

В этом уроке я буду использовать Axios

Axios - это библиотека JavaScript, используемая для выполнения HTTP-запросов из Node.js или XMLHttpRequests из браузера, который также поддерживает ES6 Promise API.
Вот полная статья, объясняющая плюсы использования Axios.

Перехватчики Axios:

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

Обработка ошибки может быть выполнена путем перехвата любого запроса, если он не удался, и выполнения некоторого кода, если он соответствует некоторым условиям error.

axiosInstance.interceptors.response.use(
  response => { return response; }, error => {
    if (errorSatifiesConditions(error)) {
      return doSomething().then({....
      // could be to resend original request 
      )} 
    }  else {
      doSomethingElse()...

Знакомство с хуками React - React v16, февраль 2019 -

В этом руководстве я буду использовать хуки React, а именно: useState и useContext.

Давайте начнем

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

Объясняя логику

Самый первый шаг - это когда пользователь вводит URL. ваше приложение должно проверить localStorage, если этот пользователь входил в систему раньше.

  • A- Если есть информация о пользователе в localStorage, ваше приложение React продолжит проверять, активен ли access_token пользователя.
    Если access_token активен, пользователь в порядке, и они успешно прошли аутентификацию.
    Если access_token истек, ваше приложение должно отправить refreshToken запрос и получить новый access_token
  • B. Если в localStorage нет информации о пользователе, ваше приложение должно перенаправить пользователя на страницу входа, где они смогут указать имя пользователя и пароль (или страницу регистрации).

За A отвечает функция getUser(), за B отвечает функция login(username, password).

Итак, основная идея заключается в том, что после запуска приложение вызывает getUser(), который либо аутентифицирует пользователя, либо нет.
Если нет, приложение вызывает login с учетные данные пользователя, и функция login вызывает getUser для получения информации currUser.

Чтобы было проще

constants.js

Создайте файл constants.js, который будет содержать все константы, чтобы вам не приходилось вводить его каждый раз при вызове конечной точки.
С axios.create это еще более абстрактно и проще в использовании.
my_app - это экземпляр axios.

import axios from "axios";
export const URL = "https://your/url.com";
export const API_URL = "https://your/api/url.com";
export const my_app = axios.create({baseURL: API_URL});

Настройка вызова API - auth.js

Создайте файл auth.js, который будет содержать все ваши функции.

1. refreshToken

Базовый вызов конечной точки обновления.

const refreshToken = () => {
  let currUser = JSON.parse(localStorage.getItem("my_app_user"));
  let getUserFormData = new FormData();
  getUserFormData.append("grant_type", "refresh_token");
  getUserFormData.append("refresh_token", currUser.refresh_token);
  return new Promise((resolve, reject) => {
    my_app
    .post(`${URL}/token/url/`, getUserFormData, {
      headers: {
        Authorization: "Basic {secret_key}"
      }
    })
    .then(async response => {
      resolve(response);
    })
    .catch(error => {
      reject(error);
    });
  });
};

2. getUser

getUser отвечает за проверку наличия пользователя, хранящегося в localStorage. Если не находит, возвращается ноль.

Если он находит пользователя в localStorage ,, он получает дату последнего access_token обновления этого пользователя, вычитает ее из текущего времени и сравнивает ее со сроком действия access_token . Если он все еще действителен, он продолжается и получает currUser информацию.

Если он недействителен, он вызывает refreshTokenendpoint и заменяет точку доступа новой и сбрасывает lastRefresh в currUser в localStorage.
Не забудьте заменить заголовок вашего Экземпляр axios my_app с новым токеном доступа.

my_app.defaults.headers.common["Authorization"] =
"Bearer " + currUser.access_token;

Перехватчики Axios:
Вы будете использовать перехватчики Axios, чтобы перехватить любой запрос, если он завершился неудачно с кодом состояния ошибки, представляющим истекший токен доступа (обычно: 401, сообщения об ошибке различаются). Это означает, что авторизация не удалась, потому что access_token истек.
В этом случае вам нужно отправить запрос refreshToken, а затем повторно отправить любой запрос, который изначально не удался, с новым действительным access_token , возвращенным из refreshToken запроса.

my_app.interceptors.response.use(
response => { return response; },
error => {
if (error.response.status === 401 && error.response.message === "blablabla") {
return refreshToken().then({....
// resend original request

Наконец, если запросы login или getUserInfo или refreshToken завершились неудачно, вы должны выйти из системы и удалить currUserfrom localStorage, так как это безопаснее и перенаправит пользователя на страницу входа. Здесь вы также можете обрабатывать определенные типы ошибок. например, конкретная сетевая ошибка и т. д.

Одна из причин, по которой запрос refreshToken может завершиться ошибкой, - это редкий случай, когда refresh_token истек. Жетоны запросов имеют относительно гораздо более длительный срок службы, чем access_token, но его истечение все еще возможно. В этом случае вам следует перенаправить пользователя на страницу входа, используя logout, как упоминалось ранее.

const getUser = () => {
  let currUser = JSON.parse(localStorage.getItem("my_app_user"));
  if (!currUser) {
    // if no user in localStorage then the user must enter their      credentials to proceed
    return Promise.resolve(null);
  }
  // get the expiry time of the current access token and measure   whether it expired or not
  let currDate = new Date();
  let diff = currDate.getTime() - currUser.lastRefresh;
  if (diff >= currUser["expires_in"] * 1000) {
    // access token expired need to refresh token
    // you could just call refreshToken I just typed it in so you could see it clearer
    // call refreshToken()
    // if success, update currUser with the new access_token and save this instant to be the last_refresh (for future refresh) and update axios headers
    // if fail logout()
  } else {
  // Do not need refresh
  my_app.defaults.headers.common["Authorization"] =
"Bearer " + currUser.access_token;
  my_app.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response.status === 401 && some other condition that sprecifies an expir state){
    // 401 is Unauthorized error
    // which means that this request failed
    // what we need to do is send a refresh request then resend the same request
    // that failed but with the new access point.
    // We can do this automatically using axios interceptors
    return refreshToken()
    .then(response => {
    
      // update currUser with new access_token
      // Set default headers to have authorization the access token as authorization for future requests
      // Get the original that failed due to 401 and resend it
      // with the new access token
      const config = error.config;
      config.headers.Authorization =
      "Bearer " + response.data.access_token;
      // Resending original request
      return new Promise((resolve, reject) => {
      my_app
      .request(config)
      .then(response => { resolve(response); })
      .catch(error => { reject(error); });
      });
    })
    .catch(error => { // just logout() if anything goes wrong});
    }
    logout();
    return new Promise((resolve, reject) => { reject(error); });
    }
  );
  // finally get user 
  return my_app.get("/users/current/url").catch(error => {
    logout();
    throw error;
   });
  }
};

3. login

Эта функция просто получает имя пользователя и пароль и отправляет их для аутентификации вместе с секретным ключом.

const login = (username, password) => {
  let loginFormData = new FormData();
  loginFormData.append("grant_type", "password");
  loginFormData.append("username", username);
  loginFormData.append("password", password);
  return new Promise((resolve, reject) => {
    axios
    .post(`${URL}/token/url`, loginFormData, {
    headers: { Authorization: "Basic {secret_key}" }
    })
    .then(async response => {
      response.data.lastRefresh = new Date().getTime();
      localStorage.setItem("my_app_user", JSON.stringify(response.data));
      getUser()
      .then(response => { resolve(response); })
      .catch(error => { reject(error); });  
      })
      .catch(error => { reject(error); });
      });
    };

4. logout

Обычно просто удаляет currUser из localStorage.

const logout = () => {
  localStorage.removeItem("my_app_user");
  return Promise.resolve();
};

5. Контекст аутентификации

Этот контекст будет постоянно содержать вашу пользовательскую переменную. Из user вы сможете узнать.

  1. Независимо от того, вошел ли пользователь в систему или нет.
  2. Информация о пользователе.
import React, { createContext, useState } from "react";
import auth from "../helpers/auth";
import LoadingPage from "../components/LoadingPage";
export const AuthenticationContext = createContext();
const AuthenticationContextProvider = props => {
  const [user, setUser] = useState(null);
  const [fetched, setFetched] = useState(false);
  const login = (username, password) => {
    return auth.login(username, password).then(data => {
    setUser(data.data);
});
};
const logout = async () => {
  setUser(null);
  await auth.logout();
};
const getUser = async () => {
  const user = await auth.getUser();
  setUser(user ? user.data : null);
};
if (!fetched) {
  getUser().then(() => setFetched(true));
}
if (!fetched) {
  return <LoadingPage />;
}
return (
  <AuthenticationContext.Provider
  value={{
    user,
    login,
    logout,
    getUser,
    setUser
  }}
  >
    {props.children}
  </AuthenticationContext.Provider>
  );
};
export { AuthenticationContextProvider };

fetched сообщает, что вы пытались получить currUser, и если он был обнаружен, информация о пользователе будет в user, иначе она будет пустой.

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

if (!fetched) {
  getUser().then(() => setFetched(true));
}

6 . последний шаг

Этот шаг является вашим переключателем аутентификации, который перенаправляет пользователя на аутентифицированные страницы или неаутентифицированную страницу входа в систему на основе user в контексте.

const { user } = useContext(AuthenticationContext);
return user ? <AuthenticatedApp /> : <UnauthenticatedApp />;

Важная заметка

Размещение ваших маршрутов и маршрутизатора в Authenticatedapp значительно упростит вам маршрутизацию; пользователь будет перенаправлен на соответствующий маршрут к URL-адресу, который они ввели, если они были успешно аутентифицированы, а не вы выполняете маршрутизацию вручную.

И наконец. Это было полное руководство по аутентификации на основе токенов с помощью React. Надеюсь, вы узнали. Если у вас есть вопросы, не стесняйтесь спрашивать.

Вы найдете все файлы кода в этом репозитории GitHub.

Примечание от Plain English

Вы знали, что у нас четыре публикации? Проявите немного любви, предложив им следующие слова: JavaScript на простом английском, AI на простом английском, UX на простом английском , Python на простом английском - спасибо и продолжайте учиться!

Кроме того, мы всегда заинтересованы в продвижении хорошего контента. Если у вас есть статья, которую вы хотели бы отправить в какую-либо из наших публикаций, отправьте электронное письмо по адресу [email protected] со своим именем пользователя Medium и тем, о чем вы хотите написать, и мы вернуться к вам!