12/08/2018, 14:28

Authentication with Elixir on Phoenix

Để tiếp tục làm quen, học tập với Elixir on Phoenix, hôm nay chúng ta sẽ tìm hiểu về Authentication với Elixir on Phoenix. Đây là một chức năng mà bất kỳ một hệ thống lớn nào cũng cần phải có. Để cho đơn giản thì mình sẽ sử dụng user_name và password để Authentication với một số trang trong hệ ...

Để tiếp tục làm quen, học tập với Elixir on Phoenix, hôm nay chúng ta sẽ tìm hiểu về Authentication với Elixir on Phoenix. Đây là một chức năng mà bất kỳ một hệ thống lớn nào cũng cần phải có. Để cho đơn giản thì mình sẽ sử dụng user_name và password để Authentication với một số trang trong hệ thống.

1. Chuẩn bị database

Như thường lệ đầu tiên chính là tạo model User có user_name và crypted_password.

mix phoenix.gen.model User users user_name:string:unique crypted_password:string

Dễ thấy lệnh trên sẽ generate ra cho chúng ta model User có user_name và crypted_password kiểu string. Trong đó user_name là giá trị unique. Đừng quên chạy mix ecto.migrate để migrate database. Sửa lại model User để có thể tạo User qua user_name và password chứ ko phải là crypted_password. Khi cập nhật password thì ta lưu lại encryption của nó vào trường crypted_password, cũng giống ruby ta sử dụng bcrypt và ở Phonenix có thư viện comeonin hỗ trợ. Ta thêm {:comeonin, "~> 1.0"} vào trong file mix.exs

# file mix.exs
  defp deps do
    [{:phoenix, "~> 1.2.1"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.0"},
     {:mariaex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.6"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     {:comeonin, "~> 1.0"}]
  end
  • Ta thêm field password với virtual: true để có thể sử dụng thuộc tính password khi tạo mới hay update đối với User

  • Sau khi thêm comeonin vào ta chạy lại mix deps.get để cài thư viện (nó tương tự như thêm gem và chạy bunder install của rails).

  • Generate giá trị crypted_password qua password

    • put_change(:crypted_password, hashed_password(params[:password])) Cụ thể các bạn xem nội dung file dưới:
# web/model/uer.ex

defmodule HelloPhoenix.User do
  use HelloPhoenix.Web, :model

  schema "users" do
    field :user_name, :string
    field :crypted_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, [:user_name, :password])
    |> unique_constraint(:user_name)
    |> put_change(:crypted_password, hashed_password(params[:password]))
  end

  defp hashed_password(password) do
    Comeonin.Bcrypt.hashpwsalt(password)
  end
end

Sau đó ta tạo một user bằng tay để login vào hệ thống. Chạy iex -S mix trong thư mục của project để vào như rails console

changeset = HelloPhoenix.User.changeset(%HelloPhoenix.User{}, %{user_name: "leban", password: "123456"})
HelloPhoenix.Repo.insert(changeset)

Câu lệnh như trên khá là dài đúng không? Hầu như khi gọi các model ra ta đều phải gọi qua tên project (HelloPhoenix) . Ở đây, ta có một cách có thể rút gọn các câu lệnh này mà không cần phải gọi qua HelloPhoenix nữa. Đó là dùng alias. Ta chỉ cần thêm file .iex.exs vào project và thêm các alias mà mình muốn vào. Khi chạy iex -S mix thì Elixir sẽ tự động load nội dung trong đó.

alias HelloPhoenix.Repo
alias HelloPhoenix.User

=> Ta chỉ cần gọi Repo và User là đủ:

changeset = User.changeset(%User{}, %{user_name: "leban", password: "123456"})
Repo.insert(changeset)

2. Tạo trang login, logout

Cũng như bao nhiêu ngôn ngữ khác, ta cứ quen tay mà làm thôi. Tạo trong login chính là tạo trang session#new

  • Tạo controller Sesstion
  # web/controllers/session_controller.ex

defmodule HelloPhoenix.SessionController do
  use HelloPhoenix.Web, :controller

  alias HelloPhoenix.User
  alias HelloPhoenix.Repo

  def new(conn, _params) do
    render conn, "new.html"
  end

  def create(conn, %{"session" => session_params}) do
    user = authenticate(session_params)
    if user do
      conn
      |> put_session(:current_user, user.id)
      |> put_flash(:info, "Logged in")
      |> redirect(to: get_session(conn, :refere_path) || "/")
    else
      conn
      |> put_flash(:info, "Wrong email or password")
      |> render("new.html")
    end
  end

  def delete(conn, _) do
    conn
    |> delete_session(:current_user)
    |> put_flash(:info, "Logged out")
    |> redirect(to: "/")
  end

  defp authenticate(session_params) do
    user = Repo.get_by(User, user_name: String.downcase(session_params["user_name"]))
    user && Comeonin.Bcrypt.checkpw(session_params["password"], user.crypted_password) && user
  end
end

  • Tạo view cho Sessstion#new
<h2>Login</h2>

<%= form_for @conn, session_path(@conn, :create), [name: :session], fn f -> %>
  <div class="form-group">
    <label>User Name</label>
    <%= text_input f, :user_name, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Password</label>
    <%= password_input f, :password, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= submit "Login", class: "btn btn-primary" %>
  </div>
<% end %>

  • Thêm vào router
get    "/login",  SessionController, :new
post   "/login",  SessionController, :create
delete "/logout", SessionController, :delete

Trong view với router thì chắc hẳn các bạn đã quen thuộc. Trong controller thì có một số hàm mới.

  • |> put_session(:current_user, user.id) tạo mới hoặc update session có key là :current_user và value là user.id (Hàm mặc định của conn)
  • |> delete_session(:current_user) xóa session có key là :current_user (Hàm mặc định của conn)
  • Comeonin.Bcrypt.checkpw(password, crypted_password) kiểm tra password này đúng với crypted_password password không (Hàm của thư viện Comeonin)

3. Tạo before_filter cho controller

Tạo một Plug

# web/plugs/authenticate.ex

defmodule HelloPhoenix.Plug.Authenticate do
  import Plug.Conn
  import HelloPhoenix.Router.Helpers
  import Phoenix.Controller

  def init(default), do: default

  def call(conn, default) do
    current_user = get_session(conn, :current_user)
    if current_user do
      assign(conn, :current_user, current_user)
    else
      conn
        |> put_flash(:error, 'You need to be signed in to view this page')
        |> put_session(:refere_path, conn.request_path)
        |> redirect(to: session_path(conn, :new))
    end
  end
end

Thêm plug này vào trong controller nào mà bạn muốn authenticate cho trang đó.

  plug HelloPhoenix.Plug.Authenticate

Khi thêm plug này vào thì nó chạy tương tự với before_action của rails. Với mỗi lần vào một action nào thì nó đều chạy qua hàm call của plug authenticate

4. Sản phẩm

Source code: github

0