이 글은 주로 yii2 프로젝트의 실제 Restful API 승인 검증에 대한 관련 정보를 소개합니다. 글의 소개는 매우 자세하며 필요한 모든 사람을 위한 특정 참고 자료와 학습 가치가 있습니다. 그것이 모두에게 도움이 되기를 바랍니다.
머리말
이 글은 주로 실제 시나리오에서의 API 배포를 위해 작성되었습니다.
오늘은 그 당시 API에서 겪었던 인증 확인 문제에 대해 이야기하겠습니다!
비즈니스 분석
먼저 전체 논리를 이해합시다
사용자가 클라이언트에서 로그인 양식을 작성합니다
사용자가 양식을 제출하고 클라이언트가 로그인 인터페이스 로그인을 요청합니다
서버 측 확인 사용자의 계정 비밀번호를 확인하고 유효한 토큰을 클라이언트에 반환합니다.
클라이언트는 사용자의 토큰을 받아 쿠키와 같은 클라이언트에 저장합니다.
클라이언트는 토큰에 액세스하기 위해 토큰을 운반합니다. 사용자 개인정보 획득 인터페이스와 같은 확인이 필요한 인터페이스
서버는 토큰의 유효성을 확인합니다. 어쨌든 확인에 실패하면 사용자에게 필요한 정보가 반환됩니다. 다시 로그인하려면
이 글에서는 사용자로 로그인하여 정보를 얻습니다. 사용자의 개인 정보는 상세하고 완전한 설명을 위해 예시로 사용되었습니다.
위 내용이 이 글의 핵심입니다. 아직 너무 흥분하거나 긴장하지 마시고, 분석 후 차근차근 세부적으로 진행하겠습니다.
준비
API 애플리케이션이 있어야 합니다
클라이언트에서는 시뮬레이션을 위해 Postman을 사용할 예정입니다. Google 브라우저에 Postman이 설치되어 있지 않은 경우 먼저 다운로드하세요
테스트할 사용자 테이블에는 api_token 필드가 있어야 합니다. 그렇지 않은 경우 먼저 직접 추가하고 필드가 충분히 긴지 확인하세요.
API 애플리케이션에서 라우팅 미화를 활성화하고 먼저 게시물 유형 로그인을 구성했습니다. 연산 및 get 유형 signup-test 연산
사용자 컴포넌트의 세션 세션을 닫습니다
위 준비 사항 중 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', ] ], ] ], // ...... ],
가입 -테스트 작업은 나중에 로그인 작업을 용이하게 하기 위해 테스트 사용자에게 추가됩니다. 다른 유형의 작업은 나중에 추가해야 합니다.
인증 클래스 선택
apimodulesv1controllersUserController
에 설정한 모델 클래스는 commonmodelsUser
클래스를 가리킵니다. 여기에서 별도로 꺼내지 마세요. 필요에 따라 다시 작성하세요. 필요한 경우 별도의 User 클래스를 apimodels
에 복사하세요. apimodulesv1controllersUserController
中设定的model类指向 commonmodelsUser
类,为了说明重点这里我们就不单独拿出来重写了,看各位需要,有必要的话再单独copy一个User类到apimodels
下。
校验用户权限我们以 yiifiltersauthQueryParamAuth
为例
use yii\filters\auth\QueryParamAuth; public function behaviors() { return ArrayHelper::merge (parent::behaviors(), [ 'authenticator' => [ 'class' => QueryParamAuth::className() ] ] ); }
如此一来,那岂不是所有访问user的操作都需要认证了?那不行,客户端第一个访问login操作的时候哪来的token,yiifiltersauthQueryParamAuth
对外提供一个属性,用于过滤不需要验证的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操作其实很简单,大部分的业务逻辑处理都在apimodelsloginForm
上,来先看看login的实现
use apimodelsLoginForm;
/** * 登录 */ 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,再来看看登录的具体逻辑的实现
新建apimodelsLoginForm.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方法,通过commonmodelsUser
类的findByUsername获取用户,找不到或者commonmodelsUser
yiifiltersauthQueryParamAuth
를 예로 듭니다🎜/** * 生成 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(); }
yiifiltersauthQueryParamAuth
는 확인이 필요하지 않은 작업을 필터링하기 위한 외부 속성을 제공합니다. 권한 확인 없이 로그인 작업에 액세스할 수 있도록 UserController 🎜🎜<?php return [ // ... // token 有效期默认1天 'user.apiTokenExpire' => 1*24*3600, ];
public function behaviors() { return ArrayHelper::merge (parent::behaviors(), [ 'authenticator' => [ 'class' => QueryParamAuth::className(), 'tokenParam' => 'token', 'optional' => [ 'login', 'signup-test' ], ] ] ); }
apimodelsloginForm
에 있습니다. 먼저 로그인 구현을 살펴보겠습니다.🎜🎜use apimodelsLoginForm;🎜'extraPatterns' => [ 'POST login' => 'login', 'GET signup-test' => 'signup-test', 'GET user-profile' => 'user-profile', ]
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.'); }
commonmodelsUser
클래스의 findByUsername을 통해 LoginForm의 getUser 메소드를 호출합니다. 사용자를 찾을 수 없거나 commonmodelsUser
의 verifyPassword가 비밀번호를 확인하는 데 실패하면 오류가 반환되었습니다🎜 5、触发LoginForm::GENERATE_API_TOKEN事件,调用LoginForm的onGenerateApiToken方法,通过common\models\User
的apiTokenIsValid校验token的有效性,如果无效,则调用User的generateApiToken方法重新生成
注意:common\models\User
类必须是用户的认证类,如果不知道如何创建完善该类,请围观这里 用户管理之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.<br/>
这是怎么回事呢?
我们找到 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授权的就好。这样真正实际用到的时候,你也能举一反三!
相关推荐:
위 내용은 yii2의 Restful API 인증 검증에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!