Phoenix 安全限制

2023-12-18 14:49 更新

限制未登錄用戶訪問用戶頁面

目前為止,所有未登錄用戶都可以訪問、操作用戶相關(guān)頁面。我們要加以限制:未登錄用戶只允許使用 :new:create 兩個動作,訪問其余動作時,全部重定向到登錄頁。

首先在 user_controller_test.exs 文件中新增一個測試,具體內(nèi)容看注釋:

diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs
index 26055e3..ac6894e 100644
--- a/test/controllers/user_controller_test.exs
+++ b/test/controllers/user_controller_test.exs
@@ -66,4 +66,18 @@ defmodule TvRecipeWeb.UserControllerTest do
     assert redirected_to(conn) == Routes.user_path(conn, :index)
     refute Repo.get(User, user.id)
   end
+
+  test "guest access user action redirected to login page", %{conn: conn} do
+    user = Repo.insert! %User{}
+    Enum.each([
+      get(conn, Routes.user_path(conn, :index)),
+      get(conn, Routes.user_path(conn, :show, user)),
+      get(conn, Routes.user_path(conn, :edit, user)),
+      put(conn, Routes.user_path(conn, :update, user), user: %{}),
+      delete(conn, Routes.user_path(conn, :delete, user))
+    ], fn conn ->
+      assert redirected_to(conn) == Routes.session_path(conn, :new)
+      assert conn.halted
+    end)
+  end
 end

接下來修改 user_controller.ex 文件中的代碼:

diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex
index b9234b1..7bb7dac 100644
--- a/web/controllers/user_controller.ex
+++ b/web/controllers/user_controller.ex
@@ -1,5 +1,6 @@
 defmodule TvRecipeWeb.UserController do
   use TvRecipeWeb, :controller
+  plug :login_require when action in [:index, :show, :edit, :update, :delete]

   alias TvRecipe.User

@@ -63,4 +64,20 @@ defmodule TvRecipeWeb.UserController do
     |> put_flash(:info, "User deleted successfully.")
     |> redirect(to: Routes.user_path(conn, :index))
   end
+
+  @doc """
+  檢查用戶登錄狀態(tài)
+
+  Returns `conn`
+  """
+  def login_require(conn, _opts) do
+    if conn.assigns.current_user do
+      conn
+    else
+      conn
+      |> put_flash(:info, "請先登錄")
+      |> redirect(to: Routes.session_path(conn, :new))
+      |> halt()
+    end
+  end
 end

我們增加了一個函數(shù)式 plug login_require,并且將它應(yīng)用在控制器中的動作前。

還記得我們的一個流程圖嗎?

conn
|> router
|> pipelines
|> controller
|> view
|> template

這里,我們還能再進一步完善它:

conn
|> router
|> pipelines
|> controller
|> plugs
|> action
|> view
|> template

我們的控制器在執(zhí)行動作前,會按指定順序執(zhí)行一系列 plug。

現(xiàn)在運行測試:

