Current section
Files
Jump to
Current section
Files
lib/cmdc_gateway/plugs/auth.ex
defmodule CMDCGateway.Plugs.Auth do
@moduledoc """
API Key 校验 Plug。
从请求头 `X-API-Key` 或 `Authorization: Bearer <key>` 中提取 API Key,
校验后写入 `conn.assigns.api_key` 和 `conn.assigns.tenant_id`。
## 配置
通过 Application env 配置(`config :cmdc_gateway, CMDCGateway.Plugs.Auth`):
- `:api_keys` — 合法 Key 列表或 `key → tenant_id` 映射
- `["key1", "key2"]` — 所有 Key 归属 `"default"` 租户
- `%{"key1" => "tenant_a", "key2" => "tenant_b"}` — 按 Key 指定租户
- `:skip_auth_paths` — 不需要认证的路径列表,默认 `["/healthz"]`
"""
@behaviour Plug
import Plug.Conn
@impl true
def init(opts), do: opts
@impl true
def call(conn, _opts) do
if skip_auth?(conn.request_path) do
conn
else
do_auth(conn)
end
end
# -----
# Private
# -----
defp do_auth(conn) do
case extract_api_key(conn) do
nil ->
unauthorized(
conn,
"Missing API key. Provide via X-API-Key header or Authorization: Bearer <key>"
)
api_key ->
case validate_key(api_key) do
{:ok, tenant_id} ->
conn
|> assign(:api_key, api_key)
|> assign(:tenant_id, tenant_id)
:error ->
unauthorized(conn, "Invalid API key")
end
end
end
defp extract_api_key(conn) do
case get_req_header(conn, "x-api-key") do
[key | _] ->
key
[] ->
case get_req_header(conn, "authorization") do
["Bearer " <> key | _] -> String.trim(key)
_ -> nil
end
end
end
defp validate_key(api_key) do
keys_config = get_keys_config()
case keys_config do
keys when is_map(keys) ->
case Map.get(keys, api_key) do
nil -> :error
tenant_id -> {:ok, tenant_id}
end
keys when is_list(keys) ->
if api_key in keys, do: {:ok, "default"}, else: :error
_ ->
:error
end
end
defp get_keys_config do
Application.get_env(:cmdc_gateway, __MODULE__, [])
|> Keyword.get(:api_keys, [])
end
@default_skip_paths ["/healthz", "/.well-known/agent.json"]
defp skip_auth?(path) do
skip_paths =
Application.get_env(:cmdc_gateway, __MODULE__, [])
|> Keyword.get(:skip_auth_paths, @default_skip_paths)
path in skip_paths
end
defp unauthorized(conn, message) do
conn
|> put_resp_content_type("application/json")
|> send_resp(401, Jason.encode!(%{error: "unauthorized", message: message}))
|> halt()
end
end