Fork me on GitHub

Guardian (Podstawy)

Od ostatniej aktualizacji wprowadzono wiele zmian i tłumaczenie może być nieaktualne.
Od ostatniej aktualizacji wprowadzono kilka dużych zmian.

Guardian jest szeroko używaną biblioteką do obsługi uwierzytelniania bazującą na JWT (JSON Web Token).

Spis treści

JWT

JWT umożliwia użycie rozbudowanego tokenu uwierzytelniania. W przeciwieństwie do innych systemów uwierzytelniania, które udostępniają jedynie informacje o identyfikatorze podmiotu i zasobu, JWT udostępnia dodatkowo:

Dodatkowo Guardian wprowadza pola, pozwalające na zdefiniowanie:

To tylko podstawowe pola JWT. Jeżeli twoja aplikacja wymaga dodatkowych informacji możne je dodać. Jedynym ograniczeniem jest maksymalna długość nagłówka HTTP.

Tak rozbudowana funkcjonalność pozwala na przekazywanie tokenów JWT w ramach całego systemu jako w pełni funkcjonalnych kontenerów na informacje o uwierzytelnieniu.

Gdzie używać?

JWT token może być użyty do uwierzytelniania w dowolnym miejscu systemu i w dowolnej aplikacji.

Tokeny JWT mogą zatem zostać użyte wszędzie tam, gdzie potrzebujemy weryfikacji uwierzytelniania.

Czy potrzebuję bazy danych?

Nie ma potrzeby przechowywania JWT w bazie danych. Na podstawie danych z tokena takich jak, żądający i data wygaśnięcia, można kontrolować udostępniane zasoby. Zazwyczaj korzystamy z bazy danych, bo tam składowane są zasoby, ale samo JWT tego nie wymaga.

Na przykład, jeżeli chcemy użyć JWT do uwierzytelniania komunikacji po UDP, to nie będziemy używać bazy danych. W zamian zapiszemy wszystkie informacje bezpośrednio w tokenie. Po weryfikacji, zakładając, że jest on poprawnie podpisany, możemy już udostępnić zasoby.

Jeżeli jednak zdecydujesz się na użycie bazy danych do przechowywania JWT, to otrzymasz możliwość weryfikacji czy token jest nadal prawidłowy, inaczej czy nie został on unieważniony. Można też wykorzystać bazę danych, by przykładowo unieważnić wszystkie tokeny danego użytkownika. W tym celu Guardian wykorzystuje GuardianDB. Samo GuardianDb używa ‘zaczepów’ Guardian, by przeprowadzić walidację i zapisać lub usunąć dane z bazy. Będziemy jeszcze o tym mówić.

Konfiguracja

Konfiguracja Guardiana jest rozbudowana i ma wiele opcji. Zajmiemy się nimi za chwilę, ale najpierw przygotujmy coś prostego.

Minimalna konfiguracja

Aby rozpocząć, musimy ustawić kilka rzeczy.

Konfiguracja środowiska

W pliku mix.exs:

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

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

W pliku config/config.ex:

# Nadpisz tą wartość w plikach konfiguracyjnych dla poszczególnych środowisk
config :guardian, Guardian,
  issuer: "MyAppId",
  secret_key: Mix.env(),
  serializer: MyApp.GuardianSerializer

Oto minimalna ilość informacji potrzebnych Guardianowi do działania. Oczywiście nie powinniśmy podawać sekretnego klucza w głównym pliku konfiguracyjnym. Każde środowisko powinno mieć własny klucz. O ile typowym jest używanie tego samego klucza w środowisku dev i test, to już środowisko produkcyjne powinno mieć własny, silny klucz wygenerowany na przykład za pomocą polecenia mix phoenix.gen.secret.

Stwórzmy teraz serializer 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

Jest on odpowiedzialny za odnalezienie zasobu, którego identyfikator znajduje się w polu sub (od subject). Może on wykorzystać w tym celu bazę danych, zewnętrzne API, albo nawet ciąg znaków. Jest on też odpowiedzialny za zapis identyfikatora do pola sub.

Tak wygląda minimalna konfiguracja. Oczywiście można tu zrobić znacznie więcej, ale na start wystarczy.

Użycie w aplikacji

