Do you want to pick up from where you left of?
Take me there

Guardian (Cơ bản)

Guardian là một thư viện xác thực danh tính người dùng được sử dụng rộng rãi dựa trên chuẩn JWT (JSON Web Token).

JWTs

Một JWT cung cấp một token với nhiều thông tin để xác thực danh tính người dùng. Trong khi nhiều hệ thống xác thực khác, chỉ cung cấp truy cập tới chủ thể của token, JWT còn cho chúng ta các thông tin khác như :

Guardian cung cấp thêm một số tính năng khác như :

Đây là các fields cơ bản trong JWT. Bạn tùy ý thêm bất cứ thông tin nào cần cho ứng dụng của bạn. Nhớ rằng, nên giữ cho JWT không có quá nhiều trường, để JWT có thể vừa vặn trong HTTP header.

Sự phong phú này cho phép bạn truyền JWTs khắp hệ thống của bạn.

Sử dụng chúng ở đâu

JWT tokens có thể sử dụng xác thực danh tính ở bộ phận bất kỳ của ứng dụng.

JWT tokens có thể sử dụng ở bất cứ chỗ nào trong ứng dụng cần thực hiện hành vi xác thực danh tính.

Tôi có sử dụng cho một cơ sở dữ liệu không?

Bạn không cần kiểm tra JWT qua một cơ sở dữ liệu. Cách đơn giản bạn dựa trên thời điểm tạo và thời điểm hết hạn để điều khiển truy cập. Thường thi bạn sẽ mở cơ sở dữ liệu để tra cứu người dùng nào đó nhưng JWT tự thân nó không cần điều này.

Ví dụ, nếu bạn sử dụng JWT để xác thực danh tính thông qua giao thức UDP, bạn có thể không cần dùng cơ sở dữ liệu. Nén tất cả thông tin bạn cần một cách trực tiếp vào token khi bạn khởi tạo nó. Sau đó bạn có thể kiểm tra tính hợp lệ của token bằng cách kiểm tra xem nó có được mã hoá đúng hay không.

Tuy nhiên bạn có thể sử dụng một cơ sở dữ liệu kiểm soát JWT. Nếu sử dụng cơ sở dữ liệu, bạn có khả năng kiểm tra xem token có còn hợp lệ hay không - tức là nó vẫn chưa bị huỷ bỏ. Hoặc bạn có thể sử dụng các bản ghi trong cơ sở dữ liệu để bắt tất cả các token của user 5 là sẽ bị log out. Điều này khá dễ dàng trong Guardian bởi sử dụng GuardianDb. GuardianDb sử dụng Guardians ‘Hooks’ để thực hiện kiểm tra xác thực, lưu và xóa khỏi DB. Chúng ta sẽ đề cập nó sau.

Thiết lập

Có nhiều lựa chọn cho việc thiết lập Guardian. Chúng ta đề cập tới chúng ở vài điểm nhưng chỉ ở mức rất đơn giản.

Thiết lập tối giản

Để bắt đầu đơn giản bạn chỉ cần vài thứ.

Cấu hình

mix.exs

def application do
  [
    mod: {MyApp, []},
    applications: [:guardian, ...]
  ]
end

def deps do
  [
    {guardian: "~> x.x"},
    ...
  ]
end

config/config.exs

config :guardian, Guardian,
  issuer: "MyAppId",
  secret_key: Mix.env, # trong mỗi tệp tin cấu hình từng môi trường bạn nên ghi đè nó nếu nó là ngoại vi
  serializer: MyApp.GuardianSerializer

Đây chỉ là thiết lập ở mức tối thiểu để bạn sử dụng Guardian. Bạn không nên để khoá bí mật của bạn trực tiếp trong file config.exs. Thay vì đó, mỗi một trường nên có một khoá bí mật riêng. Điều này có thể thực hiện bằng cách thiết lập trong các file config/dev.exs, config/test.exs. Với môi trường staging và production, các khoá này cần phải là các khoá mạnh (e.g: sử dụng mix phoenix.gen.secret để sinh ra)

lib/my_app/guardian_serializer.ex

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

Serializer của bạn đảm nhiệm phần tìm kiếm tài nguyên ở trong trường sub (subject). Nó có thể tìm trong DB, một API hoặc thậm chí trong nội dung một chuỗi đơn giản. Nó cũng chịu trách nhiệm cho việc serializer các tài nguyên trong trường sub.

Đây là cấu hình đơn giản nhất, đủ để chúng ta bắt đầu. Trên thực tế, bạn có thể làm được rất nhiều thứ nếu bạn muốn.

Sử dụng trong ứng dụng

Lúc này chúng ta đã cấu hình xong Guardian, chúng ta cần tích hợp nó vào trong ứng dụng. Bởi vì đây chỉ là cấu hình đơn giản nhất, chúng ta sẽ bắt đầu bằng việc xem xét các request HTTP.

Các yêu cầu trong giao thức HTTP

Guardian cung cấp một số Plugs để dễ dàng nhúng vào HTTP requests. Bạn có thể học về Plug tại đây separate lesson. Guardian làm việc không nhất thiết cần Phoenix, nhưng chúng ta sử dụng Phoenix trong ví dụ dưới đây sẽ dễ dàng mô tả cách hoạt động.

Dễ nhất là sử dụng HTTP qua router - module route của Phoenix. Bởi vì Guardian tích hợp HTTP hoàn toàn dựa trên plugs, bạn có thể sử dụng nó bất kỳ chỗ nào có sử dụng plug.

