Phoenix1.3にはCookieを使ったSession機能が最初から提供されています。ですからPhoenixでマルチユーザのアプリを作るにはSessionを使うのが基本でしょう。前に「Phoenix1.3+Guardian1.0でJWT - Qiita」という記事を書きましたが、順番が逆でした。以下、簡単にマルチユーザシステムのためにSession機能を使って、ユーザ登録やログイン画面を作ってみたいと思います。
#1.キャプチャー画面
これから行う設定作業のイメージを明確にするために、出来上がった画面のキャプチャーを貼っておきます。
1-1.User登録画面
パス = /users/new
User登録画面です。(yamada userを登録します)
1-2.ログイン画面(未ログイン)
パス = /login
まだログインしていない状態でログイン画面を表示しています。(yamada userでログインします)
サーバ側でログインに成功するとput_sessionでログインuserを記憶します。
1-3.ログイン画面(ログイン済み)
パス = /login
ログイン画面を表示する前に、サーバ側でget_sessionで記憶したuserを呼び出し、画面に表示します。(yamada userが表示されます)
#2.プロジェクト作成
まずプロジェクトを作成します。
mix phx.new multi_users
cd multi_users
mix ecto.create
password hashingライブラリのComeoninをインストールします。前のバージョンでは不要だったと思いますが、Comeonin 4 からはbcrypt_elixirを別途インストールする必要があります。
[riverrun/comeonin - github]
(https://github.com/riverrun/comeonin/wiki)
#
defp deps do
[
{:phoenix, "~> 1.3.0"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.2"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:comeonin, "~> 4.0.3"}, # 追加
{:bcrypt_elixir, "~> 1.0.4"} # 追加
]
#
インストールします。
mix do deps.get, compile
#3.Sessionの初期設定
Phoenixでsessionがどのように初期設定されているかを確認します。確認だけです。
endpoint.exにはcookieを使うように設定されています。
#
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session,
store: :cookie,
key: "_multi_users_key",
signing_salt: "JdJp4tZU"
#
routerをみると必ずfetch_sessionを通り、常にsessionが復元されていることが確認できます。
defmodule MultiUsersWeb.Router do
use MultiUsersWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session # これ
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
#
#4.User Accounts(User)
次にUserアカウントに必要なリソースを生成します。phx.gen.html Generatorで自動生成します。今回の作業には不要なものも生成されますが無視しておきます。どのようなファイルが生成されるかは以下の過去記事に説明してあります。
「Phoenix1.3の基本的な仕組み - Qiita」
mix phx.gen.html Accounts User users username:string email:string encrypted_password:string
routerに /users パスを追加します。
#
scope "/", MultiUsersWeb do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/users", UserController # 追加
end
#
end
User Schemaを修正します。passwordとpassword_confirmationを追加します。それらは入力フォームで入力される必要がありますが、テーブルに反映されないので、virtual: true を指定します。passwordとpassword_confirmationはハッシュ化されてencrypted_passwordとなり、encrypted_passwordがテーブルに保存されることになります。
defmodule MultiUsers.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :username, :string
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true # 追加
field :password_confirmation, :string, virtual: true # 追加
timestamps()
end
def changeset(%User{}=user, attrs) do
user
### 入力にはpasswordとpassword_confirmationが必要です。
|> cast(attrs, [:username, :email, :password, :password_confirmation])
### password==password_confirmationをチェックを行います。
|> validate_confirmation(:password, message: "does not match password!")
### passwordをハッシュ化してencrypted_passwordとしてchangesetに保存します。
|> encrypt_password()
### changesetに必要な項目がそろっていることを確認します。
|> validate_required([:username, :email, :encrypted_password])
end
### changeからpasswordをgetし、ハッシュ化しencrypted_passwordにputする。
def encrypt_password(changeset) do
with password when not is_nil(password) <- get_change(changeset, :password) do
### 入力されたpasswordをハッシュ化して保存
put_change(changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password))
else
_ -> changeset
end
end
end
ここでvalidate_confirmationはEcto.Changesetの関数で、xxxxxとxxxxx_confirmationが等しいことをチェックする関数です。xxxxx_confirmationはconfirmationパラメータと呼ばれます。
https://hexdocs.pm/ecto/Ecto.Changeset.html
form.html.eexを修正して、encrypted_passwordの入力欄を削除して、代わりにpasswordとpassword_confirmationを入力するようにします。
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="form-group">
<%= label f, :username, class: "control-label" %>
<%= text_input f, :username, class: "form-control" %>
<%= error_tag f, :username %>
</div>
<div class="form-group">
<%= label f, :email, class: "control-label" %>
<%= text_input f, :email, class: "form-control" %>
<%= error_tag f, :email %>
</div>
<div class="form-group">
<%= label f, :password, class: "control-label" %>
<%= text_input f, :password, class: "form-control" %>
<%= error_tag f, :password %>
</div>
<div class="form-group">
<%= label f, :password_confirmation, class: "control-label" %>
<%= text_input f, :password_confirmation, class: "form-control" %>
<%= error_tag f, :password_confirmation %>
</div>
<div class="form-group">
<%= submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>
#5. ログイン画面(Session)
ログイン画面の機能を追加するために、SessionControllerを作成し、以下の3つのパスをrouterに追加する必要があります。
#
scope "/", MultiUsersWeb do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/users", UserController
resources "/sessions", SessionController, only: [:create] # 追加
get "/login", SessionController, :new # 追加
get "/logout", SessionController, :delete # 追加
end
#
session_controller.exを作ります。
defmodule MultiUsersWeb.SessionController do
use MultiUsersWeb, :controller
alias MultiUsers.Accounts
def new(conn, _) do
current = get_session(conn, :user) # (1) sessionから取得
render(conn, "new.html", current: current)
end
def delete(conn, _) do
conn
|> delete_session(:user) # (2) sessionを削除
|> put_flash(:info, "Logged out successfully!")
|> redirect(to: "/")
end
def create(conn, %{"username" => username, "password" => password}) do
with user <- Accounts.get_user_by_username(username),
{:ok, login_user} <- login(user, password)
do
conn
|> put_flash(:info, "Logged in successfully!")
# (3) sessionに保存
|> put_session(:user, %{ id: login_user.id, username: login_user.username, email: login_user.email })
|> redirect(to: "/")
else
{:error, _} ->
conn
|> put_flash(:error, "Invalid username/password!")
|> render("new.html")
end
end
def login(user, password) do
### 入力されたpasswordが保存されているハッシュ値に等しいかをチェック
Comeonin.Bcrypt.check_pass(user, password)
end
end
context moduleに新関数get_user_by_usernameを追加します。
#
def get_user_by_username(username) do
Repo.get_by(User, username: username)
end
#
ログイン画面の定義new.html.eexです。controllerでget_sessionで取得したcurrentがnullでない場合は、すでにログイン済みと判断しusernameを表示します。
<div>
<%= if @current do %>
<div class="alert alert-danger">
<p> Already loggined ! : <%= @current.username %> </p>
</div>
<% end %>
<%= form_tag session_path(@conn, :create) do %>
<label>
Username:<br />
<input type="text" name="username">
</label>
<br />
<label>
Password:<br />
<input type="password" name="password">
</label>
<br />
<%= submit "Login" %>
<% end %>
</div>
#6. アクセス制限
さて現状では、ログインユーザでも非ログイン状態でも、/users パスにアクセスすると、ユーザ一覧が表示されます。これをログインユーザのみに制限したいと思います。
##6-1. Plug Module
アクセス制限を行うためのPlug moduleを以下のように定義します。Plug moduleとして実現しておけば、いつでもどこからでも呼び出すことができます。
https://hexdocs.pm/phoenix/plug.html
defmodule MultiUsersWeb.AuthPlug do
import Plug.Conn, only: [get_session: 2, halt: 1]
import Phoenix.Controller, only: [put_flash: 3, redirect: 2]
def init(opts), do: opts
def call(conn, _opts) do
case get_session(conn, :user) do
nil ->
conn
|> put_flash(:error, "まだログインしていません。ログインする必要がありま
す。")
|> redirect(to: "/")
|> halt()
_ -> conn
end
end
end
ユーザ一覧表示時に上で定義したPlug module (AuthPlug)を呼び出すように、UserControllerに以下の一行を追加します。
defmodule MultiUsersWeb.UserController do
use MultiUsersWeb, :controller
alias MultiUsers.Accounts
alias MultiUsers.Accounts.User
plug MultiUsersWeb.AuthPlug when action in [:index] # これを追加
def index(conn, _params) do
users = Accounts.list_users()
render(conn, "index.html", users: users)
end
#
##6-2.未ログインで/usersにアクセス
アクセス制限されて、トップページにredirectされます。
##6-3.ログイン済で/usersにアクセス
アクセス制限をパスして、ユーザ一覧が表示されます。
以上です。
■ Elixir/Phoenixの基礎についてまとめた過去記事
Elixir Ecto チュートリアル - Qiita
Elixir Ecto のまとめ - Qiita
[Elixir Ecto Association - Qiita]
(https://qiita.com/sand/items/5581497972473e308f05)
Phoenix1.3の基本的な仕組み - Qiita
Phoenixのログイン管理のためのSessionの使い方 - Qiita
Phoenix1.3のUserアカウントとSession - Qiita
Phoenix1.3+Guardian1.0でJWT - Qiita
■JWTを使った認証アプリ例
マルチユーザ対応geolocationアプリ - Elm + Phoenix -Qiita