Gdy mamy już naszą konfigurację na miejscu, musimy jakoś zintegrować Guardiana z aplikacją. Jako że wykorzystujemy minimalną konfigurację, to zajmijmy się najpierw żądaniami HTTP.

Żądania HTTP

Guardian udostępnia pewną ilość plugów, pozwalających na integrację z protokołem HTTP. O plugach możesz poczytać w lekcji im poświęconej. Guardian nie wymaga Phoenixa, ale użyjemy go tutaj by uprościć przykład.

Najprostszą metodą integracji jest użycie routera, ale jako że sam proces integracji opiera się o mechanizm plugów, to można go użyć wszędzie tam, gdzie mają zastosowanie plugi.

Zasadniczo zasada działania plugu Guardiana jest następująca:

  1. Znajdź token gdzieś w żądaniu: plugi Verify*
  2. Opcjonalnie załaduj identyfikator zasobu: plug LoadResource
  3. Sprawdź, czy token z żądania jest poprawny i jeżeli nie jest, zablokuj dostęp. Plug EnsureAuthenticated.

By zaspokoić wszystkie wymagania programistów, Guardian implementuje powyższe fazy w oddzielnych plugach. By znaleźć token używamy plugów Verify*.

Stwórzmy potok pracy.

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

Potoki te pozwalają na zaspokojenie różnych potrzeb związanych z uwierzytelnianiem. Pierwszy próbuje odnaleźć token w sesji, kolejny w nagłówku, a gdy token zostanie odnaleziony, to ładowane są odpowiednie zasoby.

Drugi z potoków spełnia wymagania co do weryfikacji poprawności tokenu, sprawdzając, czy jego typ, pole typ ma wartość access.

By ich użyć, dodajmy je do naszej aplikacji:

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

Powyższa konfiguracja dla procesu logowania pozwala na uwierzytelnienie użytkownika, jeżeli tylko istnieje. Druga z konfiguracji sprawdza, czy przesłano poprawny token. Oczywiście nie musimy używać potoków i zamiast nich dodać odpowiednie elementy bezpośrednio do kontrolerów, by uzyskać bardzo elastyczne do konfiguracji rozwiązanie, ale tu wybraliśmy najprostsze rozwiązanie.

Jak na razie kompletnie pominęliśmy jedną rzecz. Obsługę błędów dodaną w plugu EnsureAuthenticated. Jest to bardzo prosty moduł zawierający dwie funkcje:

Obie te funkcje jako parametry otrzymują strukturę Plug.Conn oraz mapę parametrów żądania. Powinny obsłużyć odpowiedni rodzaj błędów. Innym rozwiązaniem jest użycie kontrolera z Phoenixa.

W kontrolerze

W kontrolerze mamy kilka różnych sposobów, by otrzymać informacje o aktualnie zalogowanym użytkowniku. Zacznijmy od najprostszego.

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

  def some_action(conn, params, user, claims) do
    # do stuff
  end
end

Używając modułu Guardian.Phoenix.Controller, możemy otrzymać dwa dodatkowe argumenty i wykorzystać dopasowanie wzorców. Należy jednak pamiętać, że jeżeli nie używamy EnsureAuthenticated, to możemy otrzymać nil jako użytkownika.

Inną, bardziej elastyczną i bogatszą w informacje, metodą jest użycie kodu pomocniczego dla plugów.

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
      # No user
    end
  end
end

Logowanie i wylogowanie

Zalogowanie i wylogowanie z wykorzystaniem sesji przeglądarki jest banalnie proste. Kod kontrolera służącego do zalogowania:

def create(conn, params) do
  case find_the_user_and_verify_them_from_params(params) do
    {:ok, user} ->
      # Use access tokens. Other tokens can be used, like :refresh etc
      conn
      |> Guardian.Plug.sign_in(user, :access)
      |> respond_somehow()

    {:error, reason} ->
      nil
      # handle not verifying the user's credentials
  end
end

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

Użycie login API jest trochę inne, ponieważ nie ma tam sesji i musimy samodzielnie odesłać token do użytkownika. W tym celu login API używa nagłówka Authorization. Metoda ta jest przydatna, gdy nie chcemy lub nie możemy wykorzystać mechanizmu sesji.

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
      # handle not verifying the user's credentials
  end
end

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

Mechanizm sesji wykorzystuje pod spodem encode_and_sign, a tu robimy to samodzielnie.


Contributors

loading...



Podziel się