在 PHP 中构建一个用户类,使其更安全


Building a User class in PHP, making it more secure

我正在从头开始构建一个用户类,用于学习体验和工作项目。我需要更多地了解登录安全性,我创建的这个类是根据网络上的几个示例构建的,然后根据对讨论和其他文档的批评进行了更新。

我的msqli_object不属于这个类,但我把它包括在内进行测试。它实际上是我MyApp_DB类的一部分,但是因为这个类的[缺乏]安全性可能是我代码中最大的漏洞,所以我在这里想要它。换句话说,我知道它需要与我的 User 类分开。我没有包括 SessionManager 类,因为我还没有完成构建它。

班级..."有效",但我的代码中有几个问题,我觉得我正在用破碎的块建造一座房子:

  1. 我没有mysqlnd,所以我无法访问mysqli_stmt::get_result()。为了将 mysqli 对象保留为类的成员而不是全局(因此难以定位)变量,我不得不使用 eval() 作为解决方法,直到我弄清楚如何将数组的成员作为一组参数发送。任何帮助都会很棒。
  2. 我不知道这段代码总体上有多安全。一旦$hash变量用完,我就会销毁它,并且我的eval()行不使用任何用户输入。我认为我对password_verify()功能的理解不够好,无法知道我是否正确使用它
  3. 代码文档...我一直记录我的代码,但只是为了我自己。我现在正在从事的项目将有几个人和我一起工作,我需要能够干净地记录。我正在安装 PHPdoc,但我还没有真正弄清楚整个标记系统。
  4. 代码结构。欢迎对我的结构提出任何批评,以使其易于阅读
  5. 逻辑
  6. ——我的逻辑和方法论是否接近合理?

    class User
    {
        public $user_id;
        public $first_name;
        public $last_name;
        public $username;
        public $email;
        public $ip_address;
        public $is_logged_in = false;
        public $errors = false;
        public $login_type;
        private $session_id = false;
        private $mysqli;
        public function __construct()
        {
            $this->ip_address = $_SERVER["REMOTE_ADDR"];
            //borrowed from SessionManager class, needed for testing
            $this->mysqli = new mysqli(MYAPP_DB_HOST, MYAPP_DB_USER, MYAPP_DB_PW, MYAPP_DB_NAME);
        }
        public function __destruct()
        {
            //clean up... necessary?
            unset($this->mysqli);
        }
        /**
         * Comes from my SessionManager class, but here for testing and convenience
         */
        private function _db_select($table, $cols, $conds, $params)
        {
            $results = false;
            $query  = "SELECT "
                    . implode($cols, ", ")
                . " FROM "
                    . $table
                . " WHERE "
                    . implode($conds, " ")
                    . " LIMIT 1";
            if ( $stmt = $this->mysqli->prepare($query) )
            {
                foreach ($params as $param)
                {
                    $stmt->bind_param($param[0], $param[1]);
                }
                $stmt->execute();
                //Use the parameters to bind results, in leu of msql_stmt::get_result()
                //Be sure there is no user data here from $params! Better way to do this?
                eval('$stmt->bind_result($results["' . implode($cols, '"], $results["') . '"]);');
                $stmt->fetch();
                $stmt->close();
            }
            return $results;
        }
        /**
         * Populate the user class with an actual user
         */
        public function getByUserName($username)
        {
            $this->username = $username;
            $user_data = $this->_db_select(
                "users",
                ["user_id", "first_name", "last_name", "email",],
                ["username =?",],
                [ [ "s", $username ], ]);
            foreach($user_data as $key => $value)
            {
                $this->{$key} = $value;
            }
        }
        /*
         * Separate logon function, so User can still be used as a tool admin
         * passing by reference &SessionManager class as parameter
         */
        public function logon($user_submit_password, $session_mgr)
        {
            $user_check = $this->_db_select(
                "users",
                ["hash",],
                ["username =?",],
                [ [ "s", $this->username ], ]);
            if ( !$session_mgr::get_session())
            {
                if ( password_verify($user_submit_password, $user_check["hash"]) )
                {
                    $this->login_type = "form_submit";
                    $this->is_logged_in = $session_mgr::start_session($this->username, $this->ip_address, $this->mysqli);
                }
                else
                {
                    $this->error("Login Error.", "The information you entered is not valid. Please try again, or contact the system administrator.");
                }
            }
            else
            {
                $this->login_type = "session";
                //returns true if the ip address in DB for the given session matches the user's ip address
                if ( $session_mgr::check_session() )
                {
                    $this->is_logged_in = true;
                }
                else
                {
                    $this->error("Session Expired.", "Your session is not valid, please log in again.");
                }
            }
            //get rid of password hash so it can't be dumped (does this do anything, really?)
            unset($user_check->hash);
            return $this->is_logged_in;
        }
        //Plan to add more functionality here
        private function error($name = "Unknown Error.", $message = "An unknown error occurred processing your request. Please try again later.", $target = false)
        {
            array_push($this->errors, ["name" => $name, "message" => $message, "target" => $target]);
        }
    }
    

