码农日记

薄洪涛的个人博客

Yii2.0认证及限流

上次搭建了Yii2.0的接口框架后,现在开始搭建认证和限流模块,先说下这两个模块的作用

  1. 认证:前后端分离,每次请求都是无状态的,及每一次请求服务器不知道你是谁,你有没有登陆;我们就需要做一个认证模块去识别请求用户;

  2. 限流:为了防止接口滥用,我们可以设置这个接口,每个用户每秒钟的访问次数;

因为限流是在认证的基础上,所以我们先来说认证:流程就是我们从服务器请求获取token,然后每次请求都带着token,当然请求token肯定需要输入用户名密码或者密钥之类的,这里采用用户名密码;另外,token需要配置过期时间,过期了需要重新获取

  1. main中配置认证类,这里我使用的认证类是operator,对应数据库中的operator表

'components'=>[
        'user' => [
            'identityClass' => 'app\modules\v1\models\Operators',
            'enableAutoLogin' => true,
            //禁用session组件
            'enableSession'=>false,
        ],
 ]

2. 配置这个认证类,继承IdentityInterface这个类,数据表中增加access_token和expire_time这两个字段,想要的rules()和attributeLabels()方法中同样配置这两个字段,重写以下方法

public static function findByUsername($username = null) {
    return static::findOne(['operator_name' => $username]);
}

public function validatePassword($inputPwd, $pwd) {
    return Yii::$app->getSecurity()->validatePassword($inputPwd, $pwd) ? true : false;
}

public static function findIdentity($id)
{
    // TODO: Implement findIdentity() method.
    return static::findOne($id);
}

public static function findIdentityByAccessToken($token, $type = null)
{
    // TODO: Implement findIdentityByAccessToken() method.
    return static::findOne(['access_token' => $token]);
}

public function getId()
{
    // TODO: Implement getId() method.
    return $this->id;
}

public function getAuthKey()
{
    // TODO: Implement getAuthKey() method.
    return $this->auth_key;
}

public function validateAuthKey($authKey)
{
    // TODO: Implement validateAuthKey() method.
}

/**
 * 生成随机的token并加上时间戳
 * Generated random accessToken with timestamp
 * @throws \yii\base\Exception
 */
public function generateAccessToken() {
    $this->access_token = Yii::$app->security->generateRandomString();
    $this->expire_time = time();
}

/**
 * 验证token是否过期
 * Validates if accessToken expired
 * @param null $token
 * @return bool
 */
public static function validateAccessToken($token = null,$timestamp=0) {
    if ($token === null) {
        return false;
    } else {
        $expire = Yii::$app->params['accessTokenExpire'];
        return $timestamp + $expire >= time();
    }
}

3.既然需要获取token,我们需要一个loginForm,注意,这里的login方法和之前的login方法不一样

<?php

namespace app\modules\v1\models;

use Yii;
use yii\base\Model;

class LoginForm extends Model
{
    public $userName;
    public $password;
    private $_user = false;

    const GET_ACCESS_TOKEN = 'generate_access_token';

    public function init()
    {
        parent::init();
        $this->on(self::GET_ACCESS_TOKEN,[$this, 'onGenerateAccessToken']);
    }

    public function rules()
    {
        return [
            [['userName', 'password'], 'required'],
            ['password', 'validatePassword'],
        ];
    }
    public function attributeLabels() {
        return [
            'userName' => '用户名',
            'password' => '密码',
        ];
    }
    public function validatePassword($attribute)
    {
        if (!$this->hasErrors()) {
            $user = $this->getUser();
            if (!$user || (!$user->validatePassword($this->password,$user->password))) {
                $this->addError($attribute, '用户名/密码错误');
            }
        }
    }

    public function login()
    {
        if ($this->validate()) {
            $this->_user = $this->getUser();
            //如果token过期或者为空,直接生成新token
            if(!Operators::validateAccessToken($this->_user->access_token,$this->_user->expire_time)){
                $this->trigger(self::GET_ACCESS_TOKEN);
            }
            return Yii::$app->user->login($this->getUser(),0);
        }
        return false;
    }

    public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = Operators::findByUsername($this->userName);
        }
        return $this->_user;
    }

    /**
     * 当登录成功时更新用户的token
     * Generated new accessToken when validate successful.
     * If accessToken is invalid, generated a new token for it.
     * @throws \yii\base\Exception
     */
    public function onGenerateAccessToken() {
        if (!Operators::validateAccessToken($this->getUser()->access_token,$this->getUser()->expire_time)) {
            $this->getUser()->generateAccessToken();
            $this->getUser()->save(false);
        }
    }
}

4.我们新建接口方法去调用loginForm中的login()方法去生成或者返回token,这里要注意,不能用load,需要使用setAttributes

class LoginAction extends BaseAction {
    public function run(){
        $model = new LoginForm();
        $model->setAttributes($this->body);
        if ($model->login()) {
            $code = 0;
            $result['code'] = $code;
            $result['desc'] = $this->desc[$code];
            $result['data']['token'] = $model->user->access_token;
            return $result;
        }
        $code = 40010;
        $result['code'] = $code;
        $result['desc'] = $this->desc[$code];
        $result['data'] = [];
        return $result;
    }
}

另外,接口的基类控制器中的behaviors中需要加一下代码,yii框架会自动认证

//认证,验证
$behaviors['authenticator'] = [
    'class' => CompositeAuth::className(),
    'authMethods' => [
        HttpBasicAuth::className(),
        HttpBearerAuth::className(),
        QueryParamAuth::className(),
    ],
];

这里可以请求下v1/operator/login方法,并传递用户名和密码,得到token值

2.png

然后我们拿着这个token值去调用其它的接口,如果不传递或者传递错误,会返回401错误(无权限)

1.png

到此认证完成,其实步骤多了点,一看代码就明白了,文章最后有github地址

然后我们来说限流,限流这块yii文档中,说的特别简单(其实本来就很简单)

  1. 首先给operator这个表增加两个字段,allowance(剩余访问次数)和allowance_updated_at(访问时间)

  2. operator继承这个类RateLimitInterface,并重写以下方法

public function getRateLimit($request, $action)
{
    return [1, 1]; // 前面的1是次数,后面的1是秒数
}

public function loadAllowance($request, $action)
{
    return [$this->allowance, $this->allowance_updated_at];
}

public function saveAllowance($request, $action, $allowance, $timestamp)
{
    $this->allowance = $allowance;
    $this->allowance_updated_at = $timestamp;
    $this->save();
}

    3.我们需要在接口的基类baseController的behaviors中增加以下代码,behaviors的作用就是对类进行功能的扩充,behaviors的作用就是对类进行功能的扩充,behaviors的作用就是对类进行功能的扩充 ,不懂的同学请看我之前写的文章,传送门

//速率限制
$behaviors['rateLimiter'] = [
    'class' => RateLimiter::className(),
    'enableRateLimitHeaders' => true,
];

然后就可以啦,我设置的是1s访问一次

连续请求频繁后,我们发现,接口会返回429错误码(请求频繁)

3.png

代码传送门

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

Powered By Z-BlogPHP 1.7.3

版权所有 | 转载请标明出处