我从头开始构建一个User类,用于学习体验和工作项目。我需要了解有关登录安全性的更多信息,我创建的这个类是基于Web上的几个示例构建的,然后更新为对讨论和其他文档的批评。
我的msqli_object不是此课程的一部分,但我将其包含在测试中。它实际上是我的MyApp_DB
类的一部分,但是因为这个类的[缺乏]安全性可能是我代码中最大的漏洞,所以我想在这里。换句话说,我知道它需要与我的User类分开。我没有包含SessionManager类,因为我还没有完成构建它。
班级......"工作",但我的代码中有几个问题,我觉得我正在建造一个破碎的房子:
mysqlnd
,因此我无法访问mysqli_stmt::get_result()
。为了将mysqli对象保持为类的成员而不是全局(因此难以定位)变量,我必须使用eval()作为解决方法,直到我能够弄清楚如何将数组的成员发送为一组参数。任何帮助都会很棒。$hash
变量用完之后立即销毁它,而我的eval()
行不使用任何用户输入。我不认为我完全理解password_verify()
功能,知道我正确使用它逻辑 - 我的逻辑和方法是否接近合理?
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类时犯了哪些错误?我试着尽可能地根据我的知识,借用逻辑来编写尽可能多的内容,而不是来自网络上不同位置的代码。从某种意义上说,它没有任何错误,但我想学会更清晰地写作并学习标准。
答案 0 :(得分:2)
我的逻辑和方法是否接近合理?
不幸的是,没有。
public function __construct()
想象一下,您将在页面上列出100个用户。 您的脚本将创建多少个mysql连接?
public function __construct($mysqli) {
$this->mysqli = $mysqli;
必须是。
private function _db_select($table, $cols, $conds, $params)
这个有两个问题。
SELECT
,FROM
和WHERE
。为此,你在美丽可读的SQL中制造丑陋的胡言乱语。更不用说这个函数支持的SQL子集非常小。采取一些建议;创建一个运行任意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并使用它。
你可以从这开始。我认为这将是一个很好的做法。