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实现文件安全下载
Oct 09 PHP
新版PHP极大的增强功能和性能
Oct 09 PHP
用header 发送cookie的php代码
Mar 16 PHP
php模板之Phpbean的目录结构
Jan 10 PHP
PHP 数组遍历方法大全(foreach,list,each)
Jun 30 PHP
php cookie的操作实现代码(登录)
Dec 29 PHP
PHP模块 Memcached功能多于Memcache
Jun 14 PHP
PHP字符串比较函数strcmp()和strcasecmp()使用总结
Nov 19 PHP
PHP、Python和Javascript的装饰器模式对比
Feb 03 PHP
PHP中通过trigger_error触发PHP错误示例
Jun 23 PHP
php-redis中的sort排序函数总结
Jul 08 PHP
php gd等比例缩放压缩图片函数
Jun 12 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
星际原理概述
2020/03/04 星际争霸
浅谈PHP语法(1)
2006/10/09 PHP
如何提高MYSQL数据库的查询统计速度 select 索引应用
2007/04/11 PHP
如何使用PHP计算上一个月的今天
2013/05/23 PHP
ThinkPHP进程计数类Process用法实例详解
2015/09/25 PHP
YII CLinkPager分页类扩展增加显示共多少页
2016/01/29 PHP
如何解决PHP使用mysql_query查询超大结果集超内存问题
2016/03/14 PHP
PHP操作MySQL中BLOB字段的方法示例【存储文本与图片】
2017/09/15 PHP
用函数式编程技术编写优美的 JavaScript
2006/11/25 Javascript
Jquery 组合form元素为json格式,asp.net反序列化
2009/07/09 Javascript
js 禁止选择功能实现代码(兼容IE/Firefox)
2010/04/23 Javascript
JQuery动态给table添加、删除行 改进版
2011/01/19 Javascript
清除div下面的所有标签的方法
2014/02/17 Javascript
jquery左边浮动到一定位置时显示返回顶部按钮
2014/06/05 Javascript
JavaScript检测字符串中是否含有html标签实现方法
2015/07/01 Javascript
jquery插件格式实例分析
2016/06/16 Javascript
Jquery Easyui选项卡组件Tab使用详解(10)
2016/12/18 Javascript
vue-cli实现多页面多路由的示例代码
2018/01/30 Javascript
Nodejs对postgresql基本操作的封装方法
2019/02/20 NodeJs
小程序Request的另类用法详解
2019/08/09 Javascript
Python做文本按行去重的实现方法
2016/10/19 Python
Python基于多线程实现抓取数据存入数据库的方法
2018/06/22 Python
kaggle+mnist实现手写字体识别
2018/07/26 Python
python twilio模块实现发送手机短信功能
2019/08/02 Python
python seaborn heatmap可视化相关性矩阵实例
2020/06/03 Python
纯CSS实现的大小渐变、渐远效果
2014/04/15 HTML / CSS
HTML5注册表单的自动聚焦与占位文本示例代码
2013/07/19 HTML / CSS
Java中的类包括什么内容?设计时要注意哪些方面
2012/05/23 面试题
优秀体育委员自荐书
2014/01/31 职场文书
校园广播稿500字
2014/02/04 职场文书
大学毕业晚会开场白
2015/05/29 职场文书
学校运动会感想
2015/08/10 职场文书
幼儿园教师培训心得体会
2016/01/21 职场文书
2016年推广普通话宣传周活动总结
2016/04/06 职场文书
vue实现书本翻页动画效果实例详解
2022/04/08 Vue.js
springboot集成redis存对象乱码的问题及解决
2022/06/16 Java/Android