Yii2 详说auth_key的用途


Yii2 详说auth_key的用途


正文

当我们用yii2开始项目时,在migrate初始生成user表时,会看到里面有一个auth_key字段,这个字段是做什么用的呢? 查阅好多资料后,还是云里雾里,莫名其妙,不知所从。其实这个字段是用来用cookie自动登录验证的,这样即使session丢失了, 只要cookie信息还存在,那么就可以用cookie中的相关信息完成自动登录并写入session。

这里有详细的解说过程,可以看看:http://www.yii-china.com/post/detail/323.html

引入对比

我们看看cookie中发生了什么,后台login登录时,用F12可以查看:

_csrf-backend详细:

v5G4eIFl_Lel0yPkbWPqDw1l7jJWYknp9AXY6sF_xKeLwOIgyVXL1dyBELteFKtVTwqoAw8hFr-VSo2c9x2vyg==

解码看一下:

$input = "v5G4eIFl_Lel0yPkbWPqDw1l7jJWYknp9AXY6sF_xKeLwOIgyVXL1dyBELteFKtVTwqoAw8hFr-VSo2c9x2vyg==";

function byteSubstr($string, $start, $length = null) {
    return mb_substr($string, $start, $length === null ? mb_strlen($string, '8bit') : $length, '8bit');
}

$decoded = base64_decode(strtr($input, '-_', '+/'));
$length = mb_strlen($decoded, '8bit') / 2;

echo byteSubstr($decoded, $length, $length) ^ byteSubstr($decoded, 0, $length);  
// 输出 4QZXH07byR3_3wAZBoF1YC_VaOUv6bkm

关于csrf可以看这里:https://ibaiyang.github.io/blog/yii2/2017/12/03/Yii2-csrf综合.html

请求Cookie中:

_csrf-backend详细:

e464895e073b220c0265aa9148836f833eedefd2cafb61cbfa24fbf940d71967a:2:{i:0;s:13:"_csrf-backend";i:1;s:32:"4QZXH07byR3_3wAZBoF1YC_VaOUv6bkm";}

响应Cookie中:

_identity-backend的value详细:

bb2e3c9536c3fe6a1399519c4d43c3e7a0565fdeed79f33d95efbad9e1486781a:2:{i:0;s:17:"_identity-backend";i:1;s:46:"[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]";}

这里既有advanced-backend、又有_csrf-backend、还有_identity-backend,是由于在配置文件backend/config/main.php中有:

'components' => [
    'request' => [
        'csrfParam' => '_csrf-backend',
    ],
    'user' => [
        'identityClass' => 'common\models\Adminuser',
        'enableAutoLogin' => true,
        'identityCookie' => ['name' => '_identity-backend', 'httpOnly' => true],
    ],
    'session' => [
        // this is the name of the session cookie used for login on the backend
        'name' => 'advanced-backend',
//        'savePath' => sys_get_temp_dir(),
    ],
],   

advanced-backend是session中配置的,可以换个名字,如:session-backend。 用户用浏览器第一次访问网站,网站服务器会给用户浏览器标记一个session,就是这时候给的advanced-backend值, 20分钟内不会再发这个性质的session了。

_csrf-backend是request中配置的,用户用浏览器第一次访问网站域名时,并不会传递过来, 第一次访问网站页面(Yii2框架配置的)时,网站服务器会把这个cookie发送到客户端; 客户端第二次访问网站页面时,才会把这个cookie传递过来。以后访问该网站的页面,都会带上这个cookie参数, 网站也不再传递这个值给客户端。

_identity-backend是user组件中配置的,正如其名,是用户post登陆成功后服务器传递给客户端的。在在线情况下, 客户端每次都会传递这个值给服务器以更新有效期。退出时又传递这个值过来,不过这个值为空。

传递过来的cookie值:value是值的内容;path是服务器要把这个cookie保存在浏览器上的地址, httpOnly:true说明只有http请求才会传递这个值到服务器,expires是这个cookie存到什么时间截至。

cookie就是服务器辨识浏览器的标志,浏览器每一次访问服务器,都会带上这个服务器存在这个浏览器上的所有cookie信息。

登录后,访问页面admin-user/index时:

响应Cookie中:

_identity-backend的value详细:

bb2e3c9536c3fe6a1399519c4d43c3e7a0565fdeed79f33d95efbad9e1486781a:2:{i:0;s:17:"_identity-backend";i:1;s:46:"[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]";}

请求Cookie中:

_identity-backend详细:

bb2e3c9536c3fe6a1399519c4d43c3e7a0565fdeed79f33d95efbad9e1486781a:2:{i:0;s:17:"_identity-backend";i:1;s:46:"[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]";}

_csrf-backend详细:

e464895e073b220c0265aa9148836f833eedefd2cafb61cbfa24fbf940d71967a:2:{i:0;s:13:"_csrf-backend";i:1;s:32:"4QZXH07byR3_3wAZBoF1YC_VaOUv6bkm";}

我们打印一下请求时的$_COOKIE看一下:

array(2) {
  ["advanced-backend"]=>
  string(26) "pa82frto4p6npookbm6d2bjdp6"
  ["_identity-backend"]=>
  string(157) "f9d6c533d3e73cbadc9efc60bf74a0a9f477504acf6bfb93a3d2078d07eaa8a2a:2:{i:0;s:17:"_identity-backend";i:1;s:46:"[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]";}"
}

_identity-backend经过security组件解码后:

// $value 是$_COOKIE["_identity-backend"] , $this->cookieValidationKey  值为  0w8Wb50OEKfNeJbmuLuG3XQb4hKn2ykw
// $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
a:2:{i:0;s:17:"_identity-backend";i:1;s:46:"[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]";}
// $data = @unserialize($data);
array(2) {
  [0]=>
  string(17) "_identity-backend"
  [1]=>
  string(46) "[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]"
}
// $cookies[$name] = Yii::createObject([ 'class' => 'yii\web\Cookie',  'name' => $name, 'value' => $data[1], 'expire' => null, ]);
array(1) {
  ["_identity-backend"]=>
  object(yii\web\Cookie)#75 (7) {
    ["name"]=>
    string(17) "_identity-backend"
    ["value"]=>
    string(46) "[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]"
    ["domain"]=>
    string(0) ""
    ["expire"]=>
    NULL
    ["path"]=>
    string(1) "/"
    ["secure"]=>
    bool(false)
    ["httpOnly"]=>
    bool(true)
  }
}

Yii2的security安全组件功能比较复杂,值得研究,以备以后做安全时借鉴。

退出时:

_csrf-backend详细:

CuwhawBszNhDMPHaMKuybDNL1spYBVZl0jTvsCdMB9M-vXszSFz7ujpiwoUD3PM2cSSQ-wFGCTOze7rGES5svg==

请求Cookie中:

_identity-backend详细:

bb2e3c9536c3fe6a1399519c4d43c3e7a0565fdeed79f33d95efbad9e1486781a:2:{i:0;s:17:"_identity-backend";i:1;s:46:"[1,"ZvB1Jn0NvP9M0vdLt82W9zVs8ROo8NSS",2592000]";}

_csrf-backend详细:

e464895e073b220c0265aa9148836f833eedefd2cafb61cbfa24fbf940d71967a:2:{i:0;s:13:"_csrf-backend";i:1;s:32:"4QZXH07byR3_3wAZBoF1YC_VaOUv6bkm";}

响应Cookie中:

_identity-backend做删除(deleted)操作。

退出后,访问页面site/index时:

_csrf-backend详细:

e464895e073b220c0265aa9148836f833eedefd2cafb61cbfa24fbf940d71967a:2:{i:0;s:13:"_csrf-backend";i:1;s:32:"4QZXH07byR3_3wAZBoF1YC_VaOUv6bkm";}

可以看到没有_identity-backend。

紧接着再次登录:

注意对比上面的异同。

可以看出,服务器除了发送 _identity-backend 这个cookie过来,还修改了 advanced-backend 这个session相关的cookie的值, 想想也对,session是记录登录状态的,如果不变,怎么区分是不是同一个人、有没有登录呢。

过程研究

接下来,我们研究下过程。

backend/controllers/SiteController中:

    /**
     * Login action.
     *
     * @return string
     */
    public function actionLogin()
    {
        if (!Yii::$app->user->isGuest) {
            return $this->goHome();
        }

        $model = new AdminLoginForm();
        if ($model->load(Yii::$app->request->post()) && $model->login()) {
            return $this->goBack();
        } else {
            return $this->render('login', [
                'model' => $model,
            ]);
        }
    }

我们继承yii\base\Model并改造的common/models/AdminLoginForm中:

    /**
     * Logs in a user using the provided username and password.
     *
     * @return bool whether the user is logged in successfully
     */
    public function login()
    {
        if ($this->validate()) {
            return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
        } else {
            return false;
        }
    }

这里user组件:

'components' => [
    'user' => [
        'identityClass' => 'common\models\Adminuser',
        'enableAutoLogin' => true,
        'identityCookie' => ['name' => '_identity-backend', 'httpOnly' => true],
    ],
],   

这里user组件还有一个属性在 yii\web\Application中:

'user' => ['class' => 'yii\web\User'],

yii\web\User 重点是用户行为相关的方法,如登录、登出、检查权限等; identityClass 则是用户属性相关的方法集,如 AR操作、密码设置等。如果有其他要求,我们可以继承并拓展这两个类。

common\models\Adminuser继承\yii\db\ActiveRecord,实现yii\web\IdentityInterface:

class Adminuser extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
{
    ...
}

在这里你是找不到login方法的。上面的配置应该说只是配置组件的参数,真正的加载组件要从下面来寻找:

backend\web\index.php:

defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/../../vendor/autoload.php');
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../../common/config/bootstrap.php');
require(__DIR__ . '/../config/bootstrap.php');

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../common/config/main.php'),
    require(__DIR__ . '/../../common/config/main-local.php'),
    require(__DIR__ . '/../config/main.php'),
    require(__DIR__ . '/../config/main-local.php')
);

(new yii\web\Application($config))->run();

yii\web\Application中:

    /**
     * Returns the user component.
     * @return User the user component.
     */
    public function getUser()
    {
        return $this->get('user');
    }

    /**
     * @inheritdoc
     */
    public function coreComponents()
    {
        return array_merge(parent::coreComponents(), [
            'request' => ['class' => 'yii\web\Request'],
            'response' => ['class' => 'yii\web\Response'],
            'session' => ['class' => 'yii\web\Session'],
            'user' => ['class' => 'yii\web\User'],
            'errorHandler' => ['class' => 'yii\web\ErrorHandler'],
        ]);
    }

yii\web\User中:

/**
 * 用户登录
 *
 * 用户登录成功后:
 * - 用户的身份信息可从[[identity]]属性获得
 *
 * 如果[[enableSession]]为“ true”:
 * - 身份信息将存储在session中,并在以后的请求中可用
 * - `$duration == 0`时: session 持久有效,直到用户关闭浏览器
 * - `$duration > 0`时: 当 [[enableAutoLogin]] 设置为 `true`时, 在cookie 的 `$duration` 可用秒内,session 持久有效
 *
 * 如果[[enableSession]]为`false`:
 * - `$duration` 参数将会被忽略
 *
 * @param IdentityInterface $identity 用户身份信息 (应该已经通过了授权认证,即账号和密码正确)
 * @param int $duration 用户可以保持登录状态的秒数,默认为0
 * @return bool 用户是否已登录
 */
public function login(IdentityInterface $identity, $duration = 0)
{
    if ($this->beforeLogin($identity, false, $duration)) {
        $this->switchIdentity($identity, $duration);
        $id = $identity->getId();
        $ip = Yii::$app->getRequest()->getUserIP();
        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);
    }

    return !$this->getIsGuest();
}

/**
 * 切换到当前用户的新身份
 *
 * 当 [[enableSession]] 为true时,此方法可以根据`$ duration`的值使用 session / cookie 来存储用户身份信息。
 * 有关更多详细信息,请参考[[login()]]。
 *
 * 此方法主要由 [[login()]], [[logout()]] and [[loginByCookie()]] 调用,当前用户需要与相应的身份信息相关联时。
 *
 * @param IdentityInterface|null $identity 与当前用户关联的身份信息;如果为null,则表示切换当前用户为访客。
 * @param int $duration 用户可以保持登录状态的秒数。仅当$ identity不为null时,才使用此参数。
 */
