Глава 1 | Зачем беспокоиться?
Глава 2 | Изучение основ эликсира
Глава 3 | Введение в Phoenix
Глава 4 | Реализация React
Глава 5 | Работа с PostgreSQL
Глава 6 | Создание службы API PostgreSQL
Глава 7 | CRUD Операции
Сфера действия этой главы
В предыдущей главе мы смогли создать службу API с помощью Phoenix. Это лежит в основе разработки полного стека. Однако мы не рассмотрели критический вариант использования аутентификации пользователей. Мы будем делать еще один мини-проект, поэтому расскажите, как это сделать.
Начиная
Хотя я мог бы просто предоставить шаблон, давайте рассмотрим создание нашего нового проекта для дополнительной практики.
Настраивать
Сначала создадим новый проект с именем phoenix_user_authenticaion и войдем в каталог:
mix phoenix.new phoenix_user_authentication cd phoenix_user_authentication
Затем откройте проект в любом редакторе кода. Проверьте конфигурацию базы данных PostgreSQL, которую мы создадим в config / dev.exs.
Вот моя конфигурация:
# Configure your database config :phoenix_user_authentication, PhoenixUserAuthentication.Repo, adapter: Ecto.Adapters.Postgres, username: "postgres", password: "postgres", database: "phoenix_user_authentication_dev", hostname: "localhost", pool_size: 10, port: 5432 # I added this, 5432 is default
Если все в порядке, запустите mix ecto.create, чтобы создать эту базу данных.
Если вы откроете pgAdmin и обновите наш сервер Phoenix, вы можете дважды щелкнуть базу данных и подключиться к ней:

Прохладный.
Создание таблицы пользователей
Мы хотим создать таблицу в нашей базе данных для хранения пользователей:
Users
_____
username | password_hash
1
2
3
...
Мы сделаем это по-настоящему простым и сохраним только поля username и password_hash (зашифрованный пароль) в таблице с именем Users.
Давайте продолжим и создадим модель для этой таблицы в разделе web / models и назовем ее users.ex:
defmodule PhoenixUserAuthentication.Users do
use PhoenixUserAuthentication.Web, :model
schema "users" do
field :username, :string
field :password_hash, :string
timestamps()
end
end
Теперь мы можем запустить mix ecto.gen.migration users, чтобы создать шаблон файла миграции для нашей таблицы users.
Теперь откройте созданный файл миграции в разделе priv / repo / migrations:
defmodule PhoenixUserAuthentication.Repo.Migrations.Users do use Ecto.Migration def change do end end
Напомним, файлы миграции используются для создания таблиц в базе данных по форме, указанной в схеме. Поэтому давайте обновим функцию change следующим образом:
defmodule PhoenixUserAuthentication.Repo.Migrations.Users do
use Ecto.Migration
def change do
create table(:users) do
add :username, :string, null: false
add :password_hash, :string, null: false
timestamps()
end
end
end
Мы добавляем create table(:users), который является функцией для создания таблицы с именем users, и добавляем файл username и password_hash с их типом. Я также включил null: false, поскольку эти поля не могут быть пустыми.
Теперь мы готовы выполнить миграцию для создания нашей таблицы users в нашей базе данных, выполнив следующее:
mix ecto.migrate
Посмотрим, сработало ли это!
Обновите наш сервер Phoenix в pgAdmin:

Разверните нашу схему в разделе Databases / phoenix_user_authentication_dev / schemas / public:
Под таблицами мы видим, что были добавлены таблица users и таблица с метаданными наших миграций:

Если мы посмотрим на таблицу users, мы увидим, что в нашей таблице есть поля, указанные в нашей схеме:

Потрясающие!
Нам нужно сделать два быстрых редактирования, прежде чем мы введем подряд вручную.
Нам нужно, чтобы наши столбцы с отметками времени (Insert_at и updated_at) заполнялись автоматически с помощью следующего выражения sql: now()
Щелкните правой кнопкой мыши Insert_at и выберите свойства.
Выберите вкладку определение и добавьте now() в качестве значения по умолчанию:

Повторите этот же процесс для updated_at.
Создание нашей службы API
Прежде чем мы перейдем к аутентификации пользователей, давайте продолжим и настроим базовую службу api для выборки всех пользователей и создания нового пользователя.
Откройте web / router.ex и давайте добавим тип HTTP-запроса, маршрут, контроллер и функцию для вызова в контроллере для каждого из наших взаимодействий:
scope "/api", PhoenixUserAuthentication do pipe_through :api get "/users", UsersController, :index post "/users", UsersController, :create end
Мы хотим, чтобы маршрут / api проходил через наш конвейер api. В области пути / api у нас будет / users.
/api/users будет обрабатывать запросы GET и POST. По запросу GET функция index будет вызываться в UsersController. По запросу POST функция create будет вызываться в UsersController.
Затем давайте создадим файл с именем users_controller.ex в папке controllers для нашего UsersController.
Оболочка нашего кода выглядит так:
defmodule PhoenixUserAuthentication.UsersController do
use PhoenixUserAuthentication.Web, :controller
alias PhoenixUserAuthentication.Users
def index(conn, _params) do
# get all users
# render JSON object containing all users
end
def create(conn, %{"users" => users_params}) do
# create changeset
# use changeset to insert a new row using params
end
end
Мы создаем псевдоним, чтобы нам не приходилось записывать PhoenixUserAuthentication.Users при обращении к таблице Users.
Затем у нас есть оболочка функций index и create.
Функция index будет использовать Repo.all для выборки всех строк в таблице Users и вызова отрисовки объекта JSON, содержащего этих пользователей:
def index(conn, _params) do users= Repo.all(Users) render conn, "index.json", users: users end
Функция create получит набор изменений, который можно использовать для вставки новой строки в таблицу Users. Набор изменений будет создан с использованием параметров из HTTP-запроса.
def create(conn, %{"users" => users_params}) do
changeset = Users.changeset(%Users{}, users_params)
case Repo.insert(changeset) do
{:ok, _blogs} ->
users = Repo.all(Users)
render conn, "index.json", users: users
end
end
При успешной вставке все пользователи будут отображены, чтобы мы могли видеть добавленного пользователя.
Поскольку здесь мы ссылаемся на набор изменений, давайте создадим его дальше.
Функция changeset войдет в нашу модель, находящуюся в web / models / users.ex:
defmodule PhoenixUserAuthentication.Users do
use PhoenixUserAuthentication.Web, :model
schema "users" do
field :username, :string
field :password_hash, :string
timestamps()
end
def changeset(struct, params) do
struct
|> cast(params, [:username, :password_hash])
|> validate_required([:username, :password_hash])
|> unique_constraint(:username)
end
end
Функция changeset принимает пустую структуру, приводит параметры и проверяет обязательные поля. Кроме того, он гарантирует, что имена пользователей останутся уникальными.
Нам нужно будет добавить больше для нашей аутентификации пользователей, но пока это оставим.
Чтобы завершить работу службы API без аутентификации пользователей, давайте создадим представление для рендеринга объекта JSON со всеми пользователями.
Создайте новый файл с именем users_view.ex в web / views.
defmodule PhoenixUserAuthentication.UsersView do
use PhoenixUserAuthentication.Web, :view
#show multiple users
def render("index.json", %{users: users}) do
%{
users: Enum.map(users, &users_json/1)
}
end
def users_json(user) do
%{
username: user.username
}
end
end
Здесь мы визуализируем объект JSON с именем index, который в конечном итоге будет содержать массив с именем users, в котором есть объект, содержащий информацию для каждого пользователя в каждом индексе.
Хеширование паролей

Я уже упоминал, что мы должны выполнять вещи аутентификации пользователей. Вы можете подумать: «Что нам нужно делать?»
При работе с паролями пользователей мы не хотим хранить точный пароль в виде строки в базе данных по соображениям безопасности. Вместо этого нам нужно хешировать пароль. Хеширование пароля означает использование строки пароля и применение алгоритма, который будет генерировать совершенно другое значение.
Хешированное значение будет каждый раз одним и тем же, поэтому вы можете сохранить хешированный пароль в базе данных и проверить введенный пользователем пароль к сохраненному хешу. [1]
В Phoenix мы можем использовать библиотеку хеширования паролей под названием Comeonin.
Нам нужно добавить это как зависимость в mix.exs (эквивалент package.json в Elixir). Это произойдет в двух местах.
Сначала мы добавляем его в функцию application:
def application do
[mod: {PhoenixUserAuthentication, []},
applications: [
:phoenix, :phoenix_pubsub,
:phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :comeonin
]
] #comeonin added here
end
Мы также добавляем его в наши deps:
defp deps do
[{:phoenix, "~> 1.2.4"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:comeonin, "~> 2.6"}
]
end
Давайте установим эту новую зависимость, используя mix deps.get.
Установив Comeonin, мы можем вернуться к нашей модели в web / models / user.ex.
Мы хотим добавить виртуальный пароль, что означает, что мы можем использовать это поле в нашей модели, не сохраняя его в нашей базе данных:
schema "users" do field :username, :string field :password_hash, :string field :password, :string, virtual: true timestamps() end
Мы также обновляем ревизию для проверки имени пользователя и пароля, а затем хэшируем пароль, вызывая put_password_hash:
def changeset(struct, params) do
struct
|> cast(params, [:username, :password])
|> validate_required([:username, :password])
|> unique_constraint(:username)
|> validate_length(:password, min: 6, max: 100)
|> put_password_hash
end
put_password_hash проверит применение хеша к паролю в наборе изменений с помощью Comeonin.Bcrypt.hashpwsalt:
defp put_password_hash(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(password))
_ ->
changeset
end
end
Теперь эта строка в функции create нашего UsersController вернет имя пользователя и хешированный пароль для вставки в нашу базу данных:
changeset = Users.changeset(%Users{}, users_params)
Давайте создадим интерфейс React с формой регистрации, чтобы убедиться, что наша база данных добавляет новых пользователей через наш API.
Создание регистрационной формы

Мы уже создали форму в интерфейсе React и прошли все настройки. Опять же, я повторю это вместо того, чтобы предоставить шаблон для дополнительной практики.
Во-первых, мы можем установить наши основные зависимости:
npm install --save react react-dom react-router-dom babel-preset-react axios
Затем мы настраиваем наш файл brunch-config.js для применения предустановок для Babel и React:
plugins: {
babel: {
presets: ["es2015", "react"],
// Do not use ES6 compiler in vendor code
ignore: [/web\/static\/vendor/]
}
},
Кроме того, мы добавляем белый список под параметрами npm в этом файле, чтобы было ясно, что мы собираемся использовать react и react-dom :
npm: {
enabled: true,
whitelist: ["phoenix", "phoenix_html", "react", "react-dom"]
}
Затем мы можем создать цель для нашего приложения в шаблоне index.html.eex (удалите все остальное):
<div id="app"></div>
Шаблон приложения содержит предварительно заполненный заголовок, который мы можем удалить:
<header class="header">
<nav role="navigation">
<ul class="nav nav-pills pull-right">
<li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
</ul>
</nav>
<span class="logo"></span>
</header>
^^^ delete this
В web/static/js создадим полный каталог проекта React:

