defmodule CMDCGateway.Plugs.Auth do @moduledoc """ API Key 校验 Plug。 从请求头 `X-API-Key` 或 `Authorization: Bearer ` 中提取 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 " ) 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