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
- Is the requested identity legal
- Is the request parameter tampered with
- Is the request unique (replay attack)
2, So how to solve these three problems??
- 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
- 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
- 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.
- 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 ] ] ], ];
- 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; } }
- 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"; } }
- 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; } }
- 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
- 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 } }
- 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); } }
- 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!!!