Commit c6f711cc by Qiang Xue

Fixes #3765: Added `yii\web\User::enableSession` to support authentication without using session

parent eeb784a1
Authentication Authentication
============== ==============
Unlike Web applications, RESTful APIs should be stateless, which means sessions or cookies should not Unlike Web applications, RESTful APIs are usually stateless, which means sessions or cookies should not
be used. Therefore, each request should come with some sort of authentication credentials because be used. Therefore, each request should come with some sort of authentication credentials because
the user authentication status may not be maintained by sessions or cookies. A common practice is the user authentication status may not be maintained by sessions or cookies. A common practice is
to send a secret access token with each request to authenticate the user. Since an access token to send a secret access token with each request to authenticate the user. Since an access token
...@@ -23,12 +23,27 @@ There are different ways to send an access token: ...@@ -23,12 +23,27 @@ There are different ways to send an access token:
Yii supports all of the above authentication methods. You can also easily create new authentication methods. Yii supports all of the above authentication methods. You can also easily create new authentication methods.
To enable authentication for your APIs, do the following two steps: To enable authentication for your APIs, do the following steps:
1. Specify which authentication methods you plan to use by configuring the `authenticator` behavior 1. Configure the [[yii\web\User::enableSession|enableSession]] property of the `user` application component to be false.
2. Specify which authentication methods you plan to use by configuring the `authenticator` behavior
in your REST controller classes. in your REST controller classes.
2. Implement [[yii\web\IdentityInterface::findIdentityByAccessToken()]] in your [[yii\web\User::identityClass|user identity class]]. 3. Implement [[yii\web\IdentityInterface::findIdentityByAccessToken()]] in your [[yii\web\User::identityClass|user identity class]].
Step 1 is not required but is recommended for RESTful APIs which should be stateless. When [[yii\web\User::enableSession|enableSession]]
is false, the user authentication status will NOT be persisted across requests using sessions. Instead, authentication
will be performed for every request, which is accomplished by Step 2 and 3.
> Tip: You may configure [[yii\web\User::enableSession|enableSession]] of the `user` application component
in application configurations if you are developing RESTful APIs in terms of an application. If you develop
RESTful APIs as a module, you may put the following line in the module's `init()` method, like the following:
> ```php
public function init()
{
parent::init();
\Yii::$app->user->enableSession = false;
}
```
For example, to use HTTP Basic Auth, you may configure `authenticator` as follows, For example, to use HTTP Basic Auth, you may configure `authenticator` as follows,
......
...@@ -75,7 +75,7 @@ Yii Framework 2 Change Log ...@@ -75,7 +75,7 @@ Yii Framework 2 Change Log
- Enh #3132: `yii\rbac\PhpManager` now supports more compact data file format (qiangxue) - Enh #3132: `yii\rbac\PhpManager` now supports more compact data file format (qiangxue)
- Enh #3154: Added validation error display for `GridView` filters (ivan-kolmychek) - Enh #3154: Added validation error display for `GridView` filters (ivan-kolmychek)
- Enh #3196: Masked input upgraded to use jquery.inputmask plugin with more features. (kartik-v) - Enh #3196: Masked input upgraded to use jquery.inputmask plugin with more features. (kartik-v)
- Enh #3220: Added support for setting transation isolation levels (cebe) - Enh #3220: Added support for setting transaction isolation levels (cebe)
- Enh #3222: Added `useTablePrefix` option to the model generator for Gii (horizons2) - Enh #3222: Added `useTablePrefix` option to the model generator for Gii (horizons2)
- Enh #3230: Added `yii\filters\AccessControl::user` to support access control with different actors (qiangxue) - Enh #3230: Added `yii\filters\AccessControl::user` to support access control with different actors (qiangxue)
- Enh #3232: Added `export()` and `exportAsString()` methods to `yii\helpers\BaseVarDumper` (klimov-paul) - Enh #3232: Added `export()` and `exportAsString()` methods to `yii\helpers\BaseVarDumper` (klimov-paul)
...@@ -99,6 +99,7 @@ Yii Framework 2 Change Log ...@@ -99,6 +99,7 @@ Yii Framework 2 Change Log
- Enh #3631: Added property `currencyCode` to `yii\i18n\Formatter` (leandrogehlen) - Enh #3631: Added property `currencyCode` to `yii\i18n\Formatter` (leandrogehlen)
- Enh #3636: Hide menu container tag with empty items in `yii\widgets\Menu` (arturf) - Enh #3636: Hide menu container tag with empty items in `yii\widgets\Menu` (arturf)
- Enh #3643: Improved Mime-Type detection by using the `mime.types` file from apache http project to dected mime types by file extension (cebe, pavel-voronin, trejder) - Enh #3643: Improved Mime-Type detection by using the `mime.types` file from apache http project to dected mime types by file extension (cebe, pavel-voronin, trejder)
- Enh #3765: Added `yii\web\User::enableSession` to support authentication without using session (qiangxue)
- Enh #3773: Added `FileValidator::mimeTypes` to support validating MIME types of files (Ragazzo) - Enh #3773: Added `FileValidator::mimeTypes` to support validating MIME types of files (Ragazzo)
- Enh #3774: Added `FileValidator::checkExtensionByMimeType` to support validating file types against file mime-types (Ragazzo) - Enh #3774: Added `FileValidator::checkExtensionByMimeType` to support validating file types against file mime-types (Ragazzo)
- Enh #3801: Base migration controller `yii\console\controllers\BaseMigrateController` extracted (klimov-paul) - Enh #3801: Base migration controller `yii\console\controllers\BaseMigrateController` extracted (klimov-paul)
......
...@@ -66,3 +66,6 @@ Upgrade from Yii 2.0 Beta ...@@ -66,3 +66,6 @@ Upgrade from Yii 2.0 Beta
differentiate it more from calling `update(false)` and to ensure it can be used in `afterSave()` without triggering infinite differentiate it more from calling `update(false)` and to ensure it can be used in `afterSave()` without triggering infinite
loops. loops.
* If you are developing RESTful APIs and using an authentication method such as `yii\filters\auth\HttpBasicAuth`,
you should explicitly configure `yii\web\User::enableSession` in the application configuration to be false to avoid
starting a session when authentication is performed. Previously this was done automatically by authentication method.
...@@ -70,7 +70,7 @@ class HttpBasicAuth extends AuthMethod ...@@ -70,7 +70,7 @@ class HttpBasicAuth extends AuthMethod
if ($username !== null || $password !== null) { if ($username !== null || $password !== null) {
$identity = call_user_func($this->auth, $username, $password); $identity = call_user_func($this->auth, $username, $password);
if ($identity !== null) { if ($identity !== null) {
$user->setIdentity($identity); $user->switchIdentity($identity);
} else { } else {
$this->handleFailure($response); $this->handleFailure($response);
} }
......
...@@ -68,9 +68,16 @@ class User extends Component ...@@ -68,9 +68,16 @@ class User extends Component
public $identityClass; public $identityClass;
/** /**
* @var boolean whether to enable cookie-based login. Defaults to false. * @var boolean whether to enable cookie-based login. Defaults to false.
* Note that this property will be ignored if [[enableSession]] is false.
*/ */
public $enableAutoLogin = false; public $enableAutoLogin = false;
/** /**
* @var boolean whether to use session to persist authentication status across multiple requests.
* You set this property to be false if your application is stateless, which is often the case
* for RESTful APIs.
*/
public $enableSession = true;
/**
* @var string|array the URL for login when [[loginRequired()]] is called. * @var string|array the URL for login when [[loginRequired()]] is called.
* If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL.
* The first element of the array should be the route to the login action, and the rest of * The first element of the array should be the route to the login action, and the rest of
...@@ -138,18 +145,17 @@ class User extends Component ...@@ -138,18 +145,17 @@ class User extends Component
/** /**
* Returns the identity object associated with the currently logged-in user. * Returns the identity object associated with the currently logged-in user.
* @param boolean $checkSession whether to check the session if the identity has never been determined before. * When [[enableSession]] is true, this method may attempt to read the user's authentication data
* If the identity is already determined (e.g., by calling [[setIdentity()]] or [[login()]]), * stored in session and reconstruct the corresponding identity object, if it has not done so before.
* then this parameter has no effect.
* @return IdentityInterface|null the identity object associated with the currently logged-in user. * @return IdentityInterface|null 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($checkSession = true) public function getIdentity()
{ {
if ($this->_identity === false) { if ($this->_identity === false) {
if ($checkSession) { if ($this->enableSession) {
$this->renewAuthStatus(); $this->renewAuthStatus();
} else { } else {
return null; return null;
...@@ -162,15 +168,11 @@ class User extends Component ...@@ -162,15 +168,11 @@ class User extends Component
/** /**
* Sets the user identity object. * Sets the user identity object.
* *
* This method does nothing else except storing the specified identity object in the internal variable. * Note that this method does not deal with session or cookie. You should usually use [[switchIdentity()]]
* For this reason, this method is best used when the user authentication status should not be maintained * to change the identity of the current user.
* by session.
*
* This method is also called by other more sophisticated methods, such as [[login()]], [[logout()]],
* [[switchIdentity()]]. Those methods will try to use session and cookie to maintain the user authentication
* status.
* *
* @param IdentityInterface|null $identity the identity object associated with the currently logged user. * @param IdentityInterface|null $identity the identity object associated with the currently logged user.
* If null, it means the current user will be a guest without any associated identity.
*/ */
public function setIdentity($identity) public function setIdentity($identity)
{ {
...@@ -181,7 +183,9 @@ class User extends Component ...@@ -181,7 +183,9 @@ class User extends Component
/** /**
* Logs in a user. * Logs in a user.
* *
* By logging in a user, you may obtain the user identity information each time through [[identity]]. * After logging in a user, you may obtain the user's identity information from the [[identity]] property.
* If [[enableSession]] is true, you may even get the identity information in the next requests without
* calling this method again.
* *
* The login status is maintained according to the `$duration` parameter: * The login status is maintained according to the `$duration` parameter:
* *
...@@ -192,10 +196,14 @@ class User extends Component ...@@ -192,10 +196,14 @@ class User extends Component
* the cookie remains valid or the session is active, you may obtain the user identity information * the cookie remains valid or the session is active, you may obtain the user identity information
* via [[identity]]. * via [[identity]].
* *
* Note that if [[enableSession]] is false, the `$duration` parameter will be ignored as it is meaningless
* in this case.
*
* @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.
* Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed. * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed.
* If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported. * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported.
* Note that if [[enableSession]] is false, this parameter will be ignored.
* @return boolean whether the user is logged in * @return boolean whether the user is logged in
*/ */
public function login($identity, $duration = 0) public function login($identity, $duration = 0)
...@@ -204,7 +212,12 @@ class User extends Component ...@@ -204,7 +212,12 @@ class User extends Component
$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 with duration $duration.", __METHOD__); if ($this->enableSession) {
$log = "User '$id' logged in from $ip with duration $duration.";
} else {
$log = "User '$id' logged in from $ip. Session not enabled.";
}
Yii::info($log, __METHOD__);
$this->afterLogin($identity, false, $duration); $this->afterLogin($identity, false, $duration);
} }
...@@ -213,51 +226,55 @@ class User extends Component ...@@ -213,51 +226,55 @@ class User extends Component
/** /**
* Logs in a user by the given access token. * Logs in a user by the given access token.
* Note that unlike [[login()]], this method will NOT start a session to remember the user authentication status. * This method will first authenticate the user by calling [[IdentityInterface::findIdentityByAccessToken()]]
* Also if the access token is invalid, the user will remain as a guest. * with the provided access token. If successful, it will call [[login()]] to log in the authenticated user.
* If authentication fails or [[login()]] is unsuccessful, it will return null.
* @param string $token the access token * @param string $token the access token
* @param mixed $type the type of the token. The value of this parameter depends on the implementation. * @param mixed $type the type of the token. The value of this parameter depends on the implementation.
* For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be `yii\filters\auth\HttpBearerAuth`. * For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be `yii\filters\auth\HttpBearerAuth`.
* @return IdentityInterface the identity associated with the given access token. Null is returned if * @return IdentityInterface|null the identity associated with the given access token. Null is returned if
* the access token is invalid. * the access token is invalid or [[login()]] is unsuccessful.
*/ */
public function loginByAccessToken($token, $type = null) public function loginByAccessToken($token, $type = null)
{ {
/* @var $class IdentityInterface */ /* @var $class IdentityInterface */
$class = $this->identityClass; $class = $this->identityClass;
$identity = $class::findIdentityByAccessToken($token, $type); $identity = $class::findIdentityByAccessToken($token, $type);
$this->setIdentity($identity); if ($identity && $this->login($identity)) {
return $identity;
return $identity; } else {
return null;
}
} }
/** /**
* Logs in a user by cookie. * Logs in a user by cookie.
* *
* This method attempts to log in a user using the ID and authKey information * This method attempts to log in a user using the ID and authKey information
* provided by the given cookie. * provided by the [[identityCookie|identity cookie]].
*/ */
protected function loginByCookie() protected function loginByCookie()
{ {
$name = $this->identityCookie['name']; $value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
$value = Yii::$app->getRequest()->getCookies()->getValue($name); if ($value === null) {
if ($value !== null) { return;
$data = json_decode($value, true); }
if (count($data) === 3 && isset($data[0], $data[1], $data[2])) {
list ($id, $authKey, $duration) = $data; $data = json_decode($value, true);
/* @var $class IdentityInterface */ if (count($data) === 3 && isset($data[0], $data[1], $data[2])) {
$class = $this->identityClass; list ($id, $authKey, $duration) = $data;
$identity = $class::findIdentity($id); /* @var $class IdentityInterface */
if ($identity !== null && $identity->validateAuthKey($authKey)) { $class = $this->identityClass;
if ($this->beforeLogin($identity, true, $duration)) { $identity = $class::findIdentity($id);
$this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); if ($identity !== null && $identity->validateAuthKey($authKey)) {
$ip = Yii::$app->getRequest()->getUserIP(); if ($this->beforeLogin($identity, true, $duration)) {
Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__); $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
$this->afterLogin($identity, true, $duration); $ip = Yii::$app->getRequest()->getUserIP();
} Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__);
} elseif ($identity !== null) { $this->afterLogin($identity, true, $duration);
Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
} }
} elseif ($identity !== null) {
Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
} }
} }
} }
...@@ -267,6 +284,7 @@ class User extends Component ...@@ -267,6 +284,7 @@ class User extends Component
* This will remove authentication-related session data. * This will remove authentication-related session data.
* If `$destroySession` is true, all session data will be removed. * If `$destroySession` is true, all session data will be removed.
* @param boolean $destroySession whether to destroy the whole session. Defaults to true. * @param boolean $destroySession whether to destroy the whole session. Defaults to true.
* This parameter is ignored if [[enableSession]] is false.
* @return boolean whether the user is logged out * @return boolean whether the user is logged out
*/ */
public function logout($destroySession = true) public function logout($destroySession = true)
...@@ -277,7 +295,7 @@ class User extends Component ...@@ -277,7 +295,7 @@ class User extends Component
$id = $identity->getId(); $id = $identity->getId();
$ip = Yii::$app->getRequest()->getUserIP(); $ip = Yii::$app->getRequest()->getUserIP();
Yii::info("User '$id' logged out from $ip.", __METHOD__); Yii::info("User '$id' logged out from $ip.", __METHOD__);
if ($destroySession) { if ($destroySession && $this->enableSession) {
Yii::$app->getSession()->destroy(); Yii::$app->getSession()->destroy();
} }
$this->afterLogout($identity); $this->afterLogout($identity);
...@@ -288,34 +306,32 @@ class User extends Component ...@@ -288,34 +306,32 @@ class User extends Component
/** /**
* Returns a value indicating whether the user is a guest (not authenticated). * Returns a value indicating whether the user is a guest (not authenticated).
* @param boolean $checkSession whether to check the session to determine if the user is a guest.
* Note that if this is false, it is possible that the user may not be a guest while this method still returns
* true. This is because the session is not checked.
* @return boolean whether the current user is a guest. * @return boolean whether the current user is a guest.
* @see getIdentity()
*/ */
public function getIsGuest($checkSession = true) public function getIsGuest()
{ {
return $this->getIdentity($checkSession) === null; return $this->getIdentity() === null;
} }
/** /**
* Returns a value that uniquely represents the user. * Returns a value that uniquely represents the user.
* @param boolean $checkSession whether to check the session to determine the user ID.
* Note that if this is false, it is possible that this method returns null although the user may not
* be a guest. This is because the session is not checked.
* @return string|integer the unique identifier for the user. If null, it means the user is a guest. * @return string|integer the unique identifier for the user. If null, it means the user is a guest.
* @see getIdentity()
*/ */
public function getId($checkSession = true) public function getId()
{ {
$identity = $this->getIdentity($checkSession); $identity = $this->getIdentity();
return $identity !== null ? $identity->getId() : null; return $identity !== null ? $identity->getId() : null;
} }
/** /**
* Returns the URL that the user should be redirected to after successful login. * Returns the URL that the browser should be redirected to after successful login.
* This property is usually used by the login action. If the login is successful, *
* the action should read this property and use it to redirect the user browser. * This method reads the return URL from the session. It is usually used by the login action which
* may call this method to redirect the browser to where it goes after successful authentication.
*
* @param string|array $defaultUrl the default return URL in case it was not set previously. * @param string|array $defaultUrl the default return URL in case it was not set previously.
* If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to. * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to.
* Please refer to [[setReturnUrl()]] on accepted format of the URL. * Please refer to [[setReturnUrl()]] on accepted format of the URL.
...@@ -337,6 +353,7 @@ class User extends Component ...@@ -337,6 +353,7 @@ class User extends Component
} }
/** /**
* Remembers the URL in the session so that it can be retrieved back later by [[getReturnUrl()]].
* @param string|array $url the URL that the user should be redirected to after login. * @param string|array $url the URL that the user should be redirected to after login.
* If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL.
* The first element of the array should be the route, and the rest of * The first element of the array should be the route, and the rest of
...@@ -353,10 +370,11 @@ class User extends Component ...@@ -353,10 +370,11 @@ class User extends Component
/** /**
* Redirects the user browser to the login page. * Redirects the user browser to the login page.
* Before the redirection, the current URL (if it's not an AJAX url) will be *
* kept as [[returnUrl]] so that the user browser may be redirected back * Before the redirection, the current URL (if it's not an AJAX url) will be kept as [[returnUrl]] so that
* to the current page after successful login. Make sure you set [[loginUrl]] * the user browser may be redirected back to the current page after successful login.
* so that the user browser can be redirected to the specified login URL after *
* Make sure you set [[loginUrl]] so that the user browser can be redirected to the specified login URL after
* calling this method. * calling this method.
* *
* Note that when [[loginUrl]] is set, calling this method will NOT terminate the application execution. * Note that when [[loginUrl]] is set, calling this method will NOT terminate the application execution.
...@@ -367,7 +385,7 @@ class User extends Component ...@@ -367,7 +385,7 @@ class User extends Component
public function loginRequired() public function loginRequired()
{ {
$request = Yii::$app->getRequest(); $request = Yii::$app->getRequest();
if (!$request->getIsAjax()) { if ($this->enableSession && !$request->getIsAjax()) {
$this->setReturnUrl($request->getUrl()); $this->setReturnUrl($request->getUrl());
} }
if ($this->loginUrl !== null) { if ($this->loginUrl !== null) {
...@@ -495,26 +513,32 @@ class User extends Component ...@@ -495,26 +513,32 @@ class User extends Component
/** /**
* Switches to a new identity for the current user. * Switches to a new identity for the current user.
* *
* This method may use session and/or cookie to store the user identity information, * When [[enableSession]] is true, this method may use session and/or cookie to store the user identity information,
* according to the value of `$duration`. Please refer to [[login()]] for more details. * according to the value of `$duration`. Please refer to [[login()]] for more details.
* *
* 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|null $identity the identity information to be associated with the current user.
* If null, it means switching the current user 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.
*/ */
public function switchIdentity($identity, $duration = 0) public function switchIdentity($identity, $duration = 0)
{ {
$this->setIdentity($identity);
if (!$this->enableSession) {
return;
}
$session = Yii::$app->getSession(); $session = Yii::$app->getSession();
if (!YII_ENV_TEST) { if (!YII_ENV_TEST) {
$session->regenerateID(true); $session->regenerateID(true);
} }
$this->setIdentity($identity);
$session->remove($this->idParam); $session->remove($this->idParam);
$session->remove($this->authTimeoutParam); $session->remove($this->authTimeoutParam);
if ($identity instanceof IdentityInterface) { if ($identity instanceof IdentityInterface) {
$session->set($this->idParam, $identity->getId()); $session->set($this->idParam, $identity->getId());
if ($this->authTimeout !== null) { if ($this->authTimeout !== null) {
......
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