美文网首页Yii2PHP经验分享
yii2 restful 风格搭建(二)接口认证

yii2 restful 风格搭建(二)接口认证

作者: smoke_zl | 来源:发表于2018-03-18 17:34 被阅读320次

    做完了基本的 restful 搭建,就需要接口认证和定义返回码了

    一、yii2 支持的 3种认证方式

    1、HTTP 基本认证: \yii\filters\auth\HttpBasicAuth

    支持两种认证方式,输入用户名和密码和只输入用户名(或 access_token)

    (1)默认是只输入用户名(或acdess_token)
    The default implementation of HttpBasicAuth uses the [[\yii\web\User::loginByAccessToken()|loginByAccessToken()]]
    method of the `user` application component and only passes the user name. 
    This implementation is used for authenticating API clients.
    

    只输入用户名认证需要在你的 user identity class 类中实现 findIdentityByAccessToken() 方法

    (2)如果需要验证用户名和密码,HttpBasicAuth 中的注释中也说明了配置方法
    public function behaviors()
    {
        return [
            'basicAuth' => [
                'class' => \yii\filters\auth\HttpBasicAuth::className(),
                'auth' => function ($username, $password) {
                    $user = User::find()->where(['username' => $username])->one();
                    if ($user->verifyPassword($password)) {
                        return $user;
                    }
                    return null;
                },
            ],
        ];
    }
    

    客户端调用时,可以header中传入 Authorization:Basic 用户名:密码 (或只用户名/access_token)的base64加密字符串

    2、OAuth2认证: \yii\filters\auth\HttpBearerAuth

    从认证服务器上获取基于OAuth2协议的access token,然后通过 HTTP Bearer Tokens 发送到API 服务器。
    同样也是客户端 header中传入 Authorization:Bearer xxxxxx,然后在你的 user identity class 类中实现 findIdentityByAccessToken() 方法

    3、JSONP请求: \yii\filters\auth\QueryParamAuth

    在 URL请求参数中加入 access_token,这种方式应主要用于JSONP请求,因为它不能使用 HTTP 头来发送access token
    比如:http://localhost/user/index/index?access-token=123

    二、根据需求,为 restful api 增加业务逻辑增加验证和接口返回码

    1、业务需求

    (1)用户注册接口
    (2)用户登录接口
    (3)获取商品信息接口
    (4)三个接口在调用时,都要传递 sign 参数, 如果客户端传递的 sign 参数和服务端计算出的 sign 不一致,就认为是非法请求,sign 参数的加密算法是

    isset($params['sign']) && unset($params['sign']);
    ksort($params);
    //$privateKey 为客户端和服务端协商好的一个秘钥
    $sign = md5($privateKey . implode(',', $params))
    

    (5)用户注册接口和登录接口,不需要 access_token 验证,获取商品信息接口 需要 access_token 验证,access_token 的验证就使用 yii2 自带的 \yii\filters\auth\HttpBasicAuth

    2、user 表就用 yii2 自带的 user 表
    CREATE TABLE `user` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
      `auth_key` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
      `password_hash` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
      `password_reset_token` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
      `email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
      `status` smallint(6) NOT NULL DEFAULT '10',
      `created_at` int(11) NOT NULL,
      `updated_at` int(11) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `username` (`username`),
      UNIQUE KEY `email` (`email`),
      UNIQUE KEY `password_reset_token` (`password_reset_token`)
    ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    
    3、为了以后方便修改和扩展,写一个 rest controller 基类,\frontend\extensions\RestApiBaseController,不用自带的 \yii\rest\ActiveController,大体上和 \yii\rest\ActiveController 差不多
    <?php
    
    namespace frontend\extensions;
    
    use yii\base\Model;
    use yii\rest\Controller;
    use yii\base\InvalidConfigException;
    use yii\filters\auth\HttpBasicAuth;
    use frontend\extensions\HttpSignAuth;
    
    class RestApiBaseController extends Controller
    {
        public $modelClass;
        /**
         * @var string the scenario used for updating a model.
         * @see \yii\base\Model::scenarios()
         */
        public $updateScenario = Model::SCENARIO_DEFAULT;
        /**
         * @var string the scenario used for creating a model.
         * @see \yii\base\Model::scenarios()
         */
        public $createScenario = Model::SCENARIO_DEFAULT;
    
        public function init()
        {
            parent::init();
            if ($this->modelClass === null) {
                throw new InvalidConfigException('The "modelClass" property must be set.');
            }
        }
    
        /**
         * 重写 behaviors
         */
        public function behaviors()
        {
            return [
                //增加新的接口验证类,参数加密的sign
                'tokenValidate'     => [
                    //参数加密的sign所有接口都需要验证
                    'class'     => HttpSignAuth::className(),
                ],
                'authValidate'      => [
                    'class'     => HttpBasicAuth::className(),
                    //access-token 部分接口需要验证,需要排除比如 login register 这样的接口
                    'optional'  => ['register', 'login'],
                ],
            ];
        }
    
        public function actions()
        {
            return [
                'index' => [
                    'class' => 'yii\rest\IndexAction',
                    'modelClass' => $this->modelClass,
                    'checkAccess' => [$this, 'checkAccess'],
                ],
                'view' => [
                    'class' => 'yii\rest\ViewAction',
                    'modelClass' => $this->modelClass,
                    'checkAccess' => [$this, 'checkAccess'],
                ],
                'create' => [
                    'class' => 'yii\rest\CreateAction',
                    'modelClass' => $this->modelClass,
                    'checkAccess' => [$this, 'checkAccess'],
                    'scenario' => $this->createScenario,
                ],
                'update' => [
                    'class' => 'yii\rest\UpdateAction',
                    'modelClass' => $this->modelClass,
                    'checkAccess' => [$this, 'checkAccess'],
                    'scenario' => $this->updateScenario,
                ],
                'delete' => [
                    'class' => 'yii\rest\DeleteAction',
                    'modelClass' => $this->modelClass,
                    'checkAccess' => [$this, 'checkAccess'],
                ],
                'options' => [
                    'class' => 'yii\rest\OptionsAction',
                ],
            ];
        }
    
        /**
         * {@inheritdoc}
         */
        protected function verbs()
        {
            return [
                'index' => ['GET', 'HEAD'],
                'view' => ['GET', 'HEAD'],
                'create' => ['POST'],
                'update' => ['PUT', 'PATCH'],
                'delete' => ['DELETE'],
            ];
        }
    
        public function checkAccess($action, $model = null, $params = [])
        {
        }
    }
    
    4、实现 user identity class 类中的 findIdentityByAccessToken,我的 user identity class 是 \frontend\models\User
    public static function findIdentityByAccessToken($token, $type = null)
    {
        if(empty($token)){
            return null;
        }
        return static::findOne(['auth_key' => $token, 'status' => self::STATUS_ACTIVE]);
    }
    
    5、GoodsController 继承的父类,改成 RestApiBaseController
    6、错误码和出现错误时抛出的异常统一管理,编写 ErrorCode 类和 ApiHttpException 类
    (1)ErrorCode 类
    <?php
        
    namespace frontend\extensions;
    
    class ErrorCode{
    
        private static $error = [
            'system_error'  => [
                'status'    => 500,
                'code'      => 500000,
                'msg'       => 'system error',
            ],
            'auth_error'    => [
                'status'=> 401,
                'code'  => 400000,
                'msg'   => 'auth error',
            ],
            'params_error'  => [
                'status'=> 401,
                'code'  => 400001,
                'msg'   => 'params error',
            ],
        ];
    
        private function __construct(){
    
        }
    
        public static function getError($key){
            if(empty($key) || !isset(self::$error[$key])){
                throw new \Exception("error code not exist", 400);
            }
            return self::$error[$key];
        }
    }
    
    (2)ApiHttpException 类
    <?php
    
    namespace frontend\extensions;
    
    use Yii;
    use yii\web\HttpException;
    
    class ApiHttpException extends HttpException{
    
        public function __construct($status, $message = null, $code = 0, \Exception $previous = null)
        {
            $this->statusCode = $status;
            parent::__construct($status, $message, $code, $previous);
        }
    }
    
    7、编写 sign 验证类 HttpSignAuth
    <?php
    
    namespace frontend\extensions;
    
    use Yii;
    use yii\base\Behavior;
    use yii\web\Controller;
    use frontend\extensions\ErrorCode;
    use frontend\extensions\ApiHttpException;
    /**
     * sign 验证类
     */
    class HttpSignAuth extends Behavior{
    
        public $privateKey = '12345678';
    
        public $signParam = 'sign';
    
        public function events() {
            return [Controller::EVENT_BEFORE_ACTION => 'beforeAction'];
        }
    
        public function beforeAction($event) {
            //获取 sign
            $sign = Yii::$app->request->get($this->signParam, null);
            $getParams = Yii::$app->request->get();
            $postParams = Yii::$app->request->post();
            $params = array_merge($getParams, $postParams);
            if(empty($sign) || !$this->checkSign($sign, $params)){
                $error = ErrorCode::getError('auth_error');
                throw new ApiHttpException($error['status'], $error['msg'], $error['code']);
            }
            return true;
        }
    
        private function checkSign($sign, $params) {
            unset($params[$this->signParam]);
            ksort($params);
            return md5($this->privateKey . implode(',', $params)) === $sign;
        }
    }
    
    8、增加包含用户登录和注册接口的 UserController
    <?php
    
    namespace frontend\modules\v1\controllers;  
      
    use Yii;
    use frontend\models\User;
    use frontend\extensions\ErrorCode;
    use frontend\extensions\ApiHttpException;
    use frontend\extensions\RestApiBaseController;
      
    class UserController extends RestApiBaseController 
    {  
        public $modelClass = 'frontend\models\User';
    
        public function actionRegister(){
            //为了方便,这里只做了非常简单的参数验证
            if(!Yii::$app->request->isPost){
                $error = ErrorCode::getError('params_error');
                throw new ApiHttpException($error['status'], $error['msg'], $error['code']);
            }
            $params = Yii::$app->request->post();
            if(empty($params['name']) || empty($params['pwd']) || empty($params['email'])){
                $error = ErrorCode::getError('params_error');
                throw new ApiHttpException($error['status'], $error['msg'], $error['code']);
            }
            //用户注册
            $user = new User();
            $user->username = $params['name'];
            $user->email = $params['email'];
            $user->setPassword($params['pwd']);
            $user->generateAuthKey();
            $user->save(false);
            return [
                'error_code'    => 0,
                'res_msg'       => [
                    'uid'       => $user->primaryKey,
                    'token'     => $user->authKey,
                ]
            ];
        }
    
        public function actionLogin(){
            //为了方便,这里只做了非常简单的参数验证
            if(!Yii::$app->request->isPost){
                $error = ErrorCode::getError('params_error');
                throw new ApiHttpException($error['status'], $error['msg'], $error['code']);
            }
            $params = Yii::$app->request->post();
            if(empty($params['name']) || empty($params['pwd'])){
                $error = ErrorCode::getError('params_error');
                throw new ApiHttpException($error['status'], $error['msg'], $error['code']);
            }
            $user = User::findByUsername($params['name']);
            if (!$user || !$user->validatePassword($params['pwd'])) {
                $error = ErrorCode::getError('auth_error');
                throw new ApiHttpException($error['status'], $error['msg'], $error['code']);
            }
            return [
                'error_code'    => 0,
                'res_msg'       => [
                    'uid'       => $user->primaryKey,
                    'token'     => $user->authKey,
                ]
            ];
        }
    }
    
    9、frontend/config/main.php 中,优化用户注册、登录接口的 url
    'POST v1/login'      => '/v1/user/login',
    'POST v1/register'   => 'v1/user/register',
    
    10、测试

    (1)错误的 sign 调用 register

    命令:
    curl -X POST -s http://local.rest.com/v1/register?sign=sdasds
    返回:
    {"code":401,"msg":"auth error"}
    

    (2)正确的 sign,可是没有传 register 必须的参数 ($params = [])

    命令:
    curl -X POST -s http://local.rest.com/v1/register?sign=25d55ad283aa400af464c76d713c07ad
    返回:
    {"code":401,"msg":"params error"}
    

    (3)正确的 sign,输入 register 必须的参数

    array(
        "name"  => "smoke1",
        "email" => "smoke1@sina.com",
        "pwd"   => "123456",
    )
    
    命令:
    curl -X POST -d "name=smoke1&email=smoke1@sina.com&pwd=123456" -s http://local.rest.com/v1/register?sign=2e3ef98ccb57bf57f73ecd4745052c96
    返回:
    {"code":0,"msg":{"uid":10,"token":"J1RS0lHs-XUzNWxj3LMtH15h1j81lPyo"}
    

    (4)使用正确的 sign 错误 token 访问 goods 接口

    array(
        "id"    => 1,
    )
    
    命令:
    curl -X GET -H "Authorization:Basic dadsadsadsadsad" -s http://local.rest.com/v1/goods/1?sign=feb8dc0697a2e0a947c6e20dc4ec3ebc
    返回:
    {"code":401,"msg":"Your request was made with invalid credentials."}
    

    (5)使用正确的 sign,正确的 token 访问 goods 接口

    命令:
    curl -X GET -H "Authorization:Basic SjFSUzBsSHMtWFV6Tld4ajNMTXRIMTVoMWo4MWxQeW86" -s http://local.rest.com/v1/goods/1?sign=feb8dc0697a2e0a947c6e20dc4ec3ebc
    返回:
    {"code":0,"msg":{"id":"1","name":"测试商品1","price":"600","status":1,"create_time":"1520490595","modify_time":"1520490595"}}
    

    相关文章

      网友评论

      • b465b57553dd:博主你好,我这个配置 cors 一直接受不到数据
      • 樂猪先生丶:请问第9条中
        'POST v1/login' => '/v1/user/login',
        'POST v1/register' => 'v1/user/register',

        在哪里配置的

        ```
        [
        'class' => 'yii\rest\UrlRule',
        'controller' => 'v1/test',
        // 'only' => ['index'],
        'extraPatterns' => [
        'GET v1/hello' => 'v1/test/hello',
        // 'GET index' => 'index',
        ],
        ]
        ```

        我这样配置 报404
        smoke_zl:写在大数组 rules 下边就行
        'rules' => [
        //当然,如果自带的路由无法满足需求,可以自己增加规则
        'GET <module:(v)\d+>/<controller:\w+>/search' => '<module>/<controller>/search',

        'POST v1/login' => '/v1/user/login',
        'POST v1/register' => 'v1/user/register',

        [
        'class' => 'yii\rest\UrlRule',
        'controller' => ['v1/goods'],
        // 由于 resetful 风格规定 URL 保持格式一致并且始终使用复数形式
        // 所以如果你的 controller 是单数的名称比如 UserController
        // 设置 pluralize 为 true (默认为 true)的话,url 地址必须是 users 才可访问
        // 如果 pluralize 设置为 false, url 地址必须是 user 也可访问
        // 如果你的 controller 本身是复数名称 UsersController ,此参数没用,url 地址必须是 users
        'pluralize' => false,
        ],
        ],

      本文标题:yii2 restful 风格搭建(二)接口认证

      本文链接:https://www.haomeiwen.com/subject/pdaeqftx.html