Commit 4891aa79 by Qiang Xue

Fixes #1535: Improved `yii\web\User` to start session only when needed. Also…

Fixes #1535: Improved `yii\web\User` to start session only when needed. Also prepared it for use without session.
parent ec05c0ea
...@@ -72,6 +72,7 @@ Yii Framework 2 Change Log ...@@ -72,6 +72,7 @@ Yii Framework 2 Change Log
- Enh #1476: Add yii\web\Session::handler property (nineinchnick) - Enh #1476: Add yii\web\Session::handler property (nineinchnick)
- Enh #1499: Added `ActionColumn::controller` property to support customizing the controller for handling GridView actions (qiangxue) - Enh #1499: Added `ActionColumn::controller` property to support customizing the controller for handling GridView actions (qiangxue)
- Enh #1523: Query conditions now allow to use the NOT operator (cebe) - Enh #1523: Query conditions now allow to use the NOT operator (cebe)
- Enh #1535: Improved `yii\web\User` to start session only when needed. Also prepared it for use without session. (qiangxue)
- Enh #1562: Added `yii\bootstrap\Tabs::linkOptions` (kartik-v) - Enh #1562: Added `yii\bootstrap\Tabs::linkOptions` (kartik-v)
- Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) - Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark)
- Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue)
......
...@@ -14,11 +14,20 @@ use yii\base\InvalidConfigException; ...@@ -14,11 +14,20 @@ use yii\base\InvalidConfigException;
/** /**
* User is the class for the "user" application component that manages the user authentication status. * User is the class for the "user" application component that manages the user authentication status.
* *
* In particular, [[User::isGuest]] returns a value indicating whether the current user is a guest or not. * You may use [[isGuest]] to determine whether the current user is a guest or not.
* Through methods [[login()]] and [[logout()]], you can change the user authentication status. * If the user is a guest, the [[identity]] property would return null. Otherwise, it would
* be an instance of [[IdentityInterface]].
* *
* User works with a class implementing the [[IdentityInterface]]. This class implements * You may call various methods to change the user authentication status:
* the actual user authentication logic and is often backed by a user database table. *
* - [[login()]]: sets the specified identity and remembers the authentication status in session and cookie.
* - [[logout()]]: marks the user as a guest and clears the relevant information from session and cookie.
* - [[setIdentity()]]: changes the user identity without touching session or cookie.
* This is best used in stateless RESTful API implementation.
*
* Note that User only maintains the user authentication status. It does NOT handle how to authenticate
* a user. The logic of how to authenticate a user should be done in the class implementing [[IdentityInterface]].
* You are also required to set [[identityClass]] with the name of this class.
* *
* User is configured as an application component in [[\yii\web\Application]] by default. * User is configured as an application component in [[\yii\web\Application]] by default.
* You can access that instance via `Yii::$app->user`. * You can access that instance via `Yii::$app->user`.
...@@ -124,51 +133,42 @@ class User extends Component ...@@ -124,51 +133,42 @@ class User extends Component
if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) {
throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); throw new InvalidConfigException('User::identityCookie must contain the "name" element.');
} }
Yii::$app->getSession()->open();
$this->renewAuthStatus();
if ($this->enableAutoLogin) {
if ($this->getIsGuest()) {
$this->loginByCookie();
} elseif ($this->autoRenewCookie) {
$this->renewIdentityCookie();
}
}
} }
private $_identity = false; private $_identity = false;
/** /**
* Returns the identity object associated with the currently logged user. * Returns the identity object associated with the currently logged-in user.
* @return IdentityInterface the identity object associated with the currently logged user. * @param boolean $checkSession whether to check the session if the identity has never been determined before.
* If the identity is already determined (e.g., by calling [[setIdentity()]] or [[login()]]),
* then this parameter has no effect.
* @return IdentityInterface the identity object associated with the currently logged-in user.
* Null is returned if the user is not logged in (not authenticated). * Null is returned if the user is not logged in (not authenticated).
* @see login() * @see login()
* @see logout() * @see logout()
*/ */
public function getIdentity() public function getIdentity($checkSession = true)
{ {
if ($this->_identity === false) { if ($this->_identity === false) {
$id = $this->getId(); if ($checkSession) {
if ($id === null) { $this->renewAuthStatus();
$this->_identity = null;
} else { } else {
/** @var IdentityInterface $class */ return null;
$class = $this->identityClass;
$this->_identity = $class::findIdentity($id);
} }
} }
return $this->_identity; return $this->_identity;
} }
/** /**
* Sets the identity object. * Sets the user identity object.
* This method should be mainly be used by the User component or its child class *
* to maintain the identity object. * This method does nothing else except storing the specified identity object in the internal variable.
* For this reason, this method is best used when the user authentication status should not be maintained
* by session.
* *
* You should normally update the user identity via methods [[login()]], [[logout()]] * This method is also called by other more sophisticated methods, such as [[login()]], [[logout()]],
* or [[switchIdentity()]]. * [[switchIdentity()]]. Those methods will try to use session and cookie to maintain the user authentication
* status.
* *
* @param IdentityInterface $identity the identity object associated with the currently logged user. * @param IdentityInterface $identity the identity object associated with the currently logged user.
*/ */
...@@ -180,10 +180,16 @@ class User extends Component ...@@ -180,10 +180,16 @@ class User extends Component
/** /**
* Logs in a user. * Logs in a user.
* *
* This method stores the necessary session information to keep track * By logging in a user, you may obtain the user identity information each time through [[identity]].
* of the user identity information. If `$duration` is greater than 0 *
* and [[enableAutoLogin]] is true, it will also send out an identity * The login status is maintained according to the `$duration` parameter:
* cookie to support cookie-based login. *
* - `$duration == 0`: the identity information will be stored in session and will be available
* via [[identity]] as long as the session remains active.
* - `$duration > 0`: the identity information will be stored in session. If [[enableAutoLogin]] is true,
* it will also be stored in a cookie which will expire in `$duration` seconds. As long as
* the cookie remains valid or the session is active, you may obtain the user identity information
* via [[identity]].
* *
* @param IdentityInterface $identity the user identity (which should already be authenticated) * @param IdentityInterface $identity the user identity (which should already be authenticated)
* @param integer $duration number of seconds that the user can remain in logged-in status. * @param integer $duration number of seconds that the user can remain in logged-in status.
...@@ -193,12 +199,12 @@ class User extends Component ...@@ -193,12 +199,12 @@ class User extends Component
*/ */
public function login($identity, $duration = 0) public function login($identity, $duration = 0)
{ {
if ($this->beforeLogin($identity, false)) { if ($this->beforeLogin($identity, false, $duration)) {
$this->switchIdentity($identity, $duration); $this->switchIdentity($identity, $duration);
$id = $identity->getId(); $id = $identity->getId();
$ip = Yii::$app->getRequest()->getUserIP(); $ip = Yii::$app->getRequest()->getUserIP();
Yii::info("User '$id' logged in from $ip.", __METHOD__); Yii::info("User '$id' logged in from $ip with duration $duration.", __METHOD__);
$this->afterLogin($identity, false); $this->afterLogin($identity, false, $duration);
} }
return !$this->getIsGuest(); return !$this->getIsGuest();
} }
...@@ -221,11 +227,11 @@ class User extends Component ...@@ -221,11 +227,11 @@ class User extends Component
$class = $this->identityClass; $class = $this->identityClass;
$identity = $class::findIdentity($id); $identity = $class::findIdentity($id);
if ($identity !== null && $identity->validateAuthKey($authKey)) { if ($identity !== null && $identity->validateAuthKey($authKey)) {
if ($this->beforeLogin($identity, true)) { if ($this->beforeLogin($identity, true, $duration)) {
$this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
$ip = Yii::$app->getRequest()->getUserIP(); $ip = Yii::$app->getRequest()->getUserIP();
Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__); Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__);
$this->afterLogin($identity, true); $this->afterLogin($identity, true, $duration);
} }
} elseif ($identity !== null) { } elseif ($identity !== null) {
Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
...@@ -270,7 +276,8 @@ class User extends Component ...@@ -270,7 +276,8 @@ class User extends Component
*/ */
public function getId() public function getId()
{ {
return Yii::$app->getSession()->get($this->idParam); $identity = $this->getIdentity();
return $identity !== null ? $identity->getId() : null;
} }
/** /**
...@@ -345,13 +352,16 @@ class User extends Component ...@@ -345,13 +352,16 @@ class User extends Component
* so that the event is triggered. * so that the event is triggered.
* @param IdentityInterface $identity the user identity information * @param IdentityInterface $identity the user identity information
* @param boolean $cookieBased whether the login is cookie-based * @param boolean $cookieBased whether the login is cookie-based
* @param integer $duration number of seconds that the user can remain in logged-in status.
* If 0, it means login till the user closes the browser or the session is manually destroyed.
* @return boolean whether the user should continue to be logged in * @return boolean whether the user should continue to be logged in
*/ */
protected function beforeLogin($identity, $cookieBased) protected function beforeLogin($identity, $cookieBased, $duration)
{ {
$event = new UserEvent([ $event = new UserEvent([
'identity' => $identity, 'identity' => $identity,
'cookieBased' => $cookieBased, 'cookieBased' => $cookieBased,
'duration' => $duration,
]); ]);
$this->trigger(self::EVENT_BEFORE_LOGIN, $event); $this->trigger(self::EVENT_BEFORE_LOGIN, $event);
return $event->isValid; return $event->isValid;
...@@ -364,12 +374,15 @@ class User extends Component ...@@ -364,12 +374,15 @@ class User extends Component
* so that the event is triggered. * so that the event is triggered.
* @param IdentityInterface $identity the user identity information * @param IdentityInterface $identity the user identity information
* @param boolean $cookieBased whether the login is cookie-based * @param boolean $cookieBased whether the login is cookie-based
* @param integer $duration number of seconds that the user can remain in logged-in status.
* If 0, it means login till the user closes the browser or the session is manually destroyed.
*/ */
protected function afterLogin($identity, $cookieBased) protected function afterLogin($identity, $cookieBased, $duration)
{ {
$this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent([ $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent([
'identity' => $identity, 'identity' => $identity,
'cookieBased' => $cookieBased, 'cookieBased' => $cookieBased,
'duration' => $duration,
])); ]));
} }
...@@ -448,15 +461,14 @@ class User extends Component ...@@ -448,15 +461,14 @@ class User extends Component
/** /**
* Switches to a new identity for the current user. * Switches to a new identity for the current user.
* *
* This method will save necessary session information to keep track of the user authentication status. * This method may use session and/or cookie to store the user identity information,
* If `$duration` is provided, it will also send out appropriate identity cookie * according to the value of `$duration`. Please refer to [[login()]] for more details.
* to support cookie-based login.
* *
* This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]]
* when the current user needs to be associated with the corresponding identity information. * when the current user needs to be associated with the corresponding identity information.
* *
* @param IdentityInterface $identity the identity information to be associated with the current user. * @param IdentityInterface $identity the identity information to be associated with the current user.
* If null, it means switching to be a guest. * If null, it means switching the current user to be a guest.
* @param integer $duration number of seconds that the user can remain in logged-in status. * @param integer $duration number of seconds that the user can remain in logged-in status.
* This parameter is used only when `$identity` is not null. * This parameter is used only when `$identity` is not null.
*/ */
...@@ -483,19 +495,44 @@ class User extends Component ...@@ -483,19 +495,44 @@ class User extends Component
} }
/** /**
* Updates the authentication status according to [[authTimeout]]. * Updates the authentication status using the information from session and cookie.
* This method is called during [[init()]]. *
* It will update the user's authentication status if it has not outdated yet. * This method will try to determine the user identity using the [[idParam]] session variable.
* Otherwise, it will logout the user. *
* If [[authTimeout]] is set, this method will refresh the timer.
*
* If the user identity cannot be determined by session, this method will try to [[loginByCookie()|login by cookie]]
* if [[enableAutoLogin]] is true.
*/ */
protected function renewAuthStatus() protected function renewAuthStatus()
{ {
if ($this->authTimeout !== null && !$this->getIsGuest()) { $session = Yii::$app->getSession();
$expire = Yii::$app->getSession()->get($this->authTimeoutParam); $id = $session->getHasSessionId() ? $session->get($this->idParam) : null;
if ($id === null) {
$identity = null;
} else {
/** @var IdentityInterface $class */
$class = $this->identityClass;
$identity = $class::findIdentity($id);
}
$this->setIdentity($identity);
if ($this->authTimeout !== null && $identity !== null) {
$expire = $session->get($this->authTimeoutParam);
if ($expire !== null && $expire < time()) { if ($expire !== null && $expire < time()) {
$this->logout(false); $this->logout(false);
} else { } else {
Yii::$app->getSession()->set($this->authTimeoutParam, time() + $this->authTimeout); $session->set($this->authTimeoutParam, time() + $this->authTimeout);
}
}
if ($this->enableAutoLogin) {
if ($this->getIsGuest()) {
$this->loginByCookie();
} elseif ($this->autoRenewCookie) {
$this->renewIdentityCookie();
} }
} }
} }
......
...@@ -27,6 +27,11 @@ class UserEvent extends Event ...@@ -27,6 +27,11 @@ class UserEvent extends Event
*/ */
public $cookieBased; public $cookieBased;
/** /**
* @var integer $duration number of seconds that the user can remain in logged-in status.
* If 0, it means login till the user closes the browser or the session is manually destroyed.
*/
public $duration;
/**
* @var boolean whether the login or logout should proceed. * @var boolean whether the login or logout should proceed.
* Event handlers may modify this property to determine whether the login or logout should proceed. * Event handlers may modify this property to determine whether the login or logout should proceed.
* This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment