yii2 resetful 授权验证详解


Posted in PHP onMay 18, 2017

什么是restful风格的api呢?我们之前有写过大篇的文章来介绍其概念以及基本操作。

既然写过了,那今天是要说点什么吗?

这篇文章主要针对实际场景中api的部署来写。

我们今天就来大大的侃侃那些年api遇到的授权验证问题!独家干活,如果看完有所受益,记得不要忘记给我点赞哦。

业务分析

我们先来了解一下整个逻辑

1.用户在客户端填写登录表单
2.用户提交表单,客户端请求登录接口login
3.服务端校验用户的帐号密码,并返回一个有效的token给客户端
4.客户端拿到用户的token,将之存储在客户端比如cookie中
5.客户端携带token访问需要校验的接口比如获取用户个人信息接口
6.服务端校验token的有效性,校验通过,反正返回客户端需要的信息,校验失败,需要用户重新登录

本文我们以用户登录,获取用户的个人信息为例进行详细的完整版说明。

以上,便是我们本篇文章要实现的重点。先别激动,也别紧张,分析好了之后,细节部分我们再一步一个脚印走下去。

准备工作

1.你应该有一个api应用.
2.对于客户端,我们准备采用postman进行模拟,如果你的google浏览器还没有安装postman,请先自行下载
3.要测试的用户表需要有一个api_token的字段,没有的请先自行添加,并保证该字段足够长度
4.api应用开启了路由美化,并先配置post类型的login操作和get类型的signup-test操作
5.关闭了user组件的session会话

关于上面准备工作的第4点和第5点,我们贴一下代码方便理解

'components' => [
 'user' => [ 
  'identityClass' => 'common\models\User',
  'enableAutoLogin' => true,
  'enableSession' => false,
 ],
 'urlManager' => [
  'enablePrettyUrl' => true,
  'showScriptName' => false,
  'enableStrictParsing' => true,
  'rules' => [
   [
    'class' => 'yii\rest\UrlRule',
    'controller' => ['v1/user'],
    'extraPatterns' => [
     'POST login' => 'login',
     'GET signup-test' => 'signup-test',
    ]
   ],
  ]
 ],
 // ......
],

signup-test操作我们后面添加测试用户,为登录操作提供便利。其他类型的操作后面看需要再做添加。

认证类的选择

我们在api\modules\v1\controllers\UserController中设定的model类指向 common\models\User类,为了说明重点这里我们就不单独拿出来重写了,看各位需要,有必要的话再单独copy一个User类到api\models下。

校验用户权限我们以 yii\filters\auth\QueryParamAuth 为例

use yii\filters\auth\QueryParamAuth;

public function behaviors() 
{
 return ArrayHelper::merge (parent::behaviors(), [ 
   'authenticator' => [ 
    'class' => QueryParamAuth::className() 
   ] 
 ] );
}

如此一来,那岂不是所有访问user的操作都需要认证了?那不行,客户端第一个访问login操作的时候哪来的token,yii\filters\auth\QueryParamAuth对外提供一个属性,用于过滤不需要验证的action。我们将UserController的behaviors方法稍作修改

public function behaviors() 
{
 return ArrayHelper::merge (parent::behaviors(), [ 
   'authenticator' => [ 
    'class' => QueryParamAuth::className(),
    'optional' => [
     'login',
     'signup-test'
    ],
   ] 
 ] );
}

这样login操作就无需权限验证即可访问了。

添加测试用户

为了避免让客户端登录失败,我们先写一个简单的方法,往user表里面插入两条数据,便于接下来的校验。 

UserController增加signupTest操作,注意此方法不属于讲解范围之内,我们仅用于方便测试。

use common\models\User;
/**
 * 添加测试用户
 */
public function actionSignupTest ()
{
 $user = new User();
 $user->generateAuthKey();
 $user->setPassword('123456');
 $user->username = '111';
 $user->email = '111@111.com';
 $user->save(false);

 return [
  'code' => 0
 ];
}

如上,我们添加了一个username是111,密码是123456的用户

登录操作

假设用户在客户端输入用户名和密码进行登录,服务端login操作其实很简单,大部分的业务逻辑处理都在api\models\loginForm上,来先看看login的实现

use api\models\LoginForm;

/**
 * 登录
 */
public function actionLogin ()
{
 $model = new LoginForm;
 $model->setAttributes(Yii::$app->request->post());
 if ($user = $model->login()) {
  if ($user instanceof IdentityInterface) {
   return $user->api_token;
  } else {
   return $user->errors;
  }
 } else {
  return $model->errors;
 }
}

登录成功后这里给客户端返回了用户的token,再来看看登录的具体逻辑的实现

新建api\models\LoginForm.PHP

<?php
namespace api\models;

use Yii;
use yii\base\Model;
use common\models\User;

/**
 * Login form
 */
class LoginForm extends Model
{
 public $username;
 public $password;

 private $_user;

 const GET_API_TOKEN = 'generate_api_token';

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


 /**
  * @inheritdoc
  * 对客户端表单数据进行验证的rule
  */
 public function rules()
 {
  return [
   [['username', 'password'], 'required'],
   ['password', 'validatePassword'],
  ];
 }

 /**
  * 自定义的密码认证方法
  */
 public function validatePassword($attribute, $params)
 {
  if (!$this->hasErrors()) {
   $this->_user = $this->getUser();
   if (!$this->_user || !$this->_user->validatePassword($this->password)) {
    $this->addError($attribute, '用户名或密码错误.');
   }
  }
 }
 /**
  * @inheritdoc
  */
 public function attributeLabels()
 {
  return [
   'username' => '用户名',
   'password' => '密码',
  ];
 }
 /**
  * Logs in a user using the provided username and password.
  *
  * @return boolean whether the user is logged in successfully
  */
 public function login()
 {
  if ($this->validate()) {
   $this->trigger(self::GET_API_TOKEN);
   return $this->_user;
  } else {
   return null;
  }
 }

 /**
  * 根据用户名获取用户的认证信息
  *
  * @return User|null
  */
 protected function getUser()
 {
  if ($this->_user === null) {
   $this->_user = User::findByUsername($this->username);
  }

  return $this->_user;
 }

 /**
  * 登录校验成功后,为用户生成新的token
  * 如果token失效,则重新生成token
  */
 public function onGenerateApiToken ()
 {
  if (!User::apiTokenIsValid($this->_user->api_token)) {
   $this->_user->generateApiToken();
   $this->_user->save(false);
  }
 }
}

我们回过头来看一下,当我们在UserController的login操作中调用LoginForm的login操作后都发生了什么

1、调用LoginForm的login方法

2、调用validate方法,随后对rules进行校验

3、rules校验中调用validatePassword方法,对用户名和密码进行校验

4、validatePassword方法校验的过程中调用LoginForm的getUser方法,通过common\models\User类的findByUsername获取用户,找不到或者common\models\User的validatePassword对密码校验失败则返回error

5、触发LoginForm::GENERATE_API_TOKEN事件,调用LoginForm的onGenerateApiToken方法,通过common\models\User的apiTokenIsValid校验token的有效性,如果无效,则调用User的generateApiToken方法重新生成

注意common\models\User类必须是用户的认证类.

下面补充本节增加的common\models\User的相关方法

/**
 * 生成 api_token
 */
public function generateApiToken()
{
 $this->api_token = Yii::$app->security->generateRandomString() . '_' . time();
}

/**
 * 校验api_token是否有效
 */
public static function apiTokenIsValid($token)
{
 if (empty($token)) {
  return false;
 }

 $timestamp = (int) substr($token, strrpos($token, '_') + 1);
 $expire = Yii::$app->params['user.apiTokenExpire'];
 return $timestamp + $expire >= time();
}

继续补充apiTokenIsValid方法中涉及到的token有效期,在api\config\params.php文件内增加即可

<?php
return [
 // ...
 // token 有效期默认1天
 'user.apiTokenExpire' => 1*24*3600,
];

到这里呢,客户端登录 服务端返回token给客户端就完成了。

按照文中一开始的分析,客户端应该把获取到的token存到本地,比如cookie中。以后再需要token校验的接口访问中,从本地读取比如从cookie中读取并访问接口即可。

根据token请求用户的认证操作

假设我们已经把获取到的token保存起来了,我们再以访问用户信息的接口为例。

yii\filters\auth\QueryParamAuth类认定的token参数是 access-token,我们可以在行为中修改下

public function behaviors() 
{
 return ArrayHelper::merge (parent::behaviors(), [ 
   'authenticator' => [ 
    'class' => QueryParamAuth::className(),
    'tokenParam' => 'token',
    'optional' => [
     'login',
     'signup-test'
    ],
   ] 
 ] );
}

这里将默认的access-token修改为token。

我们在配置文件的urlManager组件中增加对userProfile操作

'extraPatterns' => [
 'POST login' => 'login',
 'GET signup-test' => 'signup-test',
 'GET user-profile' => 'user-profile',
]

我们用postman模拟请求访问下 /v1/users/user-profile?token=apeuT9dAgH072qbfrtihfzL6qDe_l4qz_1479626145发现,抛出了一个异常

\"findIdentityByAccessToken\" is not implemented.

这是怎么回事呢?

我们找到 yii\filters\auth\QueryParamAuth 的authenticate方法,发现这里调用了 common\models\User类的loginByAccessToken方法,有同学疑惑了,common\models\User类没实现loginByAccessToken方法为啥说findIdentityByAccessToken方法没实现?如果你还记得common\models\User类实现了yii\web\user类的接口的话,你应该会打开yii\web\User类找答案。没错,loginByAccessToken方法在yii\web\User中实现了,该类中调用了common\models\User的findIdentityByAccessToken,但是我们看到,该方法中通过throw抛出了异常,也就是说这个方法要我们自己手动实现!

这好办了,我们就来实现下common\models\User类的findIdentityByAccessToken方法吧

public static function findIdentityByAccessToken($token, $type = null)
{
 // 如果token无效的话,
 if(!static::apiTokenIsValid($token)) {
  throw new \yii\web\UnauthorizedHttpException("token is invalid.");
 }

 return static::findOne(['api_token' => $token, 'status' => self::STATUS_ACTIVE]);
 // throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
}

验证完token的有效性,下面就要开始实现主要的业务逻辑部分了。

/**
 * 获取用户信息
 */
public function actionUserProfile ($token)
{
 // 到这一步,token都认为是有效的了
 // 下面只需要实现业务逻辑即可,下面仅仅作为案例,比如你可能需要关联其他表获取用户信息等等
 $user = User::findIdentityByAccessToken($token);
 return [
  'id' => $user->id,
  'username' => $user->username,
  'email' => $user->email,
 ];
}

服务端返回的数据类型定义

在postman中我们可以以何种数据类型输出的接口的数据,但是,有些人发现,当我们把postman模拟请求的地址copy到浏览器地址栏,返回的又却是xml格式了,而且我们明明在UserProfile操作中返回的是属组,怎么回事呢?

这其实是官方捣的鬼啦,我们一层层源码追下去,发现在yii\rest\Controller类中,有一个 contentNegotiator行为,该行为指定了允许返回的数据格式formats是json和xml,返回的最终的数据格式根据请求头中Accept包含的首先出现在formats中的为准,你可以在yii\filters\ContentNegotiator的negotiateContentType方法中找到答案。

你可以在浏览器的请求头中看到

Accept:

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

即application/xml首先出现在formats中,所以返回的数据格式是xml类型,如果客户端获取到的数据格式想按照json进行解析,只需要设置请求头的Accept的值等于application/json即可

有同学可能要说,这样太麻烦了,啥年代了,谁还用xml,我就想服务端输出json格式的数据,怎么做?

办法就是用来解决问题滴,来看看怎么做。api\config\main.php文件中增加对response的配置

'response' => [
 'class' => 'yii\web\Response',
 'on beforeSend' => function ($event) {
  $response = $event->sender;
  $response->format = yii\web\Response::FORMAT_JSON;
 },
],

如此,不管你客户端传什么,服务端最终输出的都会是json格式的数据了。

自定义错误处理机制

再来看另外一个比较常见的问题:

你看我们上面几个方法哈,返回的结果是各式各样的,这样就给客户端解析增加了困扰,而且一旦有异常抛出,返回的代码还都是一堆一堆的,头疼,怎么办?

说到这个问题之前呢,我们先说一下yii中先关的异常处理类,当然,有很多哈。比如下面常见的一些,其他的自己去挖掘

yii\web\BadRequestHttpException
yii\web\ForbiddenHttpException
yii\web\NotFoundHttpException
yii\web\ServerErrorHttpException
yii\web\UnauthorizedHttpException
yii\web\TooManyRequestsHttpException

实际开发中各位要善于去利用这些类去捕获异常,抛出异常。说远了哈,我们回到重点,来说如何自定义接口异常响应或者叫自定义统一的数据格式,比如向下面这种配置,统一响应客户端的格式标准。

'response' => [
 'class' => 'yii\web\Response',
 'on beforeSend' => function ($event) {
  $response = $event->sender;
  $response->data = [
   'code' => $response->getStatusCode(),
   'data' => $response->data,
   'message' => $response->statusText
  ];
  $response->format = yii\web\Response::FORMAT_JSON;
 },
],

说道了那么多,本文就要结束了,刚开始接触的同学可能有一些蒙,不要蒙,慢慢消化,先知道这么个意思,了解下restful api接口在整个过程中是怎么用token授权的就好。这样真正实际用到的时候,你也能举一反三!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
用php写的serv-u的web申请账号的程序
Oct 09 PHP
推荐一篇入门级的Class文章
Mar 19 PHP
PHP 中英文混合排版中处理字符串常用的函数
Apr 12 PHP
浅析PHP原理之变量(Variables inside PHP)
Aug 09 PHP
ThinkPHP采用实现三级循环代码实例
Jul 18 PHP
Laravel框架中扩展函数、扩展自定义类的方法
Sep 04 PHP
Symfony页面的基本创建实例详解
Jan 26 PHP
php跨服务器访问方法小结
May 12 PHP
利用php获得flv视频长度的实例代码
Oct 26 PHP
tp5框架的增删改查操作示例
Oct 31 PHP
phpQuery采集网页实现代码实例
Apr 02 PHP
ThinkPHP5.1的权限控制怎么写?分享一个AUTH权限控制
Mar 09 PHP
PHP正则+Snoopy抓取框架实现的抓取淘宝店信誉功能实例
May 17 #PHP
PHP实现电商订单自动确认收货redis队列
May 17 #PHP
老生常谈PHP面向对象之解释器模式
May 17 #PHP
phpmyadmin下载、安装、配置教程
May 16 #PHP
Windows下php+mysql5.7配置教程
May 16 #PHP
php使用curl实现ftp文件下载功能
May 16 #PHP
PHP jpgraph库的配置及生成统计图表:折线图、柱状图、饼状图
May 15 #PHP
You might like
php根据isbn书号查询amazon网站上的图书信息的示例
2014/02/13 PHP
php文件缓存类用法实例分析
2015/04/22 PHP
javascript 获取select下拉列表值的代码
2009/09/07 Javascript
JQuery index()方法使用代码
2010/06/02 Javascript
js查错流程归纳
2012/05/04 Javascript
JS文本框不能输入空格验证方法
2013/03/19 Javascript
jquery easyui combox一些实用的小方法
2013/12/25 Javascript
js字符串转换成数字与数字转换成字符串的实现方法
2014/01/08 Javascript
JS获取表格内指定单元格html内容的方法
2015/03/31 Javascript
JQuery使用index方法获取Jquery对象数组下标的方法
2015/05/18 Javascript
JavaScript控制浏览器全屏及各种浏览器全屏模式的方法、属性和事件
2015/12/20 Javascript
倾力总结40条常见的移动端Web页面问题解决方案
2016/05/24 Javascript
通过Ajax使用FormData对象无刷新上传文件方法
2016/12/08 Javascript
jQuery快速实现商品数量加减的方法
2017/02/06 Javascript
js实现带简单弹性运动的导航条
2017/02/22 Javascript
vue绑定事件后获取绑定事件中的this方法
2018/09/15 Javascript
angular ng-model 无法获取值的处理方法
2018/10/02 Javascript
Python中函数的用法实例教程
2014/09/08 Python
python编写暴力破解FTP密码小工具
2014/11/19 Python
Python入门之modf()方法的使用
2015/05/15 Python
python 的列表遍历删除实现代码
2020/04/12 Python
Python通过Pygame绘制移动的矩形实例代码
2018/01/03 Python
python验证码识别教程之滑动验证码
2018/06/04 Python
详解Python 中的容器 collections
2020/08/17 Python
html5 Canvas画图教程(1)—画图的基本常识
2013/01/09 HTML / CSS
MATCHESFASHION.COM法国官网:英国奢侈品零售商
2018/01/04 全球购物
粉红色的鲸鱼:Vineyard Vines
2018/02/17 全球购物
爱尔兰灯和灯具网上商店:Lights.ie
2018/03/26 全球购物
What is the purpose of Void class? Void类的作用是什么?
2016/10/31 面试题
优秀班集体获奖感言
2014/02/03 职场文书
新闻专业学生的自我评价
2014/02/13 职场文书
《小熊住山洞》教学反思
2014/02/21 职场文书
2014年妇幼卫生工作总结
2014/12/09 职场文书
电力工程合作意向书
2015/05/11 职场文书
员工离职证明范本
2015/06/12 职场文书
SQL Server 忘记密码以及重新添加新账号
2022/04/26 SQL Server