public function switchIdentity($identity, $duration = 0)
{
    $this->setIdentity($identity);

    /* 如果不能session登录,直接返回 */
    if (!$this->enableSession) {
        return;
    }

    /* Ensure any existing identity cookies are removed. */
    if ($this->enableAutoLogin) {
        // 如果可以自动登录,则把旧的identity cookies清空
        $this->removeIdentityCookie();
    }

    $session = Yii::$app->getSession();  // session组件
    if (!YII_ENV_TEST) {
        $session->regenerateID(true);
    }
    $session->remove($this->idParam);
    $session->remove($this->authTimeoutParam);

    /* 用户身份信息不为空,设置session */
    if ($identity) {
        $session->set($this->idParam, $identity->getId());
        if ($this->authTimeout !== null) {
            $session->set($this->authTimeoutParam, time() + $this->authTimeout);
        }
        if ($this->absoluteAuthTimeout !== null) {
            $session->set($this->absoluteAuthTimeoutParam, time() + $this->absoluteAuthTimeout);
        }
        if ($duration > 0 && $this->enableAutoLogin) {
            // 如果 $duration 时长大于0,并且可以自动登录,发送 identity cookie
            $this->sendIdentityCookie($identity, $duration);
        }
    }
}

/**
 * 发送identity身份 cookie
 * [[enableAutoLogin]]为true时使用此方法
 * cookie中包含有: [[id]], [[IdentityInterface::getAuthKey()|auth key]], 基于cookie登录的时长
 * @param IdentityInterface $identity
 * @param int $duration 用户可以保持登录状态的秒数。
 * @see loginByCookie()
 */
protected function sendIdentityCookie($identity, $duration)
{
    $cookie = new Cookie($this->identityCookie);
    $cookie->value = json_encode([
        $identity->getId(),       // 用户id
        $identity->getAuthKey(),  // 这里就是我们说的 auth_key
        $duration,                // 时长
    ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $cookie->expire = time() + $duration;  // cookie到期时间
    Yii::$app->getResponse()->getCookies()->add($cookie); 
}

switchIdentity设置认证信息,登录说到底就是在设置认证信息。

这个方法主要有三个功能

1.设置session的有效期

2.如果cookie的有效期大于0并且允许自动登录,那么就把用户的认证信息保存到cookie中

3.如果允许自动登录,删除cookie信息。这个是用于退出的时候调用的。退出的时候传递进来的$identity为null

sendIdentityCookie存储在cookie中的用户信息包含有三个值:

$identity->getId()

$identity->getAuthKey()

$duration

getId()和getAuthKey()是在IdentityInterface接口中的。我们也知道在设置User组件的时候, 这个User Model是必须要实现IdentityInterface接口的。所以,可以在User Model中得到前两个值, 第三值就是cookie的有效期。

common\models\Adminuser继承\yii\db\ActiveRecord,实现yii\web\IdentityInterface:

class Adminuser extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
{
    ...

    /**
     * @inheritdoc
     */
    public function getId()
    {
        return $this->getPrimaryKey();
    }

    /**
     * @inheritdoc
     */
    public function getAuthKey()
    {
        return $this->auth_key;
    }

    /**
     * @inheritdoc
     */
    public function validateAuthKey($authKey)
    {
        return $this->getAuthKey() === $authKey;
    }

    /**
     * Validates password
     *
     * @param string $password password to validate
     * @return bool if password provided is valid for current user
     */
    public function validatePassword($password)
    {
        return Yii::$app->security->validatePassword($password, $this->password_hash);
    }

    /**
     * Generates password hash from password and sets it to the model
     *
     * @param string $password
     */
    public function setPassword($password)
    {
        $this->password_hash = Yii::$app->security->generatePasswordHash($password);
    }

    /**
     * Generates "remember me" authentication key
     */
    public function generateAuthKey()
    {
        $this->auth_key = Yii::$app->security->generateRandomString();
    }

    /**
     * Generates new password reset token
     */
    public function generatePasswordResetToken()
    {
        $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time();
    }

    /**
     * Removes password reset token
     */
    public function removePasswordResetToken()
    {
        $this->password_reset_token = null;
    }    
}

cookie登录

从上面我们知道用户的认证信息已经存储到cookie中了,那么下次的时候直接从cookie里面取信息然后设置就可以了。

Yii提供了AccessControl来判断用户是否登录,有了这个就不需要在每一个action里面再判断了。

我们看一下backend\controllers\SiteController中:

use yii\filters\AccessControl;

...

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'rules' => [
                    [
                        'actions' => ['login', 'error'],
                        'allow' => true,
                    ],
                    [
                        'actions' => ['logout', 'index'],
                        'allow' => true,
                        'roles' => ['@'],
                    ],
                ],
            ],
            'verbs' => [
                'class' => VerbFilter::className(),
                'actions' => [
                    'logout' => ['post'],
                ],
            ],
        ];
    }

