限制未登錄用戶訪問用戶頁面
目前為止,所有未登錄用戶都可以訪問、操作用戶相關(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 地址,能夠訪問他人的用戶頁面,還可以修改他人的信息。
我們需要加以限制:只有用戶自己才可以訪問、修改自己的用戶頁面。
我們有幾種解決辦法:
- 不再通過 id 獲取用戶,直接讀取
conn.assigns.current_user
。 - 把 id 隱藏起來,改用
/profile
這樣的路徑,用戶就無從修改 url 中的 id,不過我們也沒辦法從 url 中獲取 id,只能讀取conn.assigns.current_user
。 - 定義一個
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
全部通過。
更多建議: