路由管道 #

一、管道概述 #

1.1 什么是管道 #

管道(Pipeline)是一系列Plug中间件的集合,用于在请求到达控制器之前或之后进行处理。每个Plug都可以修改请求或响应。

text
Request → Pipeline → Controller → Pipeline → Response
         (Plug 1)
         (Plug 2)
         (Plug 3)

1.2 管道定义 #

elixir
defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

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

二、内置Plug #

2.1 常用内置Plug #

Plug 说明
:accepts 内容协商
:fetch_session 获取Session
:protect_from_forgery CSRF保护
:put_secure_browser_headers 安全头
:fetch_live_flash LiveView Flash
:put_root_layout 根布局

2.2 使用内置Plug #

elixir
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_root_layout, {HelloWeb.Layouts, :root}
  plug :protect_from_forgery
  plug :put_secure_browser_headers
end

三、自定义Plug #

3.1 函数Plug #

elixir
defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :authenticate_user
  end

  defp authenticate_user(conn, _opts) do
    user_id = get_session(conn, :user_id)

    if user_id do
      assign(conn, :current_user, Hello.Accounts.get_user!(user_id))
    else
      conn
      |> put_flash(:error, "Please log in")
      |> redirect(to: ~p"/login")
      |> halt()
    end
  end
end

3.2 模块Plug #

elixir
defmodule HelloWeb.Plugs.Locale do
  import Plug.Conn

  @locales ["en", "zh", "ja"]

  def init(default), do: default

  def call(conn, default) do
    locale =
      conn.params["locale"] ||
      conn.cookies["locale"] ||
      get_req_header(conn, "accept-language") |> parse_locale() ||
      default

    if locale in @locales do
      conn
      |> assign(:locale, locale)
      |> put_resp_cookie("locale", locale, max_age: 365 * 24 * 60 * 60)
    else
      assign(conn, :locale, default)
    end
  end

  defp parse_locale([header | _]) do
    header
    |> String.split(",")
    |> hd()
    |> String.split("-")
    |> hd()
  end

  defp parse_locale(_), do: nil
end

使用模块Plug:

elixir
pipeline :browser do
  plug :accepts, ["html"]
  plug HelloWeb.Plugs.Locale, "en"
end

3.3 认证Plug #

elixir
defmodule HelloWeb.Plugs.Auth do
  import Plug.Conn
  import Phoenix.Controller

  alias Hello.Accounts
  alias HelloWeb.Router.Helpers, as: Routes

  def init(opts), do: opts

  def call(conn, _opts) do
    user_id = get_session(conn, :user_id)

    cond do
      user = conn.assigns[:current_user] ->
        conn

      user = user_id && Accounts.get_user(user_id) ->
        assign(conn, :current_user, user)

      true ->
        assign(conn, :current_user, nil)
    end
  end

  def authenticate_user(conn, _opts) do
    if conn.assigns.current_user do
      conn
    else
      conn
      |> put_flash(:error, "You must be logged in")
      |> redirect(to: Routes.session_path(conn, :new))
      |> halt()
    end
  end

  def require_admin(conn, _opts) do
    if conn.assigns.current_user && conn.assigns.current_user.role == "admin" do
      conn
    else
      conn
      |> put_flash(:error, "Admin access required")
      |> redirect(to: Routes.page_path(conn, :index))
      |> halt()
    end
  end
end

四、管道组合 #

4.1 多个管道 #

elixir
scope "/", HelloWeb do
  pipe_through [:browser, :authenticate]
end

4.2 管道继承 #

elixir
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
end

pipeline :protected do
  plug :browser
  plug HelloWeb.Plugs.Auth, :authenticate_user
end

scope "/", HelloWeb do
  pipe_through :protected

  resources "/posts", PostController
end

4.3 条件管道 #

elixir
scope "/admin", HelloWeb.Admin do
  pipe_through [:browser, :require_admin]

  resources "/users", UserController
end

五、Plug执行顺序 #

5.1 执行顺序 #

elixir
pipeline :browser do
  plug :plug1
  plug :plug2
  plug :plug3
end

执行顺序:

text
请求 → plug1 → plug2 → plug3 → Controller → plug3 → plug2 → plug1 → 响应

5.2 halt停止执行 #

elixir
defp require_auth(conn, _opts) do
  if authenticated?(conn) do
    conn
  else
    conn
    |> redirect(to: "/login")
    |> halt()
  end
end

六、常用Plug示例 #

6.1 请求日志 #

elixir
defmodule HelloWeb.Plugs.RequestLogger do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    start_time = System.monotonic_time(:millisecond)

    Plug.Conn.register_before_send(conn, fn conn ->
      duration = System.monotonic_time(:millisecond) - start_time

      Logger.info("""
      [Request] #{conn.method} #{conn.request_path}
      Status: #{conn.status}
      Duration: #{duration}ms
      """)

      conn
    end)
  end
end

6.2 CORS处理 #

elixir
defmodule HelloWeb.Plugs.CORS do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    conn
    |> put_resp_header("access-control-allow-origin", "*")
    |> put_resp_header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS")
    |> put_resp_header("access-control-allow-headers", "content-type, authorization")
    |> maybe_preflight()
  end

  defp maybe_preflight(%{method: "OPTIONS"} = conn) do
    conn
    |> send_resp(200, "")
    |> halt()
  end

  defp maybe_preflight(conn), do: conn
end

6.3 请求限流 #

elixir
defmodule HelloWeb.Plugs.RateLimit do
  import Plug.Conn
  import Phoenix.Controller

  def init(opts) do
    Keyword.merge([limit: 100, period: 60_000], opts)
  end

  def call(conn, opts) do
    key = get_client_ip(conn)
    limit = Keyword.get(opts, :limit)
    period = Keyword.get(opts, :period)

    case check_rate(key, limit, period) do
      :ok ->
        conn

      :limit_exceeded ->
        conn
        |> put_status(:too_many_requests)
        |> json(%{error: "Rate limit exceeded"})
        |> halt()
    end
  end

  defp get_client_ip(conn) do
    conn.remote_ip
    |> :inet.ntoa()
    |> to_string()
  end

  defp check_rate(_key, _limit, _period) do
    :ok
  end
end

6.4 内容类型检查 #

elixir
defmodule HelloWeb.Plugs.ContentType do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, allowed_types) do
    content_type = get_req_header(conn, "content-type") |> List.first()

    if content_type && allowed_type?(content_type, allowed_types) do
      conn
    else
      conn
      |> send_resp(415, "Unsupported Media Type")
      |> halt()
    end
  end

  defp allowed_type?(content_type, allowed_types) do
    Enum.any?(allowed_types, fn type ->
      String.starts_with?(content_type, type)
    end)
  end
end

七、Session处理 #

7.1 Session配置 #

elixir
defmodule HelloWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello

  @session_options [
    store: :cookie,
    key: "_hello_key",
    signing_salt: "your_salt",
    same_site: "Lax"
  ]

  plug Plug.Session, @session_options
  plug HelloWeb.Router
end

7.2 Session Plug #

elixir
pipeline :browser do
  plug :fetch_session
end

7.3 使用Session #

elixir
defmodule HelloWeb.SessionController do
  use HelloWeb, :controller

  def create(conn, %{"email" => email, "password" => password}) do
    case Hello.Accounts.authenticate_user(email, password) do
      {:ok, user} ->
        conn
        |> put_session(:user_id, user.id)
        |> put_flash(:info, "Logged in successfully")
        |> redirect(to: ~p"/")

      {:error, :invalid_credentials} ->
        conn
        |> put_flash(:error, "Invalid email or password")
        |> render(:new)
    end
  end

  def delete(conn, _params) do
    conn
    |> clear_session()
    |> put_flash(:info, "Logged out successfully")
    |> redirect(to: ~p"/")
  end
end

八、Flash消息 #

8.1 Flash Plug #

elixir
pipeline :browser do
  plug :fetch_session
  plug :fetch_flash
end

8.2 使用Flash #

elixir
def create(conn, params) do
  case Hello.Accounts.create_user(params) do
    {:ok, user} ->
      conn
      |> put_flash(:info, "User created successfully")
      |> redirect(to: ~p"/users/#{user.id}")

    {:error, changeset} ->
      conn
      |> put_flash(:error, "Failed to create user")
      |> render(:new, changeset: changeset)
  end
end

8.3 显示Flash #

heex
defmodule HelloWeb.Layouts do
  use HelloWeb, :html

  embed_templates "layouts/*"
end

九、API管道 #

9.1 API管道配置 #

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

pipeline :api_auth do
  plug HelloWeb.Plugs.ApiAuth
end

scope "/api", HelloWeb.Api do
  pipe_through :api

  post "/login", SessionController, :create
  post "/register", UserController, :create
end

scope "/api", HelloWeb.Api do
  pipe_through [:api, :api_auth]

  resources "/users", UserController, only: [:index, :show, :update, :delete]
end

9.2 API认证Plug #

elixir
defmodule HelloWeb.Plugs.ApiAuth do
  import Plug.Conn
  import Phoenix.Controller

  def init(opts), do: opts

  def call(conn, _opts) do
    with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
         {:ok, user_id} <- verify_token(token),
         user when not is_nil(user) <- Hello.Accounts.get_user(user_id) do
      assign(conn, :current_user, user)
    else
      _ ->
        conn
        |> put_status(:unauthorized)
        |> json(%{error: "Unauthorized"})
        |> halt()
    end
  end

  defp verify_token(token) do
    Hello.Accounts.verify_user_token(token)
  end
end

十、总结 #

10.1 核心概念 #

概念 说明
Pipeline Plug集合
Plug 请求/响应处理器
pipe_through 应用管道
halt 停止管道执行
assign 存储请求级数据

10.2 常用操作 #

elixir
conn
|> assign(:key, value)
|> put_session(:key, value)
|> put_flash(:info, "Message")
|> put_resp_header("header", "value")
|> redirect(to: "/path")
|> halt()

10.3 下一步 #

现在你已经了解了路由管道,接下来让我们学习 控制器基础,深入了解控制器!

最后更新:2026-03-28