yii\filters\AccessControl ,ACF访问过滤控制:

namespace yii\filters;

use Yii;
use yii\base\Action;
use yii\base\ActionFilter;
use yii\di\Instance;
use yii\web\User;
use yii\web\ForbiddenHttpException;

/**
 * AccessControl provides simple access control based on a set of rules.
 *
 * AccessControl is an action filter. It will check its [[rules]] to find
 * the first rule that matches the current context variables (such as user IP address, user role).
 * The matching rule will dictate whether to allow or deny the access to the requested controller
 * action. If no rule matches, the access will be denied.
 *
 * To use AccessControl, declare it in the `behaviors()` method of your controller class.
 * For example, the following declarations will allow authenticated users to access the "create"
 * and "update" actions and deny all other users from accessing these two actions.
 *
 *
 * public function behaviors()
 * {
 *     return [
 *         'access' => [
 *             'class' => \yii\filters\AccessControl::className(),
 *             'only' => ['create', 'update'],
 *             'rules' => [
 *                 // deny all POST requests
 *                 [
 *                     'allow' => false,
 *                     'verbs' => ['POST']
 *                 ],
 *                 // allow authenticated users
 *                 [
 *                     'allow' => true,
 *                     'roles' => ['@'],
 *                 ],
 *                 // everything else is denied
 *             ],
 *         ],
 *     ];
 * }
 * 
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class AccessControl extends ActionFilter
{
    /**
     * @var User|array|string|false the user object representing the authentication status or the ID of the user application component.
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
     * Starting from version 2.0.12, you can set it to `false` to explicitly switch this component support off for the filter.
     */
    public $user = 'user';
    /**
     * @var callable a callback that will be called if the access should be denied
     * to the current user. If not set, [[denyAccess()]] will be called.
     *
     * The signature of the callback should be as follows:
     *
     * 
     * function ($rule, $action)
     * 
     *
     * where `$rule` is the rule that denies the user, and `$action` is the current [[Action|action]] object.
     * `$rule` can be `null` if access is denied because none of the rules matched.
     */
    public $denyCallback;
    /**
     * @var array the default configuration of access rules. Individual rule configurations
     * specified via [[rules]] will take precedence when the same property of the rule is configured.
     */
    public $ruleConfig = ['class' => 'yii\filters\AccessRule'];
    /**
     * @var array a list of access rule objects or configuration arrays for creating the rule objects.
     * If a rule is specified via a configuration array, it will be merged with [[ruleConfig]] first
     * before it is used for creating the rule object.
     * @see ruleConfig
     */
    public $rules = [];


    /**
     * Initializes the [[rules]] array by instantiating rule objects from configurations.
     */
    public function init()
    {
        parent::init();
        if ($this->user !== false) {
            $this->user = Instance::ensure($this->user, User::className());
        }
        foreach ($this->rules as $i => $rule) {
            if (is_array($rule)) {
                $this->rules[$i] = Yii::createObject(array_merge($this->ruleConfig, $rule));
            }
        }
    }

    /**
     * This method is invoked right before an action is to be executed (after all possible filters.)
     * You may override this method to do last-minute preparation for the action.
     * @param Action $action the action to be executed.
     * @return bool whether the action should continue to be executed.
     */
    public function beforeAction($action)
    {
        $user = $this->user;
        $request = Yii::$app->getRequest();
        /* @var $rule AccessRule */
        foreach ($this->rules as $rule) {
            // 判断是否允许访问
            if ($allow = $rule->allows($action, $user, $request)) {
                return true;
            } elseif ($allow === false) {
                if (isset($rule->denyCallback)) {
                    call_user_func($rule->denyCallback, $rule, $action);
                } elseif ($this->denyCallback !== null) {
                    call_user_func($this->denyCallback, $rule, $action);
                } else {
                    $this->denyAccess($user);
                }
                return false;
            }
        }
        if ($this->denyCallback !== null) {
            call_user_func($this->denyCallback, null, $action);
        } else {
            $this->denyAccess($user);
        }
        return false;
    }

    /**
     * Denies the access of the user.
     * The default implementation will redirect the user to the login page if he is a guest;
     * if the user is already logged, a 403 HTTP exception will be thrown.
     * @param User|false $user the current user or boolean `false` in case of detached User component
     * @throws ForbiddenHttpException if the user is already logged in or in case of detached User component.
     */
    protected function denyAccess($user)
    {
        if ($user !== false && $user->getIsGuest()) {
            $user->loginRequired();
        } else {
            throw new ForbiddenHttpException(Yii::t('yii', 'You are not allowed to perform this action.'));
        }
    }
}

yii\filters\AccessRule中:

    /**
     * Checks whether the Web user is allowed to perform the specified action.
     * @param Action $action the action to be performed
     * @param User|false $user the user object or `false` in case of detached User component
     * @param Request $request
     * @return bool|null `true` if the user is allowed, `false` if the user is denied, `null` if the rule does not apply to the user
     */
    public function allows($action, $user, $request)
    {
        if ($this->matchAction($action)
            && $this->matchRole($user)
            && $this->matchIP($request->getUserIP())
            && $this->matchVerb($request->getMethod())
            && $this->matchController($action->controller)
            && $this->matchCustom($action)
        ) {
            return $this->allow ? true : false;
        }

        return null;
    }

    /**
     * @param User $user the user object
     * @return bool whether the rule applies to the role
     * @throws InvalidConfigException if User component is detached
     */
    protected function matchRole($user)
    {
        if (empty($this->roles)) {
            return true;
        }
        if ($user === false) {
            throw new InvalidConfigException('The user application component must be available to specify roles in AccessRule.');
        }
        foreach ($this->roles as $role) {
            if ($role === '?') {
                if ($user->getIsGuest()) {
                    return true;
                }
            } elseif ($role === '@') {
                // 只允许登录的用户访问,判断是不是没登录的访客,这里会自动 刷新session、cookie,还有自动用 session登录、cookie登录
                if (!$user->getIsGuest()) {
                    return true;
                }
            } else {
                if (!isset($roleParams)) {
                    $roleParams = $this->roleParams instanceof Closure ? call_user_func($this->roleParams, $this) : $this->roleParams;
                }
                if ($user->can($role, $roleParams)) {
                    return true;
                }
            }
        }

        return false;
    }

