在PHP中构建User类,使其更安全

时间:2016-03-02 05:47:13

标签: php class authentication mysqli standards

我从头开始构建一个User类,用于学习体验和工作项目。我需要了解有关登录安全性的更多信息,我创建的这个类是基于Web上的几个示例构建的,然后更新为对讨论和其他文档的批评。

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

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

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

    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]);
    
        }
    
    }
    
  6. 总结一下,我在构建此User类时犯了哪些错误?我试着尽可能地根据我的知识,借用逻辑来编写尽可能多的内容,而不是来自网络上不同位置的代码。从某种意义上说,它没有任何错误,但我想学会更清晰地写作并学习标准。

2 个答案:

答案 0 :(得分:2)

  

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

不幸的是,没有。

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. 老实说,整个功能是A MESS。您正在尝试自己输入几个单词,即SELECTFROMWHERE。为此,你在美丽可读的SQL中制造丑陋的胡言乱语。更不用说这个函数支持的SQL子集非常小。
  3. 采取一些建议;创建一个运行任意SQL而不是这个Frankenstein的函数。或者寻找现成的 queryBuilder

    使用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();
    }
    

答案 1 :(得分:1)

使用PDO创建一个抽象的db类。这可能是你用mysql_ *做的最大的错误。阅读有关pdo的更多信息并实施它。

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

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

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

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