インストール
今回はPhoenixのバージョンは1.2.4
Elixirのインストール
brew install elixir
Phoenixのインストール
mix local.hex
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
サンプルプロジェクトの新規作成とDB作成
プロジェクト作成
mix phoenix.new my_app
cd my_app
mix ecto.create
UserリソースのScaffold
今回はcomeoninを使ってパスワードをハッシュ化し、ユーザが入力したパスワードとの比較を簡単にできるようにUser
モデルのハッシュ化したパスワードを保存するカラム名は
password_hash
かencrypted_password
にするのをオススメします!
mix phoenix.gen.html User users email:string encrypted_password:string
mix ecto.migrate
scope "/", MyApp do
pipe_through :browser
# ...
resources "/users", UserController
end
関連モジュールのインストールと設定
defp deps do
[
# ...
{:comeonin, "~> 4.0"},
{:bcrypt_elixir, "~> 0.12.0"},
{:guardian, "~> 0.14"}
]
end
関連モジュールのインストール
mix deps.get
Guardianの設定
公式のドキュメントの設定通りやったのですが、例にあるconfig.exs
ではうまく動かなかったので、注意してください!
内部的にJOSEを使っているのでそれ向けにconfigをかかたないとだめでした。なので、公式に書かれている内容でsecret key
の設定をしました。
Secret Keyの生成
iex -S mix phoenix.server
iex(2)> JOSE.JWS.generate_key(%{"alg" => "HS512"}) |> JOSE.JWK.to_map |> elem(1) |> Map.take(["k", "kty"])
config :guardian, Guardian,
allowed_algos: ["HS512"], # optional
verify_module: Guardian.JWT, # optional
issuer: "MyApp",
ttl: { 30, :days },
allowed_drift: 2000,
verify_issuer: true, # optional
secret_key: %{
"k" => "SET HERE",
"kty" => "oct"
},
serializer: MyApp.GuardianSerializer
Serializerの実装
defmodule MyApp.GuardianSerializer do
@behaviour Guardian.Serializer
alias MyApp.Repo
alias MyApp.User
def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
def for_token(_), do: { :error, "Unknown resource type" }
def from_token("User:" <> id), do: { :ok, Repo.get(User, id) }
def from_token(_), do: { :error, "Unknown resource type" }
end
ユーザの作成
Userモデルの実装
ユーザを新規作成する時にパスワードをハッシュ化する必要があるので、その実装です。
defmodule MyApp.User do
use MyApp.Web, :model
schema "users" do
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:email, :password])
|> validate_required([:email, :password])
|> put_change(:encrypted_password, encrypted_password(params["password"]))
end
defp encrypted_password(password) do
if password do
Comeonin.Bcrypt.hashpwsalt(password)
end
end
end
ユーザのフォームの修正
フォームがencrypted_passwordになっているのでpasswordに変更
<%= 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, :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">
<%= submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>
画面からユーザを新規作成
mix phoenix.server
http://localhost:4000/users/new にアクセスしメールアドレスとパスワードを入力する
ログイン・ログアウト
ログイン画面の実装
defmodule MyApp.User do
use MyApp.Web, :model
schema "users" do
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:email, :password])
|> validate_required([:email, :password])
|> put_change(:encrypted_password, encrypted_password(params["password"]))
end
defp encrypted_password(password) do
if password do
Comeonin.Bcrypt.hashpwsalt(password)
end
end
end
defmodule MyApp.Auth do
alias MyApp.Repo
alias MyApp.User
def valid?(changeset) do
Repo.get_by(User, email: String.downcase(changeset.changes.email))
|> Comeonin.Bcrypt.check_pass(changeset.changes.password)
end
end
defmodule MyApp.SessionController do
use MyApp.Web, :controller
alias MyApp.User
alias MyApp.Auth
def new(conn, _params) do
changeset = User.changeset(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
changeset = User.changeset(%User{}, user_params)
case Auth.valid?(changeset) do
{:ok, user} ->
conn
|> put_flash(:info, "Log In successfully.")
|> Guardian.Plug.sign_in(user)
|> redirect(to: user_path(conn, :index))
{:error, _} ->
conn
|> put_flash(:error, "Invalid password or email")
|> render("new.html", changeset: changeset)
end
end
def delete(conn, %{}) do
conn
|> Guardian.Plug.sign_out
|> put_flash(:info, "Logged out")
|> redirect(to: login_path(conn, :new))
end
end
defmodule MyApp.Router do
use MyApp.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :browser_session do
plug Guardian.Plug.VerifySession # looks in the session for the token
plug Guardian.Plug.LoadResource
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyApp do
pipe_through [:browser, :browser_session] # Use the default browser stack
get "/", PageController, :index
get "/login", SessionController, :new, as: :login
post "/login", SessionController, :create, as: :login
delete "/logout", SessionController, :delete, as: :logout
resources "/users", UserController
end
# Other scopes may use custom stacks.
# scope "/api", MyApp do
# pipe_through :api
# end
end
<h2>Sign In</h2>
<%= render "form.html", changeset: @changeset,
action: login_path(@conn, :create) %>
<%= 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, :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">
<%= submit "Login", class: "btn btn-primary" %>
</div>
<% end %>
defmodule MyApp.SessionView do
use MyApp.Web, :view
end
ログイン
http://localhost:4000/login にアクセスし先程登録したメールアドレスとパスワードを入力するとログインが成功し、間違ったメールアドレスとパスワードのペアだとエラーになります。
ログインが必要なページの実装
UserControllerへはログインが必要になるように実装します。
defmodule MyApp.UserController do
use MyApp.Web, :controller
use Guardian.Phoenix.Controller
alias MyApp.User
plug Guardian.Plug.EnsureAuthenticated, handler: __MODULE__, typ: "access"
def index(conn, _params, _user, _claims) do
users = Repo.all(User)
render(conn, "index.html", users: users)
end
def new(conn, _params, _user, _claims) do
changeset = User.changeset(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}, _user, _claims) do
changeset = User.changeset(%User{}, user_params)
case Repo.insert(changeset) do
{:ok, _user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: user_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, %{"id" => id}, _user, _claims) do
user = Repo.get!(User, id)
render(conn, "show.html", user: user)
end
def edit(conn, %{"id" => id}, _user, _claims) do
user = Repo.get!(User, id)
changeset = User.changeset(user)
render(conn, "edit.html", user: user, changeset: changeset)
end
def update(conn, %{"id" => id, "user" => user_params}, _user, _claims) do
user = Repo.get!(User, id)
changeset = User.changeset(user, user_params)
case Repo.update(changeset) do
{:ok, user} ->
conn
|> put_flash(:info, "User updated successfully.")
|> redirect(to: user_path(conn, :show, user))
{:error, changeset} ->
render(conn, "edit.html", user: user, changeset: changeset)
end
end
def delete(conn, %{"id" => id}, _user, _claims) do
user = Repo.get!(User, id)
# Here we use delete! (with a bang) because we expect
# it to always work (and if it does not, it will raise).
Repo.delete!(user)
conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: user_path(conn, :index))
end
def unauthenticated(conn, _params) do
conn
|> put_flash(:error, "Authentication required")
|> redirect(to: login_path(conn, :new))
end
end
ログアウト
任意の場所に以下のログアウトボタンを記述すればログアウトができるようになります。
<%= link "Logout", to: logout_path(@conn, :delete), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
まとめ
ドキュメントや関連するサンプルのソースを眺めてもはまるところがあったので、簡単ですがまとめてみました。
ElixrやPhoenixはまだまた初心者なので、不備があればツッコミお待ちしています!!
参考にしたページ
- https://github.com/riverrun/comeonin
- https://github.com/ueberauth/guardian
- https://github.com/hassox/phoenix_guardian
- https://hexdocs.pm/comeonin/Comeonin.Bcrypt.html
- https://elixirschool.com/en/lessons/libraries/guardian/#where-to-use-them