yii\web\User中:

    /**
     * 获取是不是没登录的访客,这里会调用getIdentity() 自动 刷新session、cookie,还有自动用 session登录、cookie登录
     * Returns a value indicating whether the user is a guest (not authenticated).
     * @return bool whether the current user is a guest.
     * @see getIdentity()
     */
    public function getIsGuest()
    {
        return $this->getIdentity() === null;
    }

    /**
     * Returns the identity object associated with the currently logged-in user.
     * When [[enableSession]] is true, this method may attempt to read the user's authentication data
     * stored in session and reconstruct the corresponding identity object, if it has not done so before.
     * @param bool $autoRenew whether to automatically renew authentication status if it has not been done so before.
     * This is only useful when [[enableSession]] is true.
     * @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).
     * @see login()
     * @see logout()
     */
    public function getIdentity($autoRenew = true)
    {
        if ($this->_identity === false) {
            if ($this->enableSession && $autoRenew) {
                $this->_identity = null;
                // 刷新授权状态
                $this->renewAuthStatus();
            } else {
                return null;
            }
        }

        return $this->_identity;
    }

    /**
     * 刷新授权状态,刷新session、cookie
     * Updates the authentication status using the information from session and cookie.
     *
     * This method will try to determine the user identity using the [[idParam]] session variable.
     *
     * 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()
    {
        $session = Yii::$app->getSession();
        $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null;

        if ($id === null) {
            $identity = null;
        } else {
            /* @var $class IdentityInterface */
            $class = $this->identityClass;
            $identity = $class::findIdentity($id);
        }

        $this->setIdentity($identity);

        if ($identity !== null && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
            $expire = $this->authTimeout !== null ? $session->get($this->authTimeoutParam) : null;
            $expireAbsolute = $this->absoluteAuthTimeout !== null ? $session->get($this->absoluteAuthTimeoutParam) : null;
            if ($expire !== null && $expire < time() || $expireAbsolute !== null && $expireAbsolute < time()) {
                $this->logout(false);
            } elseif ($this->authTimeout !== null) {
                $session->set($this->authTimeoutParam, time() + $this->authTimeout);
            }
        }

        if ($this->enableAutoLogin) {
            if ($this->getIsGuest()) {
                $this->loginByCookie();
            } elseif ($this->autoRenewCookie) {
                $this->renewIdentityCookie();
            }
        }
    }

    /**
     * Logs in a user by cookie.
     *
     * This method attempts to log in a user using the ID and authKey information
     * provided by the [[identityCookie|identity cookie]].
     */
    protected function loginByCookie()
    {
        $data = $this->getIdentityAndDurationFromCookie();
        if (isset($data['identity'], $data['duration'])) {
            $identity = $data['identity'];
            $duration = $data['duration'];
            if ($this->beforeLogin($identity, true, $duration)) {
                $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
                $id = $identity->getId();
                $ip = Yii::$app->getRequest()->getUserIP();
                Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__);
                $this->afterLogin($identity, true, $duration);
            }
        }
    }

    /**
     * Determines if an identity cookie has a valid format and contains a valid auth key.
     * This method is used when [[enableAutoLogin]] is true.
     * This method attempts to authenticate a user using the information in the identity cookie.
     * @return array|null Returns an array of 'identity' and 'duration' if valid, otherwise null.
     * @see loginByCookie()
     * @since 2.0.9
     */
    protected function getIdentityAndDurationFromCookie()
    {
        $value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
        if ($value === null) {
            return null;
        }
        $data = json_decode($value, true);
        if (count($data) == 3) {
            list ($id, $authKey, $duration) = $data;
            /* @var $class IdentityInterface */
            $class = $this->identityClass;
            $identity = $class::findIdentity($id);
            if ($identity !== null) {
                if (!$identity instanceof IdentityInterface) {
                    throw new InvalidValueException("$class::findIdentity() must return an object implementing IdentityInterface.");
                } elseif (!$identity->validateAuthKey($authKey)) {
                    Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
                } else {
                    return ['identity' => $identity, 'duration' => $duration];
                }
            }
        }
        $this->removeIdentityCookie();
        return null;
    }

    /**
     * Removes the identity cookie.
     * This method is used when [[enableAutoLogin]] is true.
     * @since 2.0.9
     */
    protected function removeIdentityCookie()
    {
        Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie));
    }

在上面的AccessControl访问控制里面间接通过IsGuest属性来判断是否是认证用户, 然后在getIsGuest方法里面是调用getIdentity来获取用户信息,如果不为空就说明是认证用户, 否则就是游客(未登录)。

renewAuthStatus 重新生成用户认证信息。这一部分先通过session来判断用户, 因为用户登录后就已经存在于session中了。然后再判断如果是自动登录,那么就通过cookie信息来登录。

通过保存的Cookie信息来登录 loginByCookie。

先读取cookie值,然后$data = json_decode($value, true);反序列化为数组。

