查看源代码 API 认证

要求:本指南假设您已经阅读过 mix phx.gen.auth 指南。

本指南演示如何在 mix phx.gen.auth 的基础上添加 API 认证。由于认证生成器已经包含一个 token 表,因此我们使用它来存储 API token,遵循最佳安全实践。

我们将把本指南分为两部分:增强上下文和插件实现。我们假设以下 mix phx.gen.auth 命令已执行

$ mix phx.gen.auth Accounts User users

如果您执行了其他操作,只需修改相应的名称即可。

向上下文添加 API 函数

我们的认证系统需要两个函数。一个用于创建 API token,另一个用于验证它。打开 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”的新 token 类型。由于这是一种电子邮件 token,如果用户更改了他们的电子邮件,token 将过期。

还要注意,我们调用第二个函数为 fetch_user_by_api_token,而不是 get_user_by_api_token。因为我们希望根据用户是否找到渲染不同的状态代码,所以我们返回 {:ok, user}:error。Elixir 的约定是将这些函数称为 fetch_*,而不是 get_*,后者通常返回 nil 而不是元组。

为了确保新函数正常工作,让我们编写测试。打开 test/my_app/accounts_test.exs 并添加这个新的 describe 块

  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 模块期望我们声明每个 token 的有效性,而我们还没有为“api-token”定义一个。长度将取决于您的应用程序以及其在安全性方面的敏感程度。对于此示例,假设 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 返回的值。如果 token 无效或没有这样的用户,我们将中止请求。

最后,我们需要将这个 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 客户端中使用此 token,您需要稍微更改 UserSessionController,以调用 Accounts.create_user_api_token/1 并返回 JSON 响应,并包含返回的 token。

如果您想为第三方用户提供 API,您需要允许他们创建 token,并向他们展示 Accounts.create_user_api_token/1 的结果。他们必须将这些 token 保存在安全的地方,并在使用“authorization”头发出请求时将其包含在内。