查看源代码 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”头发出请求时将其包含在内。