API interface design

Keywords: PHP api Middleware

API interface design

First of all, the interface cannot run naked, otherwise you will BOOM!!!
First of all, the interface cannot run naked, otherwise you will BOOM!!!
First of all, the interface cannot run naked, otherwise you will BOOM!!!

1, Then the interface generally faces three security problems

  1. Is the requested identity legal
  2. Is the request parameter tampered with
  3. Is the request unique (replay attack)

2, So how to solve these three problems??

  1. The problem of legitimate request identity is solved by interface sign ature authentication. The api that needs to log in to operate also needs to verify the user's token
  2. For the problem of tampering with the request parameters, the keys of other parameters except sign are in ascending or descending order, then spell the api encryption key secretKey =, and then use an irreversible encryption algorithm, such as md5, to get the sign
  3. The only problem with the request is to define that the api must pass two parameters: ts (timestamp) and nonce (random unique code). The backend saves nonce as a key with redis to give an expiration time. As long as the request is repeated within the expiration time, it will be intercepted

In this way, three problems can be solved. This is the conventional interface authentication method!!!

3, Next is CODING TIME

First of all, I'd like to draw a convenient diagram here. The api responds to the application of components

composer require sevming/laravel-response:^1.0

When it comes to interface interception response msg, code and cache key s, these suggestions are stored by enum, or api generally has v1, v2... And other different versions, so it is necessary to make a good directory structure.

This is an enumeration class for storing api interception response information

<?php

namespace App\Http\Enums\Api\v1;
class ApiResponseEnum
{

    const DEFECT_SIGN = 'defect sign autograph|10001';

    const DEFECT_TIMESTAMP = 'defect ts time stamp|10002';

    const DEFECT_NONCE = 'defect nonce|10003';

    const INVALID_SIGN = 'illegal sign autograph|20001';

    const INVALID_TIMESTAMP = 'illegal ts time stamp|20002';

    const INVALID_NONCE = 'Illegal request|20003';

    const DEFECT_TOKEN = 'defect token|30001';

    const INVALID_TOKEN = 'illegal token|30002';

    const TWICE_PASSWORD_NOT_SAME = 'The two passwords are inconsistent|40001';

    const ACCOUNT_HAS_REGISTER = 'Account registered|40002';

    const INVALID_EMAIL_FORMAT = 'The mailbox format is incorrect|40003';

    const INVALID_PASSWORD_LENGTH = 'The password must have at least 8 digits|40004';

    const WEI_CODE_HAS_REGISTER = 'Wechat account registered|40005';

    const REGISTER_ERROR = 'login has failed|40006';

    const ACCOUNT_NOT_EXISTS = 'Account does not exist|40007';

    const ACCOUNT_HAS_BAN = 'Account has been blocked|40008';

    const INVALID_PASSWORD = 'Password error|40009';

}

There is also a cache key

<?php

namespace App\Http\Enums\Api\v1;
//api cache KEY enumeration class
class ApiCacheKeyEnum
{
    const NONCE_CACHE_KEY = 'api_request_nonce:';

    const TOKEN_CACHE_KEY = 'user_token:';
}

Design of api authentication

Design idea: first, make a unified input parameter detection for the interface input parameters in the base class of api, that is, configure the required parameters, set the default value, etc., so that there is no need to make cumbersome blank judgment on the parameters in the business layer. Then, middleware is used to intercept api authentication and token verification.

  1. First, create an api configuration file (api.php) and read the configuration in. env. Params here_ Check is used to configure interface input parameter detection. All configured parameters are required to be passed, and key is the interface method name (depending on the route, my general route will be consistent with the interface method name). Form validators are not used here because I think it is cumbersome to write a form for each interface method, so I changed this configuration method.
<?php

use App\Http\Controllers\Api\BaseApi;

return [
    'v1' => [
        'api_key' => env('API_KEY_V1'),//api sign encryption key
        'user_key' => env('USER_KEY_V1'),//User token encryption key,
        //Interface input parameter detection
        'params_check' => [
            '_register' => [
                'name' => [
                    'type' => BaseApi::PARAM_STRING,//Input parameter type
                    'default' => 'user' . uniqid()//Default value
                ],
                'email' => BaseApi::PARAM_STRING,
                'password' => BaseApi::PARAM_STRING,
                'confirm_password' => BaseApi::PARAM_STRING
            ],
            '_login' => [
                'email' => BaseApi::PARAM_STRING,
                'password' => BaseApi::PARAM_STRING
            ]
        ]
    ],
];
  1. Implementation of api base class (BaseApi)
<?php

namespace App\Http\Controllers\Api;

use App\Http\Enums\Api\v1\ApiCacheKeyEnum;
use Sevming\LaravelResponse\Support\Facades\Response;
use Illuminate\Support\Facades\Redis;

class BaseApi
{
    const PARAM_INT = 1;//integer
    const PARAM_STRING = 2;//character string
    const PARAM_ARRAY = 3;//array
    const PARAM_FILE = 4;//file

    protected $params;

    public function __construct()
    {
        //Input parameter detection and initialization
        $this->params = $this->check_params();
    }

    //api interface unified input parameter detection
    public function check_params()
    {
        $action_list = explode('/', \request()->path());
        $params_check_key = end($action_list);
        //Input parameter detection configuration
        $params_check = config('api.v1.params_check.' . $params_check_key);
        //Input parameter
        $params = request()->input();

        if (is_array($params_check) && $params_check) {
            $flag = true;
            foreach ($params_check as $key => $check) {
                if (is_array($check)) {
                    $type = $check['type'] ?? 2;//The default is string
                    $default = $check['default'] ?? '';//Default value
                } else {
                    $type = $check;
                }
                if (array_key_exists($key, $params)) {
                    switch ($type) {
                        case self::PARAM_INT:
                            $flag = is_numeric($params[$key]) || (isset($default) && empty($params[$key]));
                            break;
                        case self::PARAM_STRING:
                            $flag = is_string($params[$key]) || (isset($default) && empty($params[$key]));
                            break;
                        case self::PARAM_ARRAY:
                            $flag = is_array($params[$key]) || (isset($default) && empty($params[$key]));
                            break;
                        case self::PARAM_FILE:
                            $flag = $_FILES[$key] && isset($_FILES[$key]['error']) && $_FILES[$key]['error'] == 0;
                            break;
                    }
                } else {
                    $flag = false;
                }
                if (!$flag) {
                    return Response::fail('invalid param ' . $key);
                }
                //Default value processing
                if (empty($params[$key]) && isset($default)) {
                    $params[$key] = $default;
                }
                //File processing
                if ($type === BaseApi::PARAM_FILE) {
                    $params[$key] = $_FILES[$key];
                }
                unset($default);
            }
        }
        //Get uid according to token
        if (array_key_exists('token', $params)) {
            //Get uid
            $redis = Redis::connection();
            $uid = $redis->get(ApiCacheKeyEnum::TOKEN_CACHE_KEY . $params['token']);
            $params['uid'] = $uid ?? 0;
            unset($params['token']);
        }
        unset($params['sign']);
        return $params;
    }
}
  1. Some public functions used are put into common.php, which is a habit
<?php

//Common function

if (!function_exists('make_sign')) {
    //Generate signature
    function make_sign($params)
    {
        unset($params['sign']);
        $params['api_key'] = config('api.v1.api_key');//Splicing api encryption key
        ksort($params);//key ascending order
        $string_temp = http_build_query($params);
        return md5($string_temp);
    }
}

if (!function_exists('encrypt_token')) {
    //Generate token
    function encrypt_token($uid)
    {
        $user_info = [
            'uid' => $uid,
            'ts' => time()
        ];
        $user_key = config('api.v1.user_key');
        return openssl_encrypt(base64_encode(json_encode($user_info)), 'DES-ECB', $user_key, 0);
    }
}

if (!function_exists('make_avatar')) {
    function make_avatar($email)
    {
        $md5_email = md5($email);
        return "https://api.multiavatar.com/{$md5_email}.png";
    }
}
  1. The Api service class implements the signature authentication and token verification methods of the interface
<?php

namespace App\Http\Contracts\Api\v1;
interface ApiInterface
{
    //api signature authentication
    public function checkSign($params);

    //User token verification
    public function checkToken($params);
}
<?php

namespace App\Http\Services\Api\v1;

use App\Http\Contracts\Api\v1\ApiInterface;
use App\Http\Enums\Api\v1\ApiCacheKeyEnum;
use App\Http\Enums\Api\v1\ApiResponseEnum;
use Illuminate\Support\Facades\Redis;
use Sevming\LaravelResponse\Support\Facades\Response;

class ApiService implements ApiInterface
{
    public static $instance = null;

    /**
     * @return static|null
     * Singleton mode
     */
    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new static();
        }
        return self::$instance;
    }

    /**
     * @param $params array Input parameter
     * Signature authentication
     */
    public function checkSign($params)
    {
        // TODO: Implement checkSign() method.
        if (!isset($params['sign'])) {
            return Response::fail(ApiResponseEnum::DEFECT_SIGN);
        }
        if (!isset($params['ts'])) {
            return Response::fail(ApiResponseEnum::DEFECT_TIMESTAMP);
        }
        if (!isset($params['nonce'])) {
            return Response::fail(ApiResponseEnum::DEFECT_NONCE);
        }

        $ts = $params['ts'];//time stamp
        $nonce = $params['nonce'];
        $sign = $params['sign'];
        $time = time();
        if ($ts > $time) {
            return Response::fail(ApiResponseEnum::INVALID_TIMESTAMP);
        }

        $redis = Redis::connection();
        if ($redis->exists(ApiCacheKeyEnum::NONCE_CACHE_KEY . $nonce)) {
            return Response::fail(ApiResponseEnum::INVALID_NONCE);
        }
        $api_sign = make_sign($params);
        if ($api_sign !== $sign) {
            return Response::fail(ApiResponseEnum::INVALID_SIGN);
        }

        //A sign cannot repeat requests within 5 minutes to prevent replay attacks
        $redis->setex(ApiCacheKeyEnum::NONCE_CACHE_KEY . $nonce, 300, $time);

        return true;
    }

    /**
     * @param $params
     * TOKEN check
     */
    public function checkToken($params)
    {
        // TODO: Implement checkToken() method.

        $action_list = explode('/', \request()->path());
        $action = end($action_list);
        //The underlined method needs no login and can be released directly
        if (stripos($action, '_')) {
            return true;
        }

        if (!isset($params['token'])) {
            return Response::fail(ApiResponseEnum::DEFECT_TOKEN);
        }

        $token = $params['token'];

        //Check whether the login user token exists in the cache
        $redis = Redis::connection();

        $cache_token = $redis->get(ApiCacheKeyEnum::TOKEN_CACHE_KEY . $token);

        if (!$cache_token) {
            return Response::fail(ApiResponseEnum::INVALID_TOKEN);
        }

        return true;
    }
}
  1. Middleware for api authentication interception
