Часть III JWT Auth с помощью Elixir на Phoenix 1.4 API и React Native

В части I мы создали невероятно быстрый Эликсир на API аутентификации JWT Phoenix Guardian. (Github Repo)

В части II мы создали наше приложение React Native и его базовые компоненты / экраны. (Github Repo)

В этой третьей и последней части мы будем использовать Axios для выполнения HTTP-запросов к нашему Elixir API, и мы будем сохранять соответствующие данные на нашем устройстве с помощью модуля AsyncStorage React Native.

Хотя это часть III руководства по веб-токенам Elixir / Phoenix - React Native JSON, клиент React Native JWT, созданный с помощью этого руководства, будет работать с любым подходящим API.

Публикация регистрации пользователя

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

Хотя мы могли бы использовать метод JavaScript fetch () для наших запросов API, мы собираемся использовать вместо него библиотеку HTTP-запросов Axios.

Почему Axios вместо fetch ()?

Axios возвращает автоматически преобразованные в строку ответы JSON на наши HTTP-запросы, тогда как ответы, возвращаемые функцией fetch (), должны быть вручную преобразованы в строку JSON.

Axios также будет правильно отлавливать ошибки, когда мы получаем статусы ошибок, тогда как fetch () будет возвращать ok при получении определенных статусов ошибки (например, 500), что было бы проблематично.

Давайте установим axios в корневой каталог нашего приложения с yarn add axios или npm i --save axios, если у вас не установлен yarn.

Импортируйте axios в Registration.js и Login.js:

В Registration.js создайте внутри нашего компонента функцию с именем registerUser (), свяжите ее в нашем конструкторе, отмените ссылку на соответствующее состояние формы с помощью const и добавьте оператор который устанавливает загрузка и ошибка в значение true при запуске registerUser ():

Теперь добавьте следующую функцию axios.post () с обещанием .then () для обработки ответа JWT. s и функция .catch () для обработки ошибок:

Как вы можете видеть выше, наша функция axios.post () нашей регистрации POST создает user: {} объект, содержащий адрес электронной почты, пароль и password_confirmation нашего удаленного состояния для “/v1/sign_up” конечной точки нашего API.

Предполагая, что наша регистрация прошла успешно, нам нужно как-то сохранить наш ответ JWT на устройстве. Для этого мы будем использовать функции AsyncStorage React Native.

О React Native AsyncStorage

Модуль AsyncStorage React Native предоставляет приложениям React Native постоянную систему хранения ключей и значений.

В iOS AsyncStorage сохраняет меньшие значения в сериализованных словарях и большие значения во всех файлах. На Android AsyncStorage будет использовать либо RocksDB, либо SQLite.

Хотя AsyncStorage по умолчанию не зашифрован, приложения на вашем устройстве могут получить доступ только к своим собственным значениям AsyncStorage (если ваше устройство не было взломано / взломано). Любые значения AsyncStorage, сохраненные вашим приложением, будут защищены от отслеживания между приложениями, если ваше устройство не было взломано.

Чтобы узнать больше об AsyncStorage, прочтите документацию React Native!

Сохранение в хранилище устройства с помощью AsyncStorage

Создайте файл src/services/deviceStorage.js . Импортируйте { AsyncStorage } из React Native, создайте экспортируемую const с именем deviceStorage и export default deviceStorage примерно так:

Внутри deviceStorage создайте асинхронную функцию с именем saveItem, которая принимает key и соответствующий value в качестве аргументов. В этой асинхронной функции напишите предложение try{ await }, которое запускает AsyncStorage.setItem(key, value);, и предложение catch(error){}, которое регистрирует любые ошибки.

Вышеупомянутое предложение try{ await function() } заставляет ваше приложение ждать, пока содержащаяся в нем функция не будет завершена, перед выполнением любых последующих функций. (Сделайте это до конца этого руководства, чтобы получить бонусную заметку об асинхронных и синхронных функциях 😉.)

Вернувшись в Register.js, давайте импортируем наш модуль deviceStorage:

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