$ mix test
.....................

  1) test renders form for editing chosen resource (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:44
     ** (RuntimeError) expected response with status 200, got: 302, with body:
     <html><body>You are being <a href="/sessions/new">redirected</a>.</body></html>
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:362: Phoenix.ConnTest.response/2
       (phoenix) lib/phoenix/test/conn_test.ex:376: Phoenix.ConnTest.html_response/2
       test/controllers/user_controller_test.exs:47: (test)



  2) test lists all entries on index (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:8
     ** (RuntimeError) expected response with status 200, got: 302, with body:
     <html><body>You are being <a href="/sessions/new">redirected</a>.</body></html>
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:362: Phoenix.ConnTest.response/2
       (phoenix) lib/phoenix/test/conn_test.ex:376: Phoenix.ConnTest.html_response/2
       test/controllers/user_controller_test.exs:10: (test)



  3) test renders page not found when id is nonexistent (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:38
     expected error to be sent as 404 status, but response sent 302 without error
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:570: Phoenix.ConnTest.assert_error_sent/2
       test/controllers/user_controller_test.exs:39: (test)

..

  4) test deletes chosen resource (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:63
     Assertion with == failed
     code:  redirected_to(conn) == Routes.user_path(conn, :index)
     left:  "/sessions/new"
     right: "/users"
     stacktrace:
       test/controllers/user_controller_test.exs:66: (test)



  5) test shows chosen resource (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:32
     ** (RuntimeError) expected response with status 200, got: 302, with body:
     <html><body>You are being <a href="/sessions/new">redirected</a>.</body></html>
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:362: Phoenix.ConnTest.response/2
       (phoenix) lib/phoenix/test/conn_test.ex:376: Phoenix.ConnTest.html_response/2
       test/controllers/user_controller_test.exs:35: (test)



  6) test updates chosen resource and redirects when data is valid (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:50
     Assertion with == failed
     code:  redirected_to(conn) == Routes.user_path(conn, :show, user)
     left:  "/sessions/new"
     right: "/users/1121"
     stacktrace:
       test/controllers/user_controller_test.exs:53: (test)



  7) test does not update chosen resource and renders errors when data is invalid (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:57
     ** (RuntimeError) expected response with status 200, got: 302, with body:
     <html><body>You are being <a href="/sessions/new">redirected</a>.</body></html>
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:362: Phoenix.ConnTest.response/2
       (phoenix) lib/phoenix/test/conn_test.ex:376: Phoenix.ConnTest.html_response/2
       test/controllers/user_controller_test.exs:60: (test)

.......

Finished in 0.4 seconds
37 tests, 7 failures

因為我們前面新增了 login_require 限制,導(dǎo)致舊的測試有 7 個失敗,它們均需要用戶登錄。

怎么測試用戶登錄的情況?

我們有一種選擇是,在每一個測試前登錄用戶,比如這樣:

  test "shows chosen resource", %{conn: conn} do
    user = Repo.insert! User.changeset(%User{}, @valid_attrs)
    conn = post conn, Routes.session_path(conn, :create), session: @valid_attrs # <= 這一行,登錄用戶
    conn = get conn, Routes.user_path(conn, :show, user)
    assert html_response(conn, 200) =~ "Show user"
  end

只是這樣我們會重復(fù)很多代碼。

我們還可以借助 setup。在 Elixir 的測試?yán)铮?code>setup 塊的代碼會在每一個 test 執(zhí)行以前執(zhí)行,它們返回的內(nèi)容合并進 context,然后我們就可以在 test 中獲取到。

但我們在一個測試文件中涉及兩種情況,登錄與未登錄,setup 要如何區(qū)分它們?

我們可以使用 tag。通過 tag,我們在上下文 context 中存儲變量,setup 讀取 context,根據(jù)需求返回不同的數(shù)據(jù)。

我們的代碼改造如下:

diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs
index ac6894e..e11df40 100644
--- a/test/controllers/user_controller_test.exs
+++ b/test/controllers/user_controller_test.exs
@@ -1,10 +1,22 @@
 defmodule TvRecipeWeb.UserControllerTest do
   use TvRecipe.ConnCase

   alias TvRecipe.Repo
   alias TvRecipe.Users
   alias TvRecipe.Users.User
   @valid_attrs %{email: "chenxsan@gmail.com", password: "some content", username: "chenxsan"}
   @invalid_attrs %{}

-  defp create_user(_) do
-    user = fixture(:user)
-    %{user: user}
-  end

+  defp login_user(%{conn: conn}) do
+     user = fixture(:user)
+     conn = post conn, Routes.session_path(conn, :create), session: @valid_attrs
+     %{conn: conn, user: user}
+  end

   describe "edit user" do
-    setup [:create_user]
+    setup [:login_user]

   test "renders form for editing chosen user", %{conn: conn, user: user} do
      conn = get(conn, Routes.user_path(conn, :edit, user))
      assert html_response(conn, 200) =~ "Edit User"
   end
  end

我們根據(jù) describe 設(shè)置不同的 setup:需要用戶登錄則添加 setup [:login_user], 不需要則不添加或留空 setup []

現(xiàn)在運行測試:

