前回では、プロジェクトの作成からユーザー登録機能の作成までを行いました。
引き続き認証まわりを実装していきます。
本記事で、認証機能が完成します。
本テーマは7記事構成になっています。
7記事それぞれで取り扱う内容は以下です。
本テーマは以下の方針で記述します。
完成版の画面イメージは以下のようになっています。
アクセスしてきたユーザーに対し、認証および認可を行う機能を作成してゆきます。
以下の手順でログインページを作っていきます。
認証用関数を追加します。
ユーザー検証が成功した場合には、DBから取得したユーザーデータを返します。
# /lib/lv_chat/accounts.ex
def get_user_by(params) do
Repo.get_by(User, params)
end
def get_user_by_name(name) do
get_user_by(name: name)
end
def authenticate_by_name_and_pass(name, given_pass) do user = get_user_by_name(name)
cond do
user && Pbkdf2.verify_pass(given_pass, user.password_hash) ->
{:ok, user}
user ->
{:error, :unauthorized}
true ->
Pbkdf2.no_user_verify()
{:error, :not_found}
end
end
Routerへセッション操作用のAPIを追加します。
# /lib/lv_chat_web/router.ex
scope "/", RumblWeb do
pipe_through :browser
...
resources "/sessions", SessionController, only: [:new, :create, :delete]
...
end
追加されるのは以下になります。
関数 | url | method | タイミング |
---|---|---|---|
new | /sessions/new | GET | ログインページの取得時 |
create | /sessions/ | POST | ログインボタン押下時 |
delete | /sessions/:id | DELETE | ログアウト選択時 |
/lib/lv_chat_web/controllers/
に session_controller.ex
というファイルを作成し、以下のように実装します。
認証が通ったときのログイン処理では、セッション変数にユーザーIDを格納します。
このセッション情報があれば、認証済みとして扱われます。
ログイン処理については、前回の記事の以下で記述しています。
ログイン処理
# /lib/lv_chat_web/controllers/session_controller.ex
defmodule LvChatWeb.SessionController do
use LvChatWeb, :controller
def new(conn, _) do
render(conn, "new.html")
end
def create(conn, %{"session" => %{"name" => name, "password" => pass}}) do
case LvChat.Accounts.authenticate_by_name_and_pass(name, pass) do {:ok, user} ->
conn
|> LvChatWeb.Auth.login(user)
|> put_flash(:info, "Welcome back!")
|> redirect(to: Routes.page_path(conn, :index))
{:error, _reason} ->
conn
|> put_flash(:error, "無効なユーザー名とパスワードの組み合わせです。")
|> render("new.html")
end
end
def delete(conn, _) do
conn
|> LvChatWeb.Auth.logout()
|> redirect(to: Routes.page_path(conn, :index))
end
end
/lib/lv_chat_web/views/
に session_view.ex
というファイルを作成し、以下のように実装します。
# /lib/lv_chat_web/views/session_view.ex
defmodule LvChatWeb.SessionView do
use LvChatWeb, :view
end
/lib/lv_chat_web/templates/
に session
というディレクトリを作成し、 そこに new.html.eex
というファイルを作成します。
# /lib/lv_chat_web/templates/session/new.html.eex
<h1>Login</h1>
<%= form_for @conn, Routes.session_path(@conn, :create),[as: :session], fn f -> %>
<div>
<%= text_input f, :name, placeholder: "Username" %>
</div>
<div>
<%= password_input f, :password, placeholder: "Password" %>
</div>
<%= submit "Log in" %>
<% end %>
ユーザー情報を :current_user
にアサインします。
適切なユーザー情報がアサインできない場合は、 nil
を指定します。
この処理は認証が必要なすべてのページで必要となるので、Plug
として作成します。
セッション処理を行う Plug
を作成します。
モジュールPlugとして作成します。
そのためには、init/1
と call/2
を実装します。
# /lib/lv_chat_web/controllers/auth.ex
defmodule LvChatWeb.Auth do
import Plug.Conn import Phoenix.Controller
alias LvChatWeb.Router.Helpers, as: Routes
def init(opts), do: opts
def call(conn, _opts) do user_id = get_session(conn, :user_id)
cond do
user = conn.assigns[:current_user] ->
put_current_user(conn, user)
user = user_id && LvChat.Accounts.get_user!(user_id) ->
put_current_user(conn, user)
true ->
assign(conn, :current_user, nil)
end
end
...
end
作成したPlugをRouterのパイプラインに仕込みます。
# /lib/lv_chat_web/router.ex
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug LvChatWeb.Authend
これでbrowserパイプラインを通るアクセス全てに、作成したPlugの処理が通るようになります。
制限ページへのアクセスに対する振り分けを行います。
パイプラインで LvChatWeb.Auth
を通ってきたことが前提となります。
さきほど作成した Plug
により、ユーザー情報が読み込まれている場合にはそのまま通過させます。
読み込まれていない場合にはログインページへリダイレクトします。
current_user | 処理 |
---|---|
値が存在 | そのまま通過 |
nil | ログインページへ |
# /lib/lv_chat_web/controllers/auth.ex
def authenticate_user(conn, _opts) do
if conn.assigns.current_user do
conn
else
conn
|> put_flash(:error, "本ページにアクセスするにはログインが必要です。")
|> redirect(to: Routes.session_path(conn, :new))
|> halt()
end
end
認証処理用関数をcontrollerやrouterで使用できるようにこの関数をインポートしておきます。
# /lib/lv_chat_web.ex
def controller do
quote do
use Phoenix.Controller, namespace: LvChatWeb
import Plug.Conn
import LvChatWeb.Gettext
import LvChatWeb.Auth, only: [authenticate_user: 2] alias LvChatWeb.Router.Helpers, as: Routes
end
end
# lib/lv_chat_web.ex
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
import LvChatWeb.Auth, only: [authenticate_user: 2] end
end
現状のNavBarについて、以下の情報が表示されるように変更します。
ステータス | 表示要素 |
---|---|
非ログイン時 |
|
ログイン時 |
|
具体的には、現状の以下の部分を
# /lib/lv_chat_web/templates/layout/app.html.eex
<nav role="navigation">
<ul>
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
</ul>
</nav>
以下のように変更します。
# /lib/lv_chat_web/templates/layout/app.html.eex
<nav role="navigation">
<ul>
<%= if @current_user do %> <li><%= @current_user.name %></li> <li> <%= link "Log out", to: Routes.session_path(@conn, :delete, @current_user), method: "delete" %> </li> <% else %> <li><%= link "Register", to: Routes.user_path(@conn, :new) %></li> <li><%= link "Log in", to: Routes.session_path(@conn, :new) %></li> <% end %> </ul>
</nav>
これで認証部分は完了です。
セッション処理を行うプラグインを作成します。
今まで作成したページの中に、認証が必要なものとそうでないものがありました。
それを分け、認証が必要なページには通常のパイプラインのあとに :authenticate_user/2
を呼び出すようにします。
今まで作成したページの中では以下のようになります。
ログインが必要? | ページ |
---|---|
いいえ |
|
はい |
|
これらをもとに以下のように実装します。
# /lib/lv_chat_web/router.ex
scope "/", LvChatWeb do
pipe_through :browser
resources "/users", UserController, only: [:new, :create]
resources "/sessions", SessionController, only: [:new, :create]
end
scope "/", LvChatWeb do
pipe_through [:browser, :authenticate_user]
get "/", PageController, :index
resources "/sessions", SessionController, only: [:delete]
end
上記において、1つ目の scope
ブロックが認証を必要としないものであり、2つ目が認証を必要とするものです。
本記事で、認証機能が完成しました。
いよいよ次回は、チャット機能に関係するところに入っていきます。
次回は以下になります。