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

最近在研究如何利用 yii2 搭建 restful api,将 yii2 restful api / yii2 rest api 搭建心得写下,欢迎一起讨论
做完了yii2 restful api 搭建,就需要进行 auth 接口认证和定义返回码了

一、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"}}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容