我正在从头开始构建一个用户类,用于学习体验和工作项目。我需要更多地了解登录安全性,我创建的这个类是根据网络上的几个示例构建的,然后根据对讨论和其他文档的批评进行了更新。
我的msqli_object不属于这个类,但我把它包括在内进行测试。它实际上是我MyApp_DB
类的一部分,但是因为这个类的[缺乏]安全性可能是我代码中最大的漏洞,所以我在这里想要它。换句话说,我知道它需要与我的 User 类分开。我没有包括 SessionManager 类,因为我还没有完成构建它。
班级..."有效",但我的代码中有几个问题,我觉得我正在用破碎的块建造一座房子:
- 我没有
mysqlnd
,所以我无法访问mysqli_stmt::get_result()
。为了将 mysqli 对象保留为类的成员而不是全局(因此难以定位)变量,我不得不使用 eval() 作为解决方法,直到我弄清楚如何将数组的成员作为一组参数发送。任何帮助都会很棒。 - 我不知道这段代码总体上有多安全。一旦
$hash
变量用完,我就会销毁它,并且我的eval()
行不使用任何用户输入。我认为我对password_verify()
功能的理解不够好,无法知道我是否正确使用它 - 代码文档...我一直记录我的代码,但只是为了我自己。我现在正在从事的项目将有几个人和我一起工作,我需要能够干净地记录。我正在安装 PHPdoc,但我还没有真正弄清楚整个标记系统。
- 代码结构。欢迎对我的结构提出任何批评,以使其易于阅读 逻辑
——我的逻辑和方法论是否接近合理?
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)
这个有两个问题。
- 您的用户不是 SQL 驱动的关系数据库。不能对用户运行 SQL 查询。用户只能使用数据库服务。因此,在 User 类中根本不应该有这样的函数。
- 老实说,整个功能是一团糟。您正在尝试节省自己输入的几个单词,即
SELECT
、FROM
和WHERE
.为此,您正在用美观且可读的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并使用它。
你可以从这个开始。我认为这将是一个很好的做法。