$ mix test
...........................

  1) test updates chosen resource and redirects when data is valid (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:66
     ** (RuntimeError) expected redirection with status 302, got: 200
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:443: Phoenix.ConnTest.redirected_to/2
       test/controllers/user_controller_test.exs:69: (test)

.........

Finished in 0.5 seconds
37 tests, 1 failure

我們修復(fù)了大部分的錯誤,但還有一個失敗的。

檢查測試代碼我們可以發(fā)現(xiàn),setup 塊里創(chuàng)建了一個郵箱為 chenxsan@gmail.com、用戶名為 chenxsan 的用戶,而更新時郵箱與用戶名重復(fù)了。

我們調(diào)整一下:

diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs
index e11df40..c8263c6 100644
--- a/test/controllers/user_controller_test.exs
+++ b/test/controllers/user_controller_test.exs
@@ -65,7 +65,7 @@ defmodule TvRecipeWeb.UserControllerTest do
   test "updates chosen resource and redirects when data is valid", %{conn: conn} do
     user = Repo.insert! %User{}
-    conn = put conn, Routes.user_path(conn, :update, user), user: @valid_attrs
+    conn = put conn, Routes.user_path(conn, :update, user), user: %{@valid_attrs | username: "samchen", email: "chenxsan+1@gmail.com"}
     assert redirected_to(conn) == Routes.user_path(conn, :show, user)
     assert Repo.get_by(User, @valid_attrs |> Map.delete(:password))
   end

這樣,我們就修正了所有測試。

限制用戶訪問管理動作

user_controller.ex 文件中,:index:delete 動作通常是管理員才允許使用的,對普通用戶來說,它們應(yīng)該不可見。

我們直接移除相應(yīng)的路由與控制器動作:

diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex
index 7bb7dac..c0056fd 100644
--- a/web/controllers/user_controller.ex
--- a/web/controllers/user_controller.ex
+++ b/web/controllers/user_controller.ex
@@ -1,14 +1,9 @@
 defmodule TvRecipeWeb.UserController do
   use TvRecipeWeb, :controller
-  plug :login_require when action in [:index, :show, :edit, :update, :delete]
+  plug :login_require when action in [:show, :edit, :update]

   alias TvRecipe.User

-  def index(conn, _params) do
-    users = Repo.all(User)
-    render(conn, "index.html", users: users)
-  end
-
   def new(conn, _params) do
     changeset = User.changeset(%User{})
     render(conn, "new.html", changeset: changeset)
@@ -53,18 +48,6 @@ defmodule TvRecipeWeb.UserController do
     end
   end

-  def delete(conn, %{"id" => id}) do
-    user = Repo.get!(User, id)
-
-    # Here we use delete! (with a bang) because we expect
-    # it to always work (and if it does not, it will raise).
-    Repo.delete!(user)
-
-    conn
-    |> put_flash(:info, "User deleted successfully.")
-    |> redirect(to: Routes.user_path(conn, :index))
-  end
-
   @doc """
   檢查用戶登錄狀態(tài)

然后運行測試。測試會幫我們定位出所有需要移除或修正的代碼,我們逐一修改如下:

diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs
index c8263c6..a2ccee0 100644
--- a/test/controllers/user_controller_test.exs
+++ b/test/controllers/user_controller_test.exs
@@ -16,12 +16,6 @@ defmodule TvRecipeWeb.UserControllerTest do
     end
   end

-  test "lists all entries on index", %{conn: conn} do
-    conn = get conn, Routes.user_path(conn, :index)
-    assert html_response(conn, 200) =~ "Listing users"
-  end
     end
   end

-  test "lists all entries on index", %{conn: conn} do
-    conn = get conn, Routes.user_path(conn, :index)
-    assert html_response(conn, 200) =~ "Listing users"
-  end
-
   test "renders form for new resources", %{conn: conn} do
     conn = get conn, Routes.user_path(conn, :new)
     assert html_response(conn, 200) =~ "New user"
@@ -77,22 +71,12 @@ defmodule TvRecipeWeb.UserControllerTest do
     assert html_response(conn, 200) =~ "Edit user"
   end

-  test "deletes chosen resource", %{conn: conn} do
-    user = Repo.insert! %User{}
-    conn = delete conn, Routes.user_path(conn, :delete, user)
-    assert redirected_to(conn) == Routes.user_path(conn, :index)
-    refute Repo.get(User, user.id)
-  end
-
   test "guest access user action redirected to login page", %{conn: conn} do
     user = Repo.insert! %User{}
     Enum.each([
-      get(conn, Routes.user_path(conn, :index)),
       get(conn, Routes.user_path(conn, :show, user)),
       get(conn, Routes.user_path(conn, :edit, user)),
       put(conn, Routes.user_path(conn, :update, user), user: %{}),
-      delete(conn, Routes.user_path(conn, :delete, user))
     ], fn conn ->
       assert redirected_to(conn) == Routes.session_path(conn, :new)
       assert conn.halted
     ], fn conn ->
       assert redirected_to(conn) == Routes.session_path(conn, :new)
       assert conn.halted
diff --git a/web/templates/user/edit.html.eex b/web/templates/user/edit.html.eex
index 7e08f2b..beae173 100644
--- a/web/templates/user/edit.html.eex
+++ b/web/templates/user/edit.html.eex
@@ -2,5 +2,3 @@

 <%= render "form.html", changeset: @changeset,
                         action: user_path(@conn, :update, @user) %>
-
-<%= link "Back", to: user_path(@conn, :index) %>
diff --git a/web/templates/user/new.html.eex b/web/templates/user/new.html.eex
index e0b494f..adf2399 100644
--- a/web/templates/user/new.html.eex
+++ b/web/templates/user/new.html.eex
@@ -2,5 +2,3 @@

 <%= render "form.html", changeset: @changeset,
                         action: user_path(@conn, :create) %>
-
-<%= link "Back", to: user_path(@conn, :index) %>
diff --git a/web/templates/user/show.html.eex b/web/templates/user/show.html.eex
index d05f88d..4c3f497 100644
--- a/web/templates/user/show.html.eex
+++ b/web/templates/user/show.html.eex
@@ -20,4 +20,3 @@
 </ul>

 <%= link "Edit", to: user_path(@conn, :edit, @user) %>
-<%= link "Back", to: user_path(@conn, :index) %>

再次運行測試,全部通過。

限制已登錄用戶訪問他人頁面

我們還有一個問題,就是登錄后的用戶,通過修改 url 地址,能夠訪問他人的用戶頁面,還可以修改他人的信息。

我們需要加以限制:只有用戶自己才可以訪問、修改自己的用戶頁面。

我們有幾種解決辦法:

  1. 不再通過 id 獲取用戶,直接讀取 conn.assigns.current_user。
  2. 把 id 隱藏起來,改用 /profile 這樣的路徑,用戶就無從修改 url 中的 id,不過我們也沒辦法從 url 中獲取 id,只能讀取 conn.assigns.current_user。
  3. 定義一個 plug,檢查用戶訪問的 id 與 conn.assigns.current_user 的 id 是否一致,不一致則跳轉(zhuǎn)。

這里使用第三種辦法。

先定義一個測試:

diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs
index a2ccee0..fd57531 100644
--- a/test/controllers/user_controller_test.exs
+++ b/test/controllers/user_controller_test.exs
@@ -82,4 +82,19 @@ defmodule TvRecipeWeb.UserControllerTest do
       assert conn.halted
     end)
   end
+
+  @tag logged_in: true
+  test "does not allow access to other user path", %{conn: conn, user: user} do
+    another_user = Repo.insert! %User{}
+    Enum.each([
+      get(conn, Routes.user_path(conn, :show, another_user)),
+      get(conn, Routes.user_path(conn, :edit, another_user)),
+      put(conn, Routes.user_path(conn, :update, another_user), user: %{})
+    ], fn conn ->
+      assert get_flash(conn, :error) == "禁止訪問未授權(quán)頁面"
+      assert redirected_to(conn) == Routes.user_path(conn, :show, user)
+      assert conn.halted
+    end)
+  end
+
 end

然后修改 user_controller.ex 文件:

diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex
index c0056fd..520d986 100644
--- a/web/controllers/user_controller.ex
+++ b/web/controllers/user_controller.ex
@@ -1,6 +1,7 @@
 defmodule TvRecipeWeb.UserController do
   use TvRecipeWeb, :controller
   plug :login_require when action in [:show, :edit, :update]
+  plug :self_require when action in [:show, :edit, :update]

   alias TvRecipe.User

@@ -63,4 +64,21 @@ defmodule TvRecipeWeb.UserController do
       |> halt()
     end
   end
+
+  @doc """
+  檢查用戶是否授權(quán)訪問動作
+
+  Returns `conn`
+  """
+  def self_require(conn, _opts) do
+    %{"id" => id} = conn.params
+    if String.to_integer(id) == conn.assigns.current_user.id do
+      conn
+    else
+      conn
+      |> put_flash(:error, "禁止訪問未授權(quán)頁面")
+      |> redirect(to: Routes.user_path(conn, :show, conn.assigns.current_user))
+      |> halt()
+    end
+  end
 end

我們增加了一個 self_require 的 plug,并應(yīng)用到幾個動作上。請注意兩個 plug 的順序,self_require 排在 login_require 后面。

執(zhí)行測試:

$ mix test
....................

  1) test shows chosen resource (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:39
     ** (RuntimeError) expected response with status 200, got: 302, with body:
     <html><body>You are being <a href="/users/2941">redirected</a>.</body></html>
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:362: Phoenix.ConnTest.response/2
       (phoenix) lib/phoenix/test/conn_test.ex:376: Phoenix.ConnTest.html_response/2
       test/controllers/user_controller_test.exs:42: (test)



  2) test renders form for editing chosen resource (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:53
     ** (RuntimeError) expected response with status 200, got: 302, with body:
     <html><body>You are being <a href="/users/2943">redirected</a>.</body></html>
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:362: Phoenix.ConnTest.response/2
       (phoenix) lib/phoenix/test/conn_test.ex:376: Phoenix.ConnTest.html_response/2
       test/controllers/user_controller_test.exs:56: (test)

....

  3) test updates chosen resource and redirects when data is valid (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:60
     Assertion with == failed
     code:  redirected_to(conn) == Routes.user_path(conn, :show, user)
     left:  "/users/2948"
     right: "/users/2949"
     stacktrace:
       test/controllers/user_controller_test.exs:63: (test)



  4) test renders page not found when id is nonexistent (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:46
     expected error to be sent as 404 status, but response sent 302 without error
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:570: Phoenix.ConnTest.assert_error_sent/2
       test/controllers/user_controller_test.exs:47: (test)

.

  5) test does not update chosen resource and renders errors when data is invalid (TvRecipeWeb.UserControllerTest)
     test/controllers/user_controller_test.exs:68
     ** (RuntimeError) expected response with status 200, got: 302, with body:
     <html><body>You are being <a href="/users/2952">redirected</a>.</body></html>
     stacktrace:
       (phoenix) lib/phoenix/test/conn_test.ex:362: Phoenix.ConnTest.response/2
       (phoenix) lib/phoenix/test/conn_test.ex:376: Phoenix.ConnTest.html_response/2
       test/controllers/user_controller_test.exs:71: (test)

......

Finished in 0.5 seconds
36 tests, 5 failures

因為代碼的改動,我們的測試又有失敗的。讓我們修正它們:

diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs
index fd57531..a1b75c6 100644
--- a/test/controllers/user_controller_test.exs
+++ b/test/controllers/user_controller_test.exs
@@ -3,6 +3,7 @@ defmodule TvRecipeWeb.UserControllerTest do

   alias TvRecipe.{Repo, User}
   @valid_attrs %{email: "chenxsan@gmail.com", password: "some content", username: "chenxsan"}
+  @another_valid_attrs %{email: "chenxsan+1@gmail.com", password: "some content", username: "samchen"}
   @invalid_attrs %{}

   setup %{conn: conn} = context do
@@ -36,37 +37,26 @@ defmodule TvRecipeWeb.UserControllerTest do
   end

-  test "shows chosen resource", %{conn: conn} do
-    user = Repo.insert! %User{}
+  test "shows chosen resource", %{conn: conn, user: user} do
     conn = get conn, Routes.user_path(conn, :show, user)
     assert html_response(conn, 200) =~ "Show user"
   end

-  test "renders page not found when id is nonexistent", %{conn: conn} do
-    assert_error_sent 404, fn ->
-      get conn, Routes.user_path(conn, :show, -1)
-  test "renders page not found when id is nonexistent", %{conn: conn} do
-    assert_error_sent 404, fn ->
-      get conn, Routes.user_path(conn, :show, -1)
-    end
-  end
-
-  test "renders form for editing chosen resource", %{conn: conn} do
-    user = Repo.insert! %User{}
+  test "renders form for editing chosen resource", %{conn: conn, user: user} do
     conn = get conn, Routes.user_path(conn, :edit, user)
     assert html_response(conn, 200) =~ "Edit user"
   end

-  test "updates chosen resource and redirects when data is valid", %{conn: conn} do
-    user = Repo.insert! %User{}
-    conn = put conn, Routes.user_path(conn, :update, user), user: %{@valid_attrs | username: "samchen", email: "chenxsan+1@gmail.com"}
+  test "updates chosen resource and redirects when data is valid", %{conn: conn, user: user} do
+    conn = put conn, Routes.user_path(conn, :update, user), user: @another_valid_attrs
     assert redirected_to(conn) == Routes.user_path(conn, :show, user)
-    assert Repo.get_by(User, @valid_attrs |> Map.delete(:password))
+    assert Repo.get_by(User, @another_valid_attrs |> Map.delete(:password))
   end

-  test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do
-    user = Repo.insert! %User{}
+  test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, user: user} do
     conn = put conn, Routes.user_path(conn, :update, user), user: @invalid_attrs
     assert html_response(conn, 200) =~ "Edit user"
   end

運行測試:

$ mix test
...................................

Finished in 0.4 seconds
35 tests, 0 failures

全部通過。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號