这个从上面的代码可以知道要想实现自动登录,这三个值都必须有值。另外, 在User Model中还必须要实现findIdentity、validateAuthKey这两个方法。

登录完成后,还可以再重新设置cookie的有效期,这样便能一起有效下去了。

$this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);

退出 logout

backend\controllers\SiteController中:

/**
 * Logout action.
 *
 * @return string
 */
public function actionLogout()
{
    Yii::$app->user->logout();

    return $this->goHome();
}

yii\web\User中:

/**
 * Logs out the current user.
 * This will remove authentication-related session data.
 * If `$destroySession` is true, all session data will be removed.
 * @param bool $destroySession whether to destroy the whole session. Defaults to true.
 * This parameter is ignored if [[enableSession]] is false.
 * @return bool whether the user is logged out
 */
public function logout($destroySession = true)
{
    $identity = $this->getIdentity();
    if ($identity !== null && $this->beforeLogout($identity)) {
        $this->switchIdentity(null);
        $id = $identity->getId();
        $ip = Yii::$app->getRequest()->getUserIP();
        Yii::info("User '$id' logged out from $ip.", __METHOD__);
        if ($destroySession && $this->enableSession) {
            Yii::$app->getSession()->destroy();
        }
        $this->afterLogout($identity);
    }

    return $this->getIsGuest();
}

/**
 * Switches to a new identity for the current user.
 *
public function switchIdentity($identity, $duration = 0)
{
    $this->setIdentity($identity);

    if (!$this->enableSession) {
        return;
    }

    /* Ensure any existing identity cookies are removed. */
    if ($this->enableAutoLogin) {
        $this->removeIdentityCookie();
    }

    $session = Yii::$app->getSession();
    if (!YII_ENV_TEST) {
        $session->regenerateID(true);
    }
    $session->remove($this->idParam);
    $session->remove($this->authTimeoutParam);

    if ($identity) {
        $session->set($this->idParam, $identity->getId());
        if ($this->authTimeout !== null) {
            $session->set($this->authTimeoutParam, time() + $this->authTimeout);
        }
        if ($this->absoluteAuthTimeout !== null) {
            $session->set($this->absoluteAuthTimeoutParam, time() + $this->absoluteAuthTimeout);
        }
        if ($duration > 0 && $this->enableAutoLogin) {
            $this->sendIdentityCookie($identity, $duration);
        }
    }
}

/**
 * Removes the identity cookie.
 * This method is used when [[enableAutoLogin]] is true.
 * @since 2.0.9
 */
protected function removeIdentityCookie()
{
    Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie));
}

退出的时候先把当前的认证设置为null,然后再判断如果是自动登录功能则再删除相关的cookie信息。

总结

auth_key是用来做cookie自动登录验证用的,是一个随机字符串,在创建用户时第一次生成,之后基本不会再修改, 总不能把密码放入cookie中,所以选了auth_key。

    /**
     * Signs user up.
     *
     * @return User|null the saved model or null if saving fails
     */
    public function signup()
    {
        if (!$this->validate()) {
            return null;
        }
        
        $user = new Adminuser();
        $user->username = $this->username;
        $user->nickname = $this->nickname;
        $user->email = $this->email;
        $user->profile = $this->profile;
        $user->setPassword($this->password);
        $user->generateAuthKey();
//        $user->generatePasswordResetToken();  // 初次添加时为空,在找回密码发送邮件时会用到
        $user->password = $user->password_hash;
//        $user->save(); VarDumper::dump($user->errors);exit(0);
        
        return $user->save() ? $user : null;
    }

注意

用户C在登录状态,管理员A修改了C的密码和auth_key,则C现在还可以访问网站不?

如果服务器上还有C的session,则C还可以访问网站,暂时与C的cookie无关, 如果服务器上没有C的session,则C需要用cookie自动登录,但是数据库中C的auth_key已经变了, cookie自动登录不成功,也不能访问网站了。






参考资料

http://www.yii-china.com/post/detail/323.html

Yii2 csrf综合 https://ibaiyang.github.io/blog/yii2/2017/12/03/Yii2-csrf%E7%BB%BC%E5%90%88.html


返回