Preface
What is a restful style API? We've written it beforeLarge articleTo introduce its concepts and basic operations.
Since I have written it, do you want to say something today?
This article is mainly written for the deployment of APIs in actual scenarios.
Today, let’s talk about the authorization verification problems encountered by APIs in those years! Exclusive work, if you benefit from reading it, remember not to forget to like me.
Business Analysis
Let's first understand the whole logic
- User fills in the login form on the client
- User submits the form, and the client requests login to the interface login
- The server checks the user's account password and returns a valid token to the client
- The client gets the user's token and stores it in the client, such as a cookie.
- The client carries token to access the interface that needs verification, such as the interface to obtain user personal information.
- The server side checks the validity of the token and passes the verification. Anyway, the information required by the client is returned. The verification fails and the user needs to log in again.
In this article, we use the user to log in and obtain the user's personal information as an example to provide a detailed and complete version description.
The above are the key points to achieve in this article. Don’t be excited or nervous. After analyzing it, let’s follow the details one by one.
Preparation
- You should have an API application. If you don't have one, please move here first →_→Restful API basics
- For the client, we are planning to use postman for simulation. If your Google browser has not installed postman yet, please download it yourself first.
- The user table to be tested needs to have an api_token field. If not, please add it yourself first and ensure that the field is sufficiently long.
- The API application has enabled routing beautification, and first configures the post-type login operation and the get-type signup-test operation.
- Closed the session session of the user component
Regarding the above preparation points 4 and 5, we will post the code to make it easier to understand.
'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', ] ], ] ], // ...... ],
We will add test users to the signup-test operation to facilitate login operation. Other types of operations need to be added later.
Selection of certification
We're inapi\modules\v1\controllers\UserController
The model class set incommon\models\User
In order to explain the key points, we will not rewrite it separately. Depending on your needs, if necessary, copy a User class separately toapi\models
Down.
Verify user permissionsyii\filters\auth\QueryParamAuth
As an example
use yii\filters\auth\QueryParamAuth; public function behaviors() { return ArrayHelper::merge (parent::behaviors(), [ 'authenticator' => [ 'class' => QueryParamAuth::className() ] ] ); }
In this way, won’t all accessing user operations require authentication? That won't work. Where did the token come from when the client first accesses the login operation?yii\filters\auth\QueryParamAuth
Provide an external attribute to filter actions that do not require verification. We slightly modify the behaviors method of UserController
public function behaviors() { return ArrayHelper::merge (parent::behaviors(), [ 'authenticator' => [ 'class' => QueryParamAuth::className(), 'optional' => [ 'login', 'signup-test' ], ] ] ); }
In this way, the login operation can be accessed without permission verification.
Add test user
In order to avoid failure of client login, we first write a simple method to insert two pieces of data into the user table to facilitate the subsequent verification.
UserController adds a signupTest operation. Note that this method does not fall within the scope of explanation, and we are only used for convenient testing.
use common\models\User; /** * Add test user */ public function actionSignupTest () { $user = new User(); $user->generateAuthKey(); $user->setPassword('123456'); $user->username = '111'; $user->email = '111@'; $user->save(false); return [ 'code' => 0 ]; }
As above, we added a user whose username is 111 and password is 123456
Login operation
Assuming that the user enters the username and password to log in on the client, the server login operation is actually very simple, and most of the business logic processing isapi\models\loginForm
Let's take a look at the implementation of login
use api\models\LoginForm;
/** * Log in */ 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; } }
After logging in successfully, the user's token is returned to the client, and then let's take a look at the specific logic of login.
Create a new api\models\
<?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 for verifying client form data */ public function rules() { return [ [['username', 'password'], 'required'], ['password', 'validatePassword'], ]; } /** * Custom password authentication method */ public function validatePassword($attribute, $params) { if (!$this->hasErrors()) { $this->_user = $this->getUser(); if (!$this->_user || !$this->_user->validatePassword($this->password)) { $this->addError($attribute, 'Incorrect username or password.'); } } } /** * @inheritdoc */ public function attributeLabels() { return [ 'username' => 'username', 'password' => '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; } } /** * Obtain user authentication information based on username * * @return User|null */ protected function getUser() { if ($this->_user === null) { $this->_user = User::findByUsername($this->username); } return $this->_user; } /** * After the login verification is successful, a new token is generated for the user * If the token fails, regenerate the token */ public function onGenerateApiToken () { if (!User::apiTokenIsValid($this->_user->api_token)) { $this->_user->generateApiToken(); $this->_user->save(false); } } }
Let's look back at what happened after we called the login operation of LoginForm in the login operation of UserController
1. Call the login method of LoginForm
2. Call the validate method and then verify the rules
3. Call the validatePassword method in rules verification to verify the user name and password
4. During the validatePassword method verification process, call the getUser method of LoginForm, and passcommon\models\User
The class findByUsername gets the user, cannot be found orcommon\models\User
The validatePassword fails to verify the password and returns error
5. Trigger the LoginForm::GENERATE_API_TOKEN event, call the onGenerateApiToken method of LoginForm, and passcommon\models\User
apiTokenIsValid checks the validity of token. If it is invalid, call User's generateApiToken method to regenerate
Notice:common\models\User
The class must be the user's authentication class. If you don't know how to create and improve this class, please watch the configuration of the user component of user management
The following adds to this sectioncommon\models\User
Related methods
/** * Generate api_token */ public function generateApiToken() { $this->api_token = Yii::$app->security->generateRandomString() . '_' . time(); } /** * Check whether api_token is valid */ public static function apiTokenIsValid($token) { if (empty($token)) { return false; } $timestamp = (int) substr($token, strrpos($token, '_') + 1); $expire = Yii::$app->params['']; return $timestamp + $expire >= time(); }
Continue to supplement the validity period of tokens involved in apiTokenIsValid method, inapi\config\
Add it in the file
<?php return [ // ... // token validity period is 1 day by default '' => 1*24*3600, ];
At this point, the client logs in and the server returns token to the client and completes.
According to the analysis at the beginning of the article, the client should save the obtained token locally, such as in a cookie. In the future, the interface that requires token verification is accessed, and you can read from local, such as reading from cookies and access the interface.
Requesting user authentication operations based on token
Suppose we have saved the obtained token, and we will take the interface to access user information as an example.
yii\filters\auth\QueryParamAuth
The token parameter recognized by the class is access-token, which we can modify in the behavior.
public function behaviors() { return ArrayHelper::merge (parent::behaviors(), [ 'authenticator' => [ 'class' => QueryParamAuth::className(), 'tokenParam' => 'token', 'optional' => [ 'login', 'signup-test' ], ] ] ); }
Here, the default access-token is changed to token.
We add userProfile operation to the urlManager component of the configuration file
'extraPatterns' => [ 'POST login' => 'login', 'GET signup-test' => 'signup-test', 'GET user-profile' => 'user-profile', ]
We use postman to simulate request to access/v1/users/user-profile?token=apeuT9dAgH072qbfrtihfzL6qDe_l4qz_1479626145 Found that an exception was thrown
\"findIdentityByAccessToken\" is not implemented.
What's going on?
We foundyii\filters\auth\QueryParamAuth authenticate
Method, found here is calledcommon\models\User
The loginByAccessToken method of the class, some students were puzzled.common\models\User
The class does not implement the loginByAccessToken method. Why does it say that the findIdentityByAccessToken method is not implemented? If you still remembercommon\models\User
The class has been implementedyii\web\user
If you have an interface of the class, you should open ityii\web\User
Find answers in categories. That's right, the loginByAccessToken method isyii\web\User
Implemented, this class is calledcommon\models\User
findIdentityByAccessToken, but we see that an exception is thrown by throw in this method, which means that we have to implement this method manually!
This is easy to deal with, let's do itcommon\models\User
ClassicfindIdentityByAccessToken
Method
public static function findIdentityByAccessToken($token, $type = null) { // If the token is invalid, 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.'); }
After verifying the validity of the token, we will start to implement the main business logic parts.
/** * Get user information */ public function actionUserProfile ($token) { // At this point, tokens think it is valid // Just implement business logic below, just use it as a case, for example, you may need to associate other tables to obtain user information, etc. $user = User::findIdentityByAccessToken($token); return [ 'id' => $user->id, 'username' => $user->username, 'email' => $user->email, ]; }
Data type definition returned by the server
In postman, what data type can we output the interface data. However, some people have found that when we copy the address requested by postman to the browser address bar, the returned xml format is also in the form of the userProfile operation, and we obviously return the belonging group in the UserProfile operation. What's going on?
This is actually the official trick. We followed the source code layer by layer and found thatyii\rest\Controller
In the class, there is a contentNegotiator behavior, which specifies that the data formats allowed to be returned are json and xml. The final data format returned shall be based on the first occurrence of formats contained in the Accept in the request header. You canyii\filters\ContentNegotiator
ofnegotiateContentType
Find the answer in the method.
You can see it in the browser's request header
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
That is, application/xml appears in formats first, so the returned data format is xml type. If the data format obtained by the client wants to be parsed according to json, you only need to set the Accept value of the request header equals application/json.
Some students may say that this is too troublesome. In what era, who still uses xml? I want to output json format data on the server. How to do it?
The solution is to solve the problem, let’s see how to do it. Add the configuration of response to the api\config\ file
'response' => [ 'class' => 'yii\web\Response', 'on beforeSend' => function ($event) { $response = $event->sender; $response->format = yii\web\Response::FORMAT_JSON; }, ],
In this way, no matter what your client transmits, the server will eventually output data in json format.
Custom error handling mechanism
Let’s look at another common question:
Look at the above methods, the results returned are of various types, which adds trouble to client parsing. Moreover, once an exception is thrown, the returned codes are still piled up. What should I do?
Before talking about this issue, let’s talk about the exception handling classes that are first closed in Yii. Of course, there are many. For example, some common ones below, dig into others yourself
yii\web\BadRequestHttpException yii\web\ForbiddenHttpException yii\web\NotFoundHttpException yii\web\ServerErrorHttpException yii\web\UnauthorizedHttpException yii\web\TooManyRequestsHttpException
In actual development, you should be good at using these classes to catch exceptions and throw exceptions. Let's talk about it far, let's go back to the point, how to customize interface exception response or customize unified data formats, such as to the following configuration, unified response client format standards.
'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; }, ],
After saying so much, this article is about to end. Students who are just starting to come into contact with may have some confusion. Don’t be confused, digest it slowly. First, know this meaning and understand how the restful API interface is authorized by token throughout the entire process. When this is really used, you can learn from it!
Summarize
The above is the entire content of this article. I hope the content of this article will be of some help to your study or work. If you have any questions, you can leave a message to communicate. Thank you for your support.