Мы просто добавляем контейнеры и презентационные папки. Контейнеры относятся к активным компонентам, в которых будет выполнен запрос API, а полученные данные будут переданы в презентационный компонент, который представляет что-то нашему пользовательскому интерфейсу с использованием унаследованных свойств.
Откройте app.js и давайте добавим следующий код:
import "phoenix_html";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
import Signup from "./containers/Signup";
class App extends React.Component {
render() {
return (
<Router>
<div>
<Route exact path="/" component={Signup}/>
</div>
</Router>
)
}
}
ReactDOM.render(
<App/>,
document.getElementById("app")
)
В приведенном выше коде мы собираемся отобразить контейнер регистрации на пути по умолчанию. Давайте создадим этот контейнер.
Создайте файл с именем Registration.js в папке container:
import React from "react";
import axios from "axios";
class Signup extends React.Component {
constructor() {
super();
this.state = {
username: '',
password: ''
};
}
handleUsername(event) {
this.setState({ username: event.target.value })
}
handlePassword(event) {
this.setState({ password: event.target.value })
}
handleSubmit (event) {
event.preventDefault();
console.log(this.state);
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<div className="field">
<label className="label">Username</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.username}
onChange = {this.handleUsername.bind(this)}
/>
</div>
</div>
<div className="field">
<label className="label">Password</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.password}
onChange = {this.handlePassword.bind(this)}
/>
</div>
</div>
<button
type="submit"
value="Submit"
className="button is-primary"
>
Submit
</button>
</form>
)
}
}
export default Signup
Приведенный выше код похож на форму, которую мы создали в предыдущей главе. Он использует Bulma для предопределенного стиля формы и управления значениями каждого ввода через обработчики событий, которые обновляют локальное состояние.
Давайте попробуем эту форму регистрации, которая в настоящее время просто регистрирует местный штат при отправке.
Однако прежде чем мы это сделаем, давайте быстро добавим CDN в заголовок web / templates / app.html.eex:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.4/css/bulma.css">
Теперь запустите brunch build и mix phoenix.server, затем перейдите по адресу http: // localhost: 4000 /:

Вставьте имя пользователя и пароль и нажмите «Отправить»:

Как и ожидалось, вводимые имя пользователя и пароль хранятся в локальном состоянии.
Давайте обновим обработчик событий handleSubmit в контейнере Registration, который находится в Registration.js:
handleSubmit (event) {
event.preventDefault();
axios({
method: 'post',
headers: {"Content-Type": "application/json"},
url: 'http://localhost:4000/api/users',
data: {
users: {
username: this.state.username,
password: this.state.password
}
}
});
}
Мы заменяем журнал нашей консоли некоторым кодом axios, чтобы сделать HTTP-запрос POST на http://localhost:4000/api/users с параметрами в теле.
Запустите brunch build и введите новое имя пользователя и пароль из формы.
Теперь мы можем обновить и просмотреть нашу таблицу users, чтобы увидеть новую строку с хешированным паролем:

Милая! Затем мы создадим форму входа и сравним входные данные с базой данных для аутентификации пользователя.
Аутентификация пользователя при входе в систему

Создание формы входа в React
Откройте web / static / js / app.js и давайте обновим его:
import Signup from "./containers/Signup";
import Login from "./containers/Login";
class App extends React.Component {
render() {
return (
<Router>
<div>
<Route exact path="/" component={Signup}/>
<Route path="/login" component={Login}/>
</div>
</Router>
)
}
}
Мы импортировали новый компонент-контейнер под названием Login и добавили для него маршрут.
Создайте Login.js в папке container, чтобы определить этот компонент:
import React from "react";
import axios from "axios";
class Login extends React.Component {
constructor() {
super();
this.state = {
username: '',
password: ''
};
}
handleUsername(event) {
this.setState({ username: event.target.value })
}
handlePassword(event) {
this.setState({ password: event.target.value })
}
handleSubmit (event) {
event.preventDefault();
//authenticate user
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<div className="field">
<label className="label">Username</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.username}
onChange = {this.handleUsername.bind(this)}
/>
</div>
</div>
<div className="field">
<label className="label">Password</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.password}
onChange = {this.handlePassword.bind(this)}
/>
</div>
</div>
<button
type="submit"
value="Submit"
className="button is-primary"
>
Submit
</button>
</form>
)
}
}
export default Login
Эта форма похожа на нашу форму регистрации, за исключением того, что нам нужно сделать что-то еще с отправкой:
handleSubmit (event) {
event.preventDefault();
//authenticate user
}
Давайте также добавим кнопку для ссылки на форму входа из формы регистрации, добавив следующее в Registration.js:
import React from "react";
import axios from "axios";
import { Link } from 'react-router-dom';
class Signup extends React.Component {
constructor() {
super();
this.state = {
username: '',
password: ''
};
}
handleUsername(event) {
this.setState({ username: event.target.value })
}
handlePassword(event) {
this.setState({ password: event.target.value })
}
handleSubmit (event) {
event.preventDefault();
// authenticate user
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit.bind(this)}>
<div className="field">
<label className="label">Username</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.username}
onChange = {this.handleUsername.bind(this)}
/>
</div>
</div>
<div className="field">
<label className="label">Password</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.password}
onChange = {this.handlePassword.bind(this)}
/>
</div>
</div>
<button
type="submit"
value="Submit"
className="button is-primary"
>
Submit
</button>
</form>
<Link
className="button is-info"
to="/login"
>
Login
</Link>
</div>
)
}
}
export default Signup
Теперь у нас есть правильная рендеринг и маршрутизация форм регистрации и входа:

