檢視原始程式碼 API 驗證

必要條件:本指南假設您已完成 mix phx.gen.auth 指南。

此指南說明如何新增 API 驗證至 mix phx.gen.auth 之上。由於驗證產生器已包含代幣表格,因此我們也使用它儲存 API 代幣,以符合最佳安全性做法。

我們將此指南分成兩部分:擴充內容和外掛程式實作。我們假設已經執行以下 mix phx.gen.auth 指令

$ mix phx.gen.auth Accounts User users

如果您執行了其他指令,應可輕易調整名稱。

新增 API 函式至內容

我們的驗證系統需要兩個函式。一個是建立 API 代幣,另一個是驗證代幣。開啟 lib/my_app/accounts.ex 並新增這兩個新函式

  ## API

  @doc """
  Creates a new api token for a user.

  The token returned must be saved somewhere safe.
  This token cannot be recovered from the database.
  """
  def create_user_api_token(user) do
    {encoded_token, user_token} = UserToken.build_email_token(user, "api-token")
    Repo.insert!(user_token)
    encoded_token
  end

  @doc """
  Fetches the user by API token.
  """
  def fetch_user_by_api_token(token) do
    with {:ok, query} <- UserToken.verify_email_token_query(token, "api-token"),
         %User{} = user <- Repo.one(query) do
      {:ok, user}
    else
      _ -> :error
    end
  end

新函式使用現有的 UserToken 功能性,儲存稱為「api-token」的新型態代幣。由於這是一個電子郵件代幣,因此如果使用者變更電子郵件,代幣就會過期。

請注意,我們將第二個函式命名為 fetch_user_by_api_token,而不是 get_user_by_api_token。由於我們想要在我們的 API 呈現不同的狀態碼,視是否有找到使用者,因此我們傳回 {:ok, user}:error。Elixir 的慣例是將這些函式命名為 fetch_*,而不是 get_*,後者通常會傳回 nil,而不是叢集。

為了確保我們的函式正常運作,我們撰寫測試。開啟 test/my_app/accounts_test.exs 並新增這個新的敘述區塊

  describe "create_user_api_token/1 and fetch_user_by_api_token/1" do
    test "creates and fetches by token" do
      user = user_fixture()
      token = Accounts.create_user_api_token(user)
      assert Accounts.fetch_user_by_api_token(token) == {:ok, user}
      assert Accounts.fetch_user_by_api_token("invalid") == :error
    end
  end

如果您執行測試,測試實際上會失敗。類似下列訊息

1) test create_user_api_token/1 and fetch_user_by_api_token/1 creates and verify token (Demo.AccountsTest)
   test/demo/accounts_test.exs:21
   ** (FunctionClauseError) no function clause matching in Demo.Accounts.UserToken.days_for_context/1

   The following arguments were given to Demo.Accounts.UserToken.days_for_context/1:

       # 1
       "api-token"

   Attempted function clauses (showing 2 out of 2):

       defp days_for_context("confirm")
       defp days_for_context("reset_password")

   code: assert Accounts.verify_api_token(token) == {:ok, user}
   stacktrace:
     (demo 0.1.0) lib/demo/accounts/user_token.ex:129: Demo.Accounts.UserToken.days_for_context/1
     (demo 0.1.0) lib/demo/accounts/user_token.ex:114: Demo.Accounts.UserToken.verify_email_token_query/2
     (demo 0.1.0) lib/demo/accounts.ex:301: Demo.Accounts.verify_api_token/1
     test/demo/accounts_test.exs:24: (test)

如果您願意,請檢視錯誤並自行修正。說明如下。

UserToken 模組預期我們宣告每個代幣的效期,我們尚未針對「api-token」定義效期。長度會因應用程式而異,以及它在安全方面的機敏程度。對於這個範例,我們假設代幣的效期為 365 天。

開啟 lib/my_app/accounts/user_token.ex,找到定義 defp days_for_context 的位置,然後新增一個新的區塊,如下所示

  defp days_for_context("api-token"), do: 365
  defp days_for_context("confirm"), do: @confirm_validity_in_days
  defp days_for_context("reset_password"), do: @reset_password_validity_in_days

現在測試應該會通過,我們準備向前邁進!

API 驗證插件

最後一部份是要加入 API 的驗證。

當我們執行 mix phx.gen.auth 時,它產生了一個 MyAppWeb.UserAuth 模組,其中有幾個插件,這些插件是接收 conn 並自訂我們的請求/回應生命週期的函式。開啟 lib/my_app_web/user_auth.ex 並加入這個新的函式

def fetch_api_user(conn, _opts) do
  with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
       {:ok, user} <- Accounts.fetch_user_by_api_token(token) do
    assign(conn, :current_user, user)
  else
    _ ->
      conn
      |> send_resp(:unauthorized, "No access for you")
      |> halt()
  end
end

我們的函式會接收連結並檢查「authorization」標頭是否已設定為「Bearer TOKEN」,其中「TOKEN」是 Accounts.create_user_api_token/1 回傳的值。如果令牌無效或沒有該使用者,我們會中止請求。

最後,我們需要將這個 plug 加入我們的管道。開啟 lib/my_app_web/router.ex 而且你會找到 API 的管道。我們將這個新的插件加入管道中,像這樣

  pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_api_user
  end

現在你已經可以接收並驗證 API 請求了。請開啟 test/my_app_web/user_auth_test.exs 並撰寫你的測試。你可以使用其他插件的測試作為範本!

你的回合

整體的 API 驗證流程將取決於你的應用程式。

如果你想要在 JavaScript 客戶端使用這個令牌,你會需要稍微修改一下 UserSessionController,以便呼叫 Accounts.create_user_api_token/1 並回傳 JSON 回應並包含傳回的令牌。

如果你想要提供 API 給第三方使用者,你會需要允許他們建立令牌,並將 Accounts.create_user_api_token/1 結果顯示給他們。他們必須將這些令牌儲存在安全的地方,並使用「authorization」標頭將它們納入他們的要求之一。