Phoenix框架基于用户角色的模板部分渲染权限控制技术问询
嘿,这个需求在Phoenix项目里太常见了,我给你梳理一套清晰的实现步骤,从模板渲染到控制器权限保护都覆盖到,确保既满足前端展示需求,又保证后端安全:
1. 确认用户模型的角色字段
首先确保你的User模型已经包含:role字段(字符串类型,值为"user"或"admin")。如果之前用phx.gen.html生成时没加,需要跑个数据库迁移补上,但你说项目已经有用户和角色,这里默认已经配置好,比如在lib/your_app/users/user.ex里有类似定义:
field :role, :string, default: "user"
2. 在视图中添加权限判断辅助函数
Phoenix的视图是存放模板辅助逻辑的最佳位置,打开lib/your_app_web/views/user_view.ex,添加两个权限判断函数:
# 判断是否有权限编辑用户 def can_edit?(current_user, target_user) do current_user.role == "admin" || current_user.id == target_user.id end # 判断是否有权限删除用户(这里可以根据需求调整,比如普通用户不能删自己) def can_delete?(current_user, target_user) do current_user.role == "admin" end
3. 修改模板实现条件渲染
打开用户列表模板lib/your_app_web/templates/user/index.html.heex,找到表格里的操作按钮部分,把原来固定渲染的按钮改成条件渲染:
<td> <!-- Show按钮所有用户都能看,保持不变 --> <span><%= link "Show", to: Routes.user_path(@conn, :show, user) %></span> <!-- 只有满足编辑权限时才渲染Edit按钮 --> <%= if can_edit?(@current_user, user) do %> <span><%= link "Edit", to: Routes.user_path(@conn, :edit, user) %></span> <% end %> <!-- 只有满足删除权限时才渲染Delete按钮 --> <%= if can_delete?(@current_user, user) do %> <span><%= link "Delete", to: Routes.user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"] %></span> <% end %> </td>
注意:要确保控制器已经把当前登录用户current_user传给模板,比如在UsersController的index动作里:
def index(conn, _params) do users = Users.list_users() current_user = conn.assigns.current_user # 假设已通过认证中间件赋值 render(conn, "index.html", users: users, current_user: current_user) end
4. 控制器层面的权限保护(重中之重!)
模板隐藏按钮只是前端视觉控制,必须在控制器里做后端权限校验,不然用户可以直接构造URL访问编辑/删除接口。打开lib/your_app_web/controllers/users_controller.ex,修改edit、update、delete动作:
先在控制器里定义和视图一致的权限判断函数(或者直接调用视图的函数,不过控制器单独定义更清晰):
defp can_edit?(current_user, target_user) do current_user.role == "admin" || current_user.id == target_user.id end defp can_delete?(current_user, target_user) do current_user.role == "admin" end
然后修改对应动作:
def edit(conn, %{"id" => id}) do user = Users.get_user!(id) current_user = conn.assigns.current_user if can_edit?(current_user, user) do changeset = Users.change_user(user) render(conn, "edit.html", user: user, changeset: changeset) else conn |> put_flash(:error, "你没有权限编辑该用户") |> redirect(to: Routes.user_path(conn, :index)) end end def update(conn, %{"id" => id, "user" => user_params}) do user = Users.get_user!(id) current_user = conn.assigns.current_user if can_edit?(current_user, user) do case Users.update_user(user, user_params) do {:ok, user} -> conn |> put_flash(:info, "用户更新成功") |> redirect(to: Routes.user_path(conn, :show, user)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "edit.html", user: user, changeset: changeset) end else conn |> put_flash(:error, "你没有权限更新该用户") |> redirect(to: Routes.user_path(conn, :index)) end end def delete(conn, %{"id" => id}) do user = Users.get_user!(id) current_user = conn.assigns.current_user if can_delete?(current_user, user) do {:ok, _user} = Users.delete_user(user) conn |> put_flash(:info, "用户删除成功") |> redirect(to: Routes.user_path(conn, :index)) else conn |> put_flash(:error, "你没有权限删除该用户") |> redirect(to: Routes.user_path(conn, :index)) end end
5. 可选:用权限库简化复杂逻辑
如果以后你的权限规则变复杂(比如新增角色、更细的权限粒度),可以考虑用bodyguard这类Elixir权限库,它能帮你更系统地管理权限,避免重复写判断逻辑。不过对于当前的简单场景,上面的手动判断已经足够灵活好用了。
内容的提问来源于stack exchange,提问作者Sorebrez