Редакционное примечание. После этого снимка экрана я внес небольшие изменения, поэтому стили кнопок будут немного отличаться.
Затем нам нужно добавить функцию аутентификации пользователя при входе в систему путем сравнения имени пользователя и пароля с хешем имени пользователя и пароля в базе данных.
Аутентификация с использованием Guardian
Для этого воспользуемся библиотекой Guardian.
Во-первых, нам нужно добавить Guardian в наше приложение и в качестве зависимости в mix.exs:
def application do
[mod: {PhoenixUserAuthentication, []},
applications: [
:phoenix, :phoenix_pubsub,
:phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :comeonin, :guardian
]
]
end
defp deps do
[{:phoenix, "~> 1.3.0-rc"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:comeonin, "~> 2.6"},
{:guardian, "~> 0.14"}
]
end
Затем мы получаем эту зависимость, выполнив:
mix deps.get
Откройте config / config.exs и давайте добавим код конфигурации:
# Configures Guardian
config :guardian, Guardian,
issuer: "PhoenixUserAuthentication",
ttl: {30, :days},
verify_issuer: true,
serializer: PhoenixUserAuthentication.GuardianSerializer
Нам также нужно добавить секретный ключ, который мы можем сгенерировать с помощью mix phoenix.gen.secret:
# Configures Guardian
config :guardian, Guardian,
issuer: "PhoenixUserAuthentication",
ttl: {30, :days},
verify_issuer: true,
serializer: PhoenixUserAuthentication.GuardianSerializer,
secret_key: "syR5soTnDOp25yxyU4Y4rL/r0j4v/MNy/5l2gnjRcMwCx/UlWO88C28lWI0UrgJP"
# update for your generated secret key
Если вам интересна эта конфигурация, вы можете проверить официальную документацию. Единственное, что нас сейчас беспокоит, - это создать сериализатор, который мы указали в PhoenixUserAuthentication.GuardianSerializer.
Создайте новый файл в lib / phoenix_user_authentication с именем guardian_serializer.ex:
defmodule PhoenixUserAuthentication.GuardianSerializer do @behaviour Guardian.Serializer alias PhoenixUserAuthentication.Repo alias PhoenixUserAuthentication.Users def for_token(users = %Users{}), do: {:ok, "Users:#{users.id}"} def for_token(_), do: {:error, "Unknown resource type"} def from_token("Users:" <> id), do: {:ok, Repo.get(Users, String.to_integer(id))} def from_token(_), do: {:error, "Unknown resource type"} end
Guardian использует веб-токены JSON для аутентификации. Я настоятельно рекомендую прочитать введение о веб-токенах JSON здесь.
На высоком уровне владение токеном можно рассматривать как аутентификацию пользователя во время сеанса и предоставление им доступа к нашему приложению. Это похоже на владение токеном для доступа к аркадным играм. Приведенный выше код можно рассматривать как предоставление и удаление токена для одного пользователя.
Нам необходимо отслеживать владение пользователем токеном в сеансе в разных точках нашего приложения. В нашем случае это будет вход и выход. Для этого мы будем использовать отдельный контроллер под названием SessionsController с разными функциями для выполнения действий в разных точках нашего приложения.
Чтобы запустить эти функции в SessionsController, нам нужно добавить несколько маршрутов API, которые мы можем сделать в web / router.ex:
scope "/api", PhoenixUserAuthentication do pipe_through :api post "/sessions", SessionsController, :create # login delete "/sessions", SessionsController, :delete # log out get "/users", UsersController, :index post "/users", UsersController, :create end
Нам также нужно добавить несколько плагинов в наш конвейер api:
pipeline :api do plug :accepts, ["json"] plug Guardian.Plug.VerifyHeader, realm: "Bearer" plug Guardian.Plug.LoadResource end
plug Guardian.Plug.VerifyHeader, realm: “Bearer” ищет токен в заголовке и проверяет его. Если есть токен, пользователь будет загружен, чтобы мы могли проверить разрешения и пройти аутентификацию соответствующим образом из SessionsController. Если токен не может быть проверен, ничего не происходит.
Давайте начнем писать функции в SessionsController, как мы указали в наших маршрутах / sessions для аутентификации при входе в систему и выходе из системы.
Создайте файл с именем session_controller.ex в разделе web / controllers.
Во-первых, мы можем добавить функцию создания:
defmodule PhoenixUserAuthentication.SessionsController do
use PhoenixUserAuthentication.Web, :controller
alias PhoenixUserAuthentication.Users
def create(conn, %{"username" => username, "password" => password}) do
case authenticate(%{"username" => username, "password" => password}) do
{:ok, user} ->
new_conn = Guardian.Plug.api_sign_in(conn, user, :access)
jwt = Guardian.Plug.current_token(new_conn)
new_conn
|> put_status(:created)
:error ->
conn
|> put_status(:unauthorized)
end
end
defp authenticate(%{"username" => username, "password" => password}) do
user = Repo.get_by(Users, username: username)
case check_password(user, password) do
true -> {:ok, user}
_ -> :error
end
end
defp check_password(nil, _), do: Comeonin.Bcrypt.dummy_checkpw()
defp check_password(user, password) do
Comeonin.Bcrypt.checkpw(password, user.password_hash)
end
end
Когда HTTP-запрос POST выполняется по маршруту / api / sessions (который будет выполняться при отправке из нашей формы входа), мы вызываем функцию с именем Authenticate, которая получает строка пользователя по имени пользователя с использованием параметра имени пользователя и проверка параметра пароля с сохраненным паролем. Если есть совпадение, возвращается {:ok, user}.
При возврате {:ok, user} мы создаем новое соединение, в котором пользователь аутентифицируется через Guardian.Plug.api_sign_in(conn, user, :access). Мы также получаем текущий веб-токен JSON. Затем мы ставим статус создано. В случае ошибки ставим статус неавторизован.
Когда пользователь выходит из системы и HTTP-запрос DELETE выполняется по маршруту / api / sessions, мы хотим отозвать токен, предоставляющий ему доступ:
def delete(conn, _params) do jwt = Guardian.Plug.current_token(conn) Guardian.revoke(jwt) conn |> put_status(:ok) end
Когда регистрация происходит через HTTP-запрос POST на / api / users, мы хотим также аутентифицировать пользователя, чтобы ему не приходилось входить в систему. Поэтому давайте обновим нашу функцию create в users_controller.ex:
def create(conn, %{"users" => users_params}) do
changeset = Users.changeset(%Users{}, users_params)
case Repo.insert(changeset) do
{:ok, _} ->
user = Repo.get_by(Users, username: username)
new_conn = Guardian.Plug.api_sign_in(conn, user, :access)
jwt = Guardian.Plug.current_token(new_conn)
new_conn
|> put_status(:created)
end
end
Теперь мы не только вставляем новую строку в нашу базу данных, но и аутентифицируем пользователя через Guardian.Plug.api_sign_in(conn, user, :access), а также сохраняем веб-токен JSON (точно так же, как мы это делали при обработке входа в систему).
Последним шагом на стороне сервера является отправка объекта JSON, содержащего данные для текущего пользователя и веб-токен JSON. Мы делаем это, добавляя представление сеанса в web / views / sessions_view.ex.
В этом файле мы вернем объект JSON, содержащий два объекта, которые называются data и meta.
defmodule PhoenixUserAuthentication.SessionsView do
use PhoenixUserAuthentication.Web, :view
def render("show.json", %{user: user, jwt: jwt}) do
%{
data: user_data_json(user),
meta: meta_json(jwt)
}
end
def user_data_json(user) do
%{
id: user.id,
username: user.username
}
end
def meta_json(jwt) do
%{
token: jwt
}
end
end
data содержит идентификатор и имя пользователя, а meta содержит токен.
Давайте вызовем отрисовку show.json в SessionsController после нового соединения:
def create(conn, %{"username" => username, "password" => password}) do
case authenticate(%{"username" => username, "password" => password}) do
{:ok, user} ->
new_conn = Guardian.Plug.api_sign_in(conn, user, :access)
jwt = Guardian.Plug.current_token(new_conn)
new_conn
|> put_status(:created)
|> render("show.json", user: user, jwt: jwt) # new insertion
:error ->
conn
|> put_status(:unauthorized)
end
end
Мы также хотим сделать это для входа в систему в UsersController:
def create(conn, %{"users" => users_params}) do
changeset = Users.changeset(%Users{}, users_params)
case Repo.insert(changeset) do
{:ok, user} ->
new_conn = Guardian.Plug.api_sign_in(conn, user, :access)
jwt = Guardian.Plug.current_token(new_conn)
new_conn
|> put_status(:created)
|> render(PhoenixUserAuthentication.SessionsView, "show.json", user: user, jwt: jwt)
end
end
Вместо того, чтобы писать другое представление, мы можем просто добавить PhoenixUserAuthentication.SessionsView перед вызовом show.json в session_view.ex.
Я знаю! В первый раз аутентификация - это непростая задача, но мы почти закончили!
Давайте попробуем войти в систему и посмотреть, аутентифицирован ли пользователь, обновив web / static / js / container / Login.js, чтобы выполнить HTTP-запрос при отправке:
handleSubmit (event) {
event.preventDefault();
axios({
method: 'post',
headers: {
"Content-Type": "application/json",
},
url: 'http://localhost:4000/api/sessions',
data: {
username: this.state.username,
password: this.state.password
}
})
.then((response) => {
console.log(response);
});
}
Запустите brunch build и попробуйте войти в систему с тем же именем пользователя и паролем при создании пользователя:

После отправки проверьте ответ, который мы вошли в консоль:

Мы видим, что он получил пользователя из базы данных и предоставил токен!
Теперь вы можете использовать ответ, чтобы сообщить React, что пользователь аутентифицирован, и перенаправить пользователя на клиентский маршрут, который требует аутентификации для доступа. Это выходит за рамки данной главы, но вы можете увидеть это полезное руководство.
В заключение давайте также проверим, аутентифицируется ли пользователь при регистрации, как это должно происходить с учетом кода в users_controller.ex.
Чтобы убедиться, что он работает, мы можем зарегистрировать ответ на HTTP-запрос при регистрации в web / static / js / container / Registration.js:
handleSubmit (event) {
event.preventDefault();
axios({
method: 'post',
headers: {"Content-Type": "application/json"},
url: 'http://localhost:4000/api/users',
data: {
users: {
username: this.state.username,
password: this.state.password
}
}
})
.then((response) => {
console.log(response);
});
}
Запустите brunch build перейдите по адресу http: // localhost: 4000 / и зарегистрируйтесь:

Проверьте консоль, и мы должны увидеть следующее:

Woot woot! Пользователь прошел аутентификацию, но был ли он создан в нашей базе данных?
Вы держите пари, да!

Окончательный код
Заключительные мысли
Аутентификация - это настоящий зверь, потому что она всего лишь часть приложения. Однако это одна из тех вещей, которые вы узнаете один раз и можете использовать снова и снова.
Теперь мы знаем суть полнофункциональной веб-разработки, которая включает взаимодействие с базой данных для заполнения нашего пользовательского интерфейса и аутентификации пользователей.
В этой книге осталось осветить лишь несколько вещей. В следующей главе мы рассмотрим двустороннюю связь с использованием встроенных каналов Phoenix.
Глава 9
Подпишитесь на уведомления
Получайте уведомления о выходе каждой главы.
С уважением,
Майк Манджаларди
Основатель Coding Artist