Luồng tiến trình chung của Guardian plug là:

  1. Tìm ra một token trong request và xác minh nó : Verify* plugs
  2. Tìm ra tài nguyên tương ứng với mỗi token: LoadResource plug
  3. Đảm bảo tính hợp lệ của token đó nếu không từ chối nó. EnsureAuthenticated plug

Để đáp ứng tất cả các nhu cầu của các nhà phát triển ứng dụng, Guardian hiện thực các pha riêng rẽ. Để tìm token sử dụng Verify* plugs.

Hãy cùng tạo một số pipelines.

pipeline :maybe_browser_auth do
  plug(Guardian.Plug.VerifySession)
  plug(Guardian.Plug.VerifyHeader, realm: "Bearer")
  plug(Guardian.Plug.LoadResource)
end

pipeline :ensure_authed_access do
  plug(Guardian.Plug.EnsureAuthenticated, %{"typ" => "access", handler: MyApp.HttpErrorHandler})
end

Các pipelines có thể được sử dụng để tạo các yêu cầu xác thực khác nhau. Pipeline thứ nhất cố gắng tìm kiếm ra token đầu tiên trong phiên làm việc, nếu không có, nó sẽ tìm token trong header. Nếu nó tìm thấy token, nó sẽ đọc/ghi các thông tin cho bạn.

Pipeline thứ 2 cần token hợp lệ, xác nhận hợp lệ token hiện tại và đánh dấu nó “access”. Để sử dụng nó, ta thêm chúng vào scope.

scope "/", MyApp do
  pipe_through([:browser, :maybe_browser_auth])

  get("/login", LoginController, :new)
  post("/login", LoginController, :create)
  delete("/login", LoginController, :delete)
end

scope "/", MyApp do
  pipe_through([:browser, :maybe_browser_auth, :ensure_authed_access])

  resource("/protected/things", ProtectedController)
end

Các login routes ở trên sẽ chứng thực danh tính của user nếu cùng một đối tượng. Scope thứ hai đảm bảo rằng có một token hợp lệ được truyền cho tất cả các actions. Bạn không nhất thiết phải đặt chúng trong các pipelines. Bạn có thể đặt chúng trong các controller để có thể tuỳ biến một cách linh hoạt, tuy nhiên, ở đây chúng ta đã sử dụng cấu hình đơn giản nhất.

Chúng ta chưa nói phần mã sau này dùng. Đó là bắt các lỗi xảy ra khi ấy thêm EnsureAuthenticated plug. Đây là một module rất đơn giản trả về tới user

Cả hai chức năng nhận một struct Plug.Conn và các parameter đầu vào sẽ có các lỗi tương ứng xảy ra. Bạn thậm chí có thể sử dụng một Phoenix controller!

Bên trong controller

Bên trong controller, để truy cập vào user hiện tại đang logged in, chúng ta có một vài cách. Hãy bắt đầu với cách đơn giản nhất.

defmodule MyApp.MyController do
  use MyApp.Web, :controller
  use Guardian.Phoenix.Controller

  def some_action(conn, params, user, claims) do
    # làm gì đó
  end
end

Bằng việc sử dụng Guardian.Phoenix.Controller module, các action sẽ nhận 2 tham trị mà bạn tùy ý sử dụng pattern match cho chúng. Nên nhớ, nếu bạn không sử dụng EnsureAuthenticated bạn có thể nhận giá trị nil cho user và claims.

Mặt khác - chúng ta có cách viết lắt léo/rườm rà hơn sau - là để sử dụng plug helpers.

defmodule MyApp.MyController do
  use MyApp.Web, :controller

  def some_action(conn, params) do
    if Guardian.Plug.authenticated?(conn) do
      user = Guardian.Plug.current_resource(conn)
    else
      # Không phải user
    end
  end
end

Đăng nhập/Thoát

Đăng nhập và thoát của phiên làm việc trên trình duyệt là rất đơn giản. Trong controller login ta viết như sau:

def create(conn, params) do
  case find_the_user_and_verify_them_from_params(params) do
    {:ok, user} ->
      # Ở đây ta dùng access. Các token khác có thể sử dụng, như :resfresh vân vân
      conn
      |> Guardian.Plug.sign_in(user, :access)
      |> respond_somehow()

    {:error, reason} ->
      nil
      # xử lý xảy ra lỗi xác minh user
  end
end

def delete(conn, params) do
  conn
  |> Guardian.Plug.sign_out()
  |> respond_somehow()
end

Khi sử dụng API đăng nhập, nó có chút khác biệt bởi vì ở đó không có dựa trên session và bạn cần cung cấp một raw token - token gốc trở về lại cho người dùng. Hàm này tiện lợi khi bạn không có ý định cho việc sử dụng một session.

def create(conn, params) do
  case find_the_user_and_verify_them_from_params(params) do
    {:ok, user} ->
      {:ok, jwt, _claims} = Guardian.encode_and_sign(user, :access)

      conn
      |> respond_somehow(%{token: jwt})

    {:error, reason} ->
      nil
      # xử lý xảy ra lỗi xác minh user
  end
end

def delete(conn, params) do
  jwt = Guardian.Plug.current_token(conn)
  Guardian.revoke!(jwt)
  respond_somehow(conn)
end

Bản chất việc session đăng nhập trình duyệt gọi encode_and_sign vẫn thế vì vậy bạn có thể sử dụng chúng khá tương tự.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!