axios.post("http://localhost:4000/api/v1/sign_up",{
      user: {
        email: email,
        password: password,
        password_confirmation: password_confirmation
      }
    },)
    .then((response) => {
      console.log(response);

Теперь откройте консоль удаленного отладчика и посмотрите на наш объект ответа:

Наш JWT расположен по адресу .data.jwt в нашем объекте ответа, поэтому response.data.jwt.

Давайте удалим наш console.log и передадим JWT-часть нашего ответа через deviceStorage.saveKey:

axios.post("http://localhost:4000/api/v1/sign_up",{
      user: {
        email: email,
        password: password,
        password_confirmation: password_confirmation
      }
    },)
    .then((response) => {
      deviceStorage.saveKey("id_token", response.data.jwt);
    })

В deviceStorage.saveKey(“id_token”, response.data.jwt) наш JWT сохраняется в хранилище устройства с ключом id_token. Когда мы получим наш токен позже, мы будем искать значение AsyncStorage с этим ключом id_token.

Теперь мы сохраняем наш ответ JWT в локальное хранилище, но не в состоянии нашего корневого компонента. Давайте сделаем это.

Установка родительского состояния из дочерних элементов

Чтобы установить состояние JWT нашего компонента App.js из наших компонентов Регистрация или Вход, мы должны создать функцию, которая устанавливает состояние JWT в App.js и привяжите его в конструкторе App:

По-прежнему в App.js передайте newJWT(JWT) через ‹Auth› как опору:

newJWT(jwt){
  this.setState({
    jwt: jwt
  });
}
render() {
    if (!this.state.jwt) {
      return (
        <Auth newJWT={this.newJWT}/>
      );

В Auth.js передайте newJWT={this.props.newJWT} в качестве опоры как в Регистрация, так и в Вход в функции whichForm ():

Наконец, вернувшись в Registration.js, запустите this.props.newJWT(response.data.jwt) в нашем обратном вызове axios:

Теперь наш axios.post () сохраняет возвращенный JWT в хранилище устройства и устанавливает JWT в состояние родительского приложения.

Мы достигаем этого, передавая App.js newJWT(){ setState } как опору на экран Auth, а затем снова как опору через наш компонент Регистрация внутри нашего Компонент экрана аутентификации.

Таким образом, наша функция настройки состояния передается от родителя к потомку как: Корень приложения - ›Auth -› Регистрация - ›Auth -› Приложение.

Это идеальное место для реализации Redux или альтернативной архитектуры управления состоянием flux вместо передачи функции deep prop, но это выходит за рамки данного руководства 😉.

Публикация логина пользователя

В components / Login.js импортируйте axios и deviceStorage и создайте + привяжите функцию с именем loginUser() внутри компонента входа, которая POST будет содержать наш адрес электронной почты для входа в систему. / пароль к нашей конечной точке /sign_in API:

Как и в случае с Registration.js, мы настраиваем функцию onPress в нашей форме ‹Button /› на запуск axios.post (), который отправляет учетные данные. из состояния компонента в наш API в обмен на JWT, который мы сохраняем в хранилище устройства и устанавливаем в состояние нашего корневого компонента через props.

POST Обработчик ошибок

Давайте добавим обработчик ошибок к нашим компонентам Регистрация и Вход, которые будут запускаться при активации .catch ((error) = ›{}). .

Создайте функцию onRegistrationFail () в Registration.js, которая устанавливает для нашего состояния ошибки значение ‘Registration Failed’, а для загрузки - false. . Затем привяжите это onRegistrationFail в конструкторе и добавьте его в axios .catch ():

Когда для нашего состояния ошибки установлено значение 'Registration Failed', ошибка будет отображаться в тексте ошибки формы, который мы написали ранее:

<Text style={errorTextStyle}>
  {error}
</Text>

Создайте и привяжите аналогичную функцию onLoginFail () в Login.js:

Загрузка и удаление JWT с помощью AsyncStorage

Давайте создадим и экспортируем компонент, содержащий кнопку «Выйти» в screen / LoggedIn.js. Этот экран будет отображаться в App.js, когда его состояние содержит JWT:

Теперь, если мы успешно зарегистрируем или войдем в систему пользователя, этот экран будет отображаться; однако это довольно бесполезно без функции, которая удаляет JWT приложения из состояния и AsyncStorage.

Кроме того, теперь, когда мы сохраняем JWT в хранилище устройства при входе в систему / регистрации, мы хотим иметь возможность загружать этот JWT из хранилища при запуске нашего приложения.

Откройте services / deviceStorage.js и добавьте еще одну асинхронную функцию с именем loadJWT (), которая проверяет AsyncStorage на наличие элемента. с ключом ‘id_token’ через AsyncStorage.getItem(‘id_token’) и устанавливает его значение в состояние, если оно найдено:

Затем добавьте еще одну асинхронную функцию в deviceStorage под названием deleteJWT (), которая удаляет пару "ключ-значение" id_token из памяти устройства через AsyncStorage.removeItem(‘id_token’), затем стирает состояние JWT нашего приложения с помощью this.setState({ jwt: ‘’ }):

Вернувшись в App.js, импортируйте deviceStorage в верхней части файла. Свяжите deviceStorage.deleteJWT и deviceStorage.loadJWT в конструкторе:

Связывая наши импортированные вспомогательные функции deviceStorage в конструкторе, мы позволяем им устанавливать состояние. Довольно аккуратно!

Теперь добавьте loading: true к нашему начальному состоянию и запустите this.loadJWT() в конце нашего конструктора:

this.loadJWT () запускается при создании нашего компонента App.js перед его монтированием. Запуск функции в конструкторе компонента React аналогичен запуску функции в устаревшем методе жизненного цикла компонента componentWillMount ().

Перепишите функцию render () приложения с начальным оператором if (this.state.loading), затем передайте this.deleteJWT через <LoggedIn /> как опора, как <LoggedIn deleteJWT={this.deleteJWT} />:

В LoggedIn.js передайте this.props.deleteJWT через нашу кнопку Выйти:

Теперь, если вы запустите это приложение в симуляторе, загрузится экран LoggedIn, если ваше устройство содержит JWT!

Кроме того, если вы нажмете кнопку Выйти во время входа в систему, этот JWT будет удален, и вы вернетесь обратно на экран Аутентификация!

Наш процесс аутентификации с поддержкой AsyncStorage завершен!

… Но мы еще не отправляем запросы с проверкой подлинности.

Подготовка к выполнению HTTP-запросов с аутентификацией JWT

Мы сделаем GET запрос с Authorization: Bearer [jwt here] в заголовках нашего "api/v1/my_user" аутентифицированного конечного пункта API.

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

В верхней части LoggedIn.js импортируйте текст из react-native, axios из axios и Загрузка из общих компоненты:

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { Button, Loading } from '../components/common/';
import axios from 'axios';

Добавьте Loading: true, email: ‘’, error: ‘’ в состояние нашего конструктора:

export default class LoggedIn extends Component {
  constructor(props){
    super(props);
    this.state = {
      loading: true,
      email: '',
      error: ''
    }
  }

В нашем объекте const styles добавьте стили для emailText и errorText:

const styles = {
  container: {
    flex: 1,
    justifyContent: 'center'
  },
  emailText: {
    alignSelf: 'center',
    color: 'black',
    fontSize: 20
  },
  errorText: {
    alignSelf: 'center',
    fontSize: 18,
    color: 'red'
  }
};

Добавьте state и стили const без ссылки в начало render () LoggedIn:

render() {
    const { container, emailText, errorText } = styles;
    const { loading, email, error } = this.state;

Теперь мы добавим условное if / else, которое будет отображать загрузку if this.state.loading = true и наше аутентифицированное представление if loading = false:

if (loading){
      return(
        <View style={container}>
          <Loading size={'large'} />
        </View>
      )
    } else {
        return(
          <View style={container}>
            <View>
              {email ?
                <Text style={emailText}>
                  Your email: {email}
                </Text>
                :
                <Text style={errorText}>
                  {error}
                </Text>}
            </View>
            <Button onPress={this.props.deleteJWT}>
              Log Out
            </Button>
          </View>
      );
    }

Обратите внимание на троичный оператор: email ? [email Text] : [error Text]. Если loading: false и email: ‘[email protected], отобразится компонент ‹Text›, содержащий {email}, в противном случае отобразится {error}.

Теперь сделаем запрос!

Выполнение HTTP-запросов с аутентификацией JWT с помощью Axios в React Native

Прежде чем мы сделаем наш GET запрос с аутентификацией, нам нужно сделать JWT-состояние корневого компонента доступным для LoggedIn.

Откройте App.js и запустите this.state.jwt через ‹LoggedIn /› как опору, как:

render() {
    if (this.state.loading) {
      # code omitted
    } else if (!this.state.jwt) {
      # code omitted
    } else if (this.state.jwt) {
      return (
        <LoggedIn jwt={this.state.jwt} deleteJWT={this.deleteJWT} />
      );
    }
  }

Теперь вернитесь к LoggedIn.js.

Мы сделаем наш GET запрос с JWT-аутентификацией в методе жизненного цикла componentDidMount () нашего компонента LoggedIn.

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

Во-первых, давайте создадим заголовок авторизации нашего запроса как const:

componentDidMount(){
    const headers = {
      'Authorization': 'Bearer ' + this.props.jwt
    };
}

Затем напишите полный HTTP-запрос axios ({request_object}):

componentDidMount(){
    const headers = {
      'Authorization': 'Bearer ' + this.props.jwt
    };
    axios({
      method: 'GET',
      url: 'http://localhost:4000/api/v1/my_user',
      headers: headers,
    })
}

Мы передаем тип запроса (GET, если вы еще не знали) через method:, нашу конечную точку API через url:, и нашу JWT «Авторизация» заголовки от const до headers:.

Наконец, добавьте предложения .then () и .catch (), которые устанавливают состояние электронной почты из response.data в событие успешного запроса, состояние ошибки в случае неудачного запроса и loading: false в любом случае:

componentDidMount(){
    const headers = {
      'Authorization': 'Bearer ' + this.props.jwt
    };
    axios({
      method: 'GET',
      url: 'http://localhost:4000/api/v1/my_user',
      headers: headers,
    }).then((response) => {
      this.setState({
        email: response.data.email,
        loading: false
      });
    }).catch((error) => {
      this.setState({
        error: 'Error retrieving data',
        loading: false
      });
    });
  }

Наш завершенный экран LoggedIn.js должен выглядеть следующим образом:

Красивый! Почти шекспировский.

Теперь давайте запустим наш сервер Phoenix из Части I с mix phx.server.

В новом окне интерфейса командной строки запустите наше приложение React Native с react-native run-ios и зарегистрируйтесь или войдите в систему:

Мы сделали это!

Заключение

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

Ознакомьтесь с кодом на Github для справки:

Phoenix 1.4 / Elixir API на Github

React Native JWT Client на Github

Пожалуйста, прокомментируйте ниже, если у вас есть какие-либо отзывы / вопросы / проблемы, и присоединяйтесь ко мне в разногласиях Hoptok!

И, как всегда…

🍹Советы оценены! 😉

Мой биткойн-адрес: 1QJuBzHpis4jqQXnSuYxKzGS4Yu3GHhNtX

Бонус: примечание об асинхронных и синхронных функциях

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

Один из примеров проблемы синхронности:

var retrievedValue = AsyncStorage.getItem("some_key");
console.log(retrievedValue);

Практически во всех случаях указанный выше console.log () не сможет отобразить элемент, полученный из AsyncStorage, потому что console.log () будет запущен до getItem ( «Some_key») завершает работу.

Асинхронности можно достичь с помощью вложенных обратных вызовов, обещаний или синтаксически красивой функции async try / await (async funcName(args){ try{ await func(args)} }).