<?php

namespace App\Http\Middleware;

use App\Http\Services\Api\v1\ApiService;
use Closure;

class ApiIntercept
{
    public function handle($request, Closure $next)
    {
        $params = $request->input();
        $env = config('env');
        if ($env !== 'local') {
            //Non local environment, signature authentication is required
            ApiService::getInstance()->checkSign($params);
        }
        //token test
        ApiService::getInstance()->checkToken($params);

        return $next($request);
    }
}


4, Let's take a simple login as an example

  1. User model class
<?php
/**
 * User: yanjianfei
 * Date: 2021/9/18
 * Time: 10:17
 */

namespace App\Model;

use App\Http\Enums\Api\v1\ApiCacheKeyEnum;
use App\Http\Enums\Api\v1\ApiResponseEnum;
use Illuminate\Support\Facades\Redis;
use Sevming\LaravelResponse\Support\Facades\Response;

class User extends BaseModel
{
    //register
    public function checkRegister($params)
    {
        if ($params['password'] !== $params['confirm_password']) {
            return Response::fail(ApiResponseEnum::TWICE_PASSWORD_NOT_SAME);
        }
        if (strlen($params['password']) < 8) {
            return Response::fail(ApiResponseEnum::INVALID_PASSWORD_LENGTH);
        }
        $pattern = '^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$';
        if (preg_match($pattern, $params['email'])) {
            return Response::fail(ApiResponseEnum::INVALID_EMAIL_FORMAT);
        }
        $account_exits = self::query()->where('email', $params['email'])->exists();
        if ($account_exits) {
            return Response::fail(ApiResponseEnum::ACCOUNT_HAS_REGISTER);
        }

        $wei_code_exists = self::query()->where('wei_code', $params['wei_code'])->exists();

        if ($wei_code_exists) {
            return Response::fail(ApiResponseEnum::WEI_CODE_HAS_REGISTER);
        }

        $data = [
            'name' => $params['name'],
            'password' => md5($params['password']),
            'avatar' => make_avatar($params['email']),
            'email' => $params['email']
        ];

        $user = self::query()->create($data);

        if (!$user) {
            return Response::fail();
        }
        //Automatic login after registration
        return $this->checkLogin($user, true);
    }

    /**
     * @param $params
     * @param false $auto automatic logon
     */
    public function checkLogin($params, $auto = false)
    {
        $user = $params;
        if (!$auto) {
            $user = self::query()->where('email', $params['email'])->first();
            if (!$user) {
                return Response::fail(ApiResponseEnum::ACCOUNT_NOT_EXISTS);
            }
            if ($user['status'] == 0) {
                return Response::fail(ApiResponseEnum::ACCOUNT_HAS_BAN);
            }

            if ($user['password'] !== md5($params['password'])) {
                return Response::fail(ApiResponseEnum::INVALID_PASSWORD);
            }
        }

        $token = encrypt_token($user['id']);//Generate token
        $redis = Redis::connection();
        $redis->setex(ApiCacheKeyEnum::TOKEN_CACHE_KEY . $token, 86400, $user['id']);//reids store token s

        return [
            'token' => $token,
            'name' => $user['name'],
            'avatar' => $user['avatar']
        ];//Return login information
    }

}
  1. User controller
<?php
/**
 * User: yanjianfei
 * Date: 2021/9/17
 * Time: 17:01
 */

namespace App\Http\Controllers\Api\v1;

use App\Http\Controllers\Api\BaseApi;
use Sevming\LaravelResponse\Support\Facades\Response;
use App\Model\User as UserModel;

class User extends BaseApi
{
    public function _login(UserModel $user)
    {
        $data = $user->checkLogin($this->params);
        return Response::success($data);
    }

    public function _register(UserModel $user)
    {
        $data = $user->checkRegister($this->params);
        return Response::success($data);
    }
}
  1. Configure routing
<?php

//User routing
Route::group([
    'prefix' => 'user',
    'namespace' => 'Api\v1',
    'middleware' => 'api.intercept'//api authentication interception Middleware
], function ($router) {
    $router->post('_login', 'User@_login');
    $router->post('_register', 'User@_register');
});

Here, the signature authentication of api has been designed and developed!!! Thanks for watching!!!

Posted by rivasivan on Sat, 18 Sep 2021 08:03:26 -0700