综上所述,我在构建此 User 类时犯了哪些错误?我试图根据我所知道的,借用逻辑,而不是来自网络上不同地方的代码,尽可能多地编写它。从某种意义上说,它有效,我没有任何错误,但我想学习更干净地写作并学习标准。

我的逻辑和方法论是否接近合理?

不幸的是,没有。

public function __construct()

假设您要在一个页面上列出 100 个用户。您的脚本将创建多少个 mysql 连接?

public function __construct($mysqli) {
    $this->mysqli = $mysqli;

一定是这样。

private function _db_select($table, $cols, $conds, $params)

这个有两个问题。

  1. 您的用户不是 SQL 驱动的关系数据库。不能对用户运行 SQL 查询。用户只能使用数据库服务。因此,在 User 类中根本不应该有这样的函数。
  2. 老实说,整个功能是一团糟。您正在尝试节省自己输入的几个单词,即 SELECTFROMWHERE .为此,您正在用美观且可读的SQL制作丑陋的胡言乱语。更不用说此函数支持的 SQL 子集小得离谱。

接受一些建议;创建一个运行任意SQL的函数,而不是这个弗兰肯斯坦。 或者寻找一个现成的查询生成器

使用PDO也是一个

好主意,因为使用PDO,您将不需要邪恶的eval或其他肮脏的技巧。

以下是使用 PDO 重写的函数:

public function getByUserName($username)
{
    $sql = "SELECT user_id, username, first_name, last_name, email FROM users WHERE username=?";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute([$username]);
    $stmt->setFetchMode(PDO::FETCH_INTO, $this);
    $stmt->fetch();
}

使用 PDO 创建一个抽象的数据库类。这可能是您使用 mysql_* 时犯的最大错误。阅读更多关于PDO的信息并实现它。

您还可以阅读有关一些基本项目结构的更多信息。您有模型/服务、控制器和存储库。存储库的想法是与数据库通信。你在那里写你的查询。没有其他地方。因此,您可以做的是创建另一个称为例如UserRepository的类,您可以在其中与数据库中的用户表进行通信。您可以在构造中执行的操作是传递数组并创建 UserRepository 对象。例如,根据数组键,您可以传递id/用户名和密码。然后基于它们,您要么仅填充对象(按 id 选择用户),要么登录他并使用私有函数填充对象 logon .

您可以做的另一件很酷的事情是抛出异常。无需总是检查函数的返回,它们必须返回正确的内容或根本不返回。因此,在登录函数中,如果您无法对用户进行身份验证,则可以引发异常,以便之后不会出现任何问题。

另一个错误是将每个字段设置为公共字段,例如用户名,电子邮件,userId等。只需覆盖神奇的方法__get并使用它。

你可以从这个开始。我认为这将是一个很好的做法。