From 7dc131bf205826e911e6e5ad066f326344dd525f Mon Sep 17 00:00:00 2001 From: Rhilip Date: Wed, 30 Jan 2019 22:36:20 +0800 Subject: [PATCH] :lock: (Auth) Use UserLoginForm to manager user login action --- apps/controllers/AuthController.php | 57 ++++++------------------- apps/models/form/UserLoginForm.php | 66 ++++++++++++++++++++++++----- framework/User/User.php | 40 ++++------------- 3 files changed, 77 insertions(+), 86 deletions(-) diff --git a/apps/controllers/AuthController.php b/apps/controllers/AuthController.php index 9763937..cca75e0 100644 --- a/apps/controllers/AuthController.php +++ b/apps/controllers/AuthController.php @@ -9,10 +9,10 @@ namespace apps\controllers; use apps\models\User; +use apps\models\form\UserLoginForm; use apps\models\form\UserRegisterForm; use Mix\Http\Controller; -use RobThree\Auth\TwoFactorAuth; class AuthController extends Controller @@ -59,46 +59,22 @@ public function actionRecover() public function actionLogin() { if (app()->request->isPost()) { - $username = app()->request->post("username"); - $self = app()->pdo->createCommand("SELECT `id`,`username`,`password`,`status`,`opt`,`class` from users WHERE `username` = :uname OR `email` = :email LIMIT 1")->bindParams([ - "uname" => $username, "email" => $username, - ])->queryOne(); + $login = new UserLoginForm(); + $login->importAttributes(app()->request->post()); + $error = $login->validate(); - try { - // User is not exist - if (!$self) throw new \Exception("Invalid username/password"); - - // User's password is not correct - if (!password_verify(app()->request->post("password"), $self["password"])) - throw new \Exception("Invalid username/password"); - - // User enable 2FA but it's code is wrong - if ($self["opt"]) { - $tfa = new TwoFactorAuth(app()->config->get("base.site_name")); - if ($tfa->verifyCode($self["opt"], app()->request->post("opt")) == false) - throw new \Exception("2FA Validation failed"); - } - - // User 's status is banned or pending~ - if (in_array($self["status"], ["banned", "pending"])) { - throw new \Exception("User account is not confirmed."); + if (count($error) > 0) { + return $this->render("auth/login.html.twig", ["username" => $login->username, "error_msg" => $error->get(0)]); + } else { + $success = $login->createUserSession(); + if ($success) { + $login->updateUserLoginInfo(); + $return_to = app()->session->pop('login_return_to') ?? '/index'; + return app()->response->redirect($return_to); + } else { + return $this->render('errors/action_fail.html.twig',['title'=> 'Login Failed','msg' => 'Reach the limit of Max User Session.']); } - } catch (\Exception $e) { - return $this->render("auth/login.html.twig", ["username" => $username, "error_msg" => $e->getMessage()]); - } - - $success = app()->user->createUserSessionId($self["id"]); - - if (!$success) { - return $this->render('errors/action_fail.html.twig',['title'=> 'Login Failed','msg' => 'Reach the limit of Max User Session.']); } - - app()->pdo->createCommand("UPDATE `users` SET `last_login_at` = NOW() , `last_login_ip` = INET6_ATON(:ip) WHERE `id` = :id")->bindParams([ - "ip" => app()->request->getClientIp(), "id" => $self["id"] - ])->execute(); - - $return_to = app()->session->pop('login_return_to') ?? '/index'; - return app()->response->redirect($return_to); } else { return $this->render("auth/login.html.twig"); } @@ -110,9 +86,4 @@ public function actionLogout() app()->user->deleteUserThisSession(); return app()->response->redirect('/auth/login'); } - - private function isMaxLoginIpReached() - { - - } } diff --git a/apps/models/form/UserLoginForm.php b/apps/models/form/UserLoginForm.php index 5d800fb..6fd0f35 100644 --- a/apps/models/form/UserLoginForm.php +++ b/apps/models/form/UserLoginForm.php @@ -8,11 +8,13 @@ namespace apps\models\form; +use Mix\Helpers\StringHelper; use Mix\User\UserInterface; use Mix\Validators\Validator; use RobThree\Auth\TwoFactorAuth; use RobThree\Auth\TwoFactorAuthException; + use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -25,6 +27,26 @@ class UserLoginForm extends Validator private $self; + // Key Information of User Session + private $sessionLength = 26; + private $sessionSaveKey = 'SESSION:user_set'; + + // Cookie + private $cookieName = 'rid'; + private $cookieExpires = 0x7fffffff; + private $cookiePath = '/'; + private $cookieDomain = ''; + private $cookieSecure = false; + private $cookieHttpOnly = true; + + public function __construct(array $config = []) + { + parent::__construct($config); + $this->sessionSaveKey = app()->user->sessionSaveKey; + $this->cookieName = app()->user->cookieName; + } + + public static function rules() { return [ @@ -49,50 +71,72 @@ public static function loadValidatorMetadata(ClassMetadata $metadata) foreach ($rules as $property => $constraints) { $metadata->addPropertyConstraints($property, $constraints); } - $metadata->addConstraint(new Assert\Callback('loadUserFromPDO')); + $metadata->addConstraint(new Assert\Callback('loadUserFromPdo')); + $metadata->addConstraint(new Assert\Callback('isMaxLoginIpReached')); } - public function loadUserFromPDO(ExecutionContextInterface $context) + public function loadUserFromPdo(ExecutionContextInterface $context) { $this->self = app()->pdo->createCommand("SELECT `id`,`username`,`password`,`status`,`opt`,`class` from users WHERE `username` = :uname OR `email` = :email LIMIT 1")->bindParams([ "uname" => $this->username, "email" => $this->username, ])->queryOne(); - if (!$this->self) // User is not exist + if (!$this->self) { // User is not exist $context->buildViolation("This User is not exist in this site.")->addViolation(); + return; + } // User's password is not correct - if (!password_verify($this->password, $this->self["password"])) + if (!password_verify($this->password, $this->self["password"])) { $context->buildViolation("Invalid username/password")->addViolation(); + return; + } // User enable 2FA but it's code is wrong if ($this->self["opt"]) { try { $tfa = new TwoFactorAuth(app()->config->get("base.site_name")); - if ($tfa->verifyCode($this->self["opt"], $this->opt) == false) + if ($tfa->verifyCode($this->self["opt"], $this->opt) == false) { $context->buildViolation("2FA Validation failed")->addViolation(); + return; + } } catch (TwoFactorAuthException $e) { $context->buildViolation("2FA Validation failed")->addViolation(); + return; } } // User 's status is banned or pending~ if (in_array($this->self["status"], [UserInterface::STATUS_BANNED, UserInterface::STATUS_PENDING])) { $context->buildViolation("User account is not confirmed.")->addViolation(); + return; } } - public function createUserSession() + public function isMaxLoginIpReached() { - app()->user->createUserSessionId(); - app()->user->set('user', $this->self); + // TODO Check User Fail Login Ip Count + } - // TODO record user login session - $this->updateUserLogin(); + public function createUserSession() + { + $userId = $this->self['id']; + $userSessionId = StringHelper::getRandomString($this->sessionLength); + + $exist_session_count = app()->redis->zCount($this->sessionSaveKey, $userId, $userId); + if ($exist_session_count < app()->config->get('base.max_per_user_session')) { + app()->redis->zAdd($this->sessionSaveKey, $userId, $userSessionId); + // TODO store user login information , ( for example `login ip`,`platform`,`browser`,`last activity at` ) + app()->response->setCookie($this->cookieName, $userSessionId, $this->cookieExpires, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, $this->cookieHttpOnly); + return true; + } else { + return false; + } } - private function updateUserLogin() { + public function updateUserLoginInfo() + { app()->pdo->createCommand("UPDATE `users` SET `last_login_at` = NOW() , `last_login_ip` = INET6_ATON(:ip) WHERE `id` = :id")->bindParams([ "ip" => app()->request->getClientIp(), "id" => $this->self["id"] ])->execute(); diff --git a/framework/User/User.php b/framework/User/User.php index c2299de..cf39b15 100644 --- a/framework/User/User.php +++ b/framework/User/User.php @@ -9,33 +9,25 @@ namespace Mix\User; use Mix\Base\Component; -use Mix\Helpers\StringHelper; + class User extends Component implements UserInterface { use UserTrait; - public $sessionSaveKey = 'USER_SESSION'; - // Cookie + public $sessionSaveKey = 'SESSION:user_set'; public $cookieName = 'rid'; - public $cookieExpires = 0x7fffffff; - public $cookiePath = '/'; - public $cookieDomain = ''; - public $cookieSecure = false; - public $cookieHttpOnly = false; // Key of User Session - protected $_userKey; protected $_userSessionId; - protected $_userIdLength = 26; private $anonymous = false; public function onRequestBefore() { parent::onRequestBefore(); - $this->loadUserFromCookies(); + $this->loadUser(); } public function onRequestAfter() @@ -49,6 +41,11 @@ public function isAnonymous() return $this->anonymous; } + public function loadUser() { + // TODO Load User From Passkey in some route , for example '/rss' + return $this->loadUserFromCookies(); + } + public function loadUserFromCookies() { $this->_userSessionId = \Mix::app()->request->cookie($this->cookieName); @@ -62,20 +59,6 @@ public function loadUserFromCookies() } } - public function createUserSessionId($userId) - { - $this->_userSessionId = StringHelper::getRandomString($this->_userIdLength); - - $exist_session_count = app()->redis->zCount($this->sessionSaveKey, $userId, $userId); - if ($exist_session_count < app()->config->get('base.max_per_user_session')) { - app()->redis->zAdd($this->sessionSaveKey, $userId, $this->_userSessionId); - app()->cookie->set($this->cookieName, $this->_userSessionId); - return true; - } else { - return false; - } - } - public function deleteUserThisSession() { $success = app()->redis->zRem($this->sessionSaveKey, $this->_userSessionId); @@ -83,13 +66,6 @@ public function deleteUserThisSession() return $success ? true : false; } - // 删除 - public function delete($name) - { - $success = app()->redis->zRem($this->sessionSaveKey, $name); - return $success ? true : false; - } - // 获取SessionId public function getSessionId() {