路由管道 #
一、管道概述 #
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