API接口设计
首先接口是不能裸奔的,不然你就BOOM了!!!
首先接口是不能裸奔的,不然你就BOOM了!!!
首先接口是不能裸奔的,不然你就BOOM了!!!
一、那么接口一般面临三个安全问题
- 请求身份是否合法
- 请求参数是否被篡改
- 请求是否唯一(重放攻击)
二、那么针对这三个问题,怎么解决呢??
- 请求身份合法问题就用接口签名认证(sign)解决,需要登录才能操作的api还要验证用户的token
- 请求参数篡改的问题就对入参除sign外的其他参数的key升序或者降序,再拼上api的加密密钥secretKey=,然后用一个不可逆的加密算法,例如md5,这样就能得出sign
- 请求的唯一问题就定义api必须传ts(时间戳)和nonce(随机唯一code)这两个参数,后端将nonce作为key用redis存起来,给一个过期时间,只要是在过期内重复请求就拦截
这样下来,三个问题就能解决了,这是常规的接口认证方式!!!
三、接下来就是CODING TIME
首先我这里图个方便,api响应用了组件
1
| composer require sevming/laravel-response:^1.0
|
涉及到接口拦截响应msg,code还有用到得缓存key这些建议都用枚举(enum)存放,还有api一般都有v1、v2…等不同版本,所以要做好目录结构。
这是存放api拦截响应信息的枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php
namespace App\Http\Enums\Api\v1; class ApiResponseEnum {
const DEFECT_SIGN = '缺失sign签名|10001';
const DEFECT_TIMESTAMP = '缺失ts时间戳|10002';
const DEFECT_NONCE = '缺失nonce|10003';
const INVALID_SIGN = '非法sign签名|20001';
const INVALID_TIMESTAMP = '非法ts时间戳|20002';
const INVALID_NONCE = '非法请求|20003';
const DEFECT_TOKEN = '缺失token|30001';
const INVALID_TOKEN = '非法token|30002';
const TWICE_PASSWORD_NOT_SAME = '两次密码不一致|40001';
const ACCOUNT_HAS_REGISTER = '账号已注册|40002';
const INVALID_EMAIL_FORMAT = '邮箱格式不对|40003';
const INVALID_PASSWORD_LENGTH = '密码至少8位|40004';
const WEI_CODE_HAS_REGISTER = '微聊号已注册|40005';
const REGISTER_ERROR = '注册失败|40006';
const ACCOUNT_NOT_EXISTS = '账号不存在|40007';
const ACCOUNT_HAS_BAN = '账号已被封禁|40008';
const INVALID_PASSWORD = '密码错误|40009';
}
|
还有一个存放缓存key的
1 2 3 4 5 6 7 8 9 10 11
| <?php
namespace App\Http\Enums\Api\v1;
class ApiCacheKeyEnum { const NONCE_CACHE_KEY = 'api_request_nonce:';
const TOKEN_CACHE_KEY = 'user_token:'; }
|
关于api认证的设计
设计思想:首先在api的基类中统一对接口入参做一个入参检测,也就是配置必传参数、设置默认值等,这样就不用在业务层中对参数做繁琐的判空处理。然后api认证及token校验的拦截用中间件去做。
- 首先建一个api的配置文件(api.php),读.env里的配置,这里的
params_check
就是配置接口入参检测的,凡是配置的参数都是必传的,key是接口方法名(取决于路由,本人一般路由与接口方法名会保持一致)。这里不用表单验证器是因为本人觉得每个接口方法都要写一个表单验证实在繁琐,所以改成了这种配置的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
use App\Http\Controllers\Api\BaseApi;
return [ 'v1' => [ 'api_key' => env('API_KEY_V1'), 'user_key' => env('USER_KEY_V1'), 'params_check' => [ '_register' => [ 'name' => [ 'type' => BaseApi::PARAM_STRING, 'default' => 'user' . uniqid() ], 'email' => BaseApi::PARAM_STRING, 'password' => BaseApi::PARAM_STRING, 'confirm_password' => BaseApi::PARAM_STRING ], '_login' => [ 'email' => BaseApi::PARAM_STRING, 'password' => BaseApi::PARAM_STRING ] ] ], ];
|
- api基类的实现(BaseApi)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <?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; const PARAM_STRING = 2; const PARAM_ARRAY = 3; const PARAM_FILE = 4;
protected $params;
public function __construct() { $this->params = $this->check_params(); }
public function check_params() { $action_list = explode('/', \request()->path()); $params_check_key = end($action_list); $params_check = config('api.v1.params_check.' . $params_check_key); $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; $default = $check['default'] ?? ''; } 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); } if (empty($params[$key]) && isset($default)) { $params[$key] = $default; } if ($type === BaseApi::PARAM_FILE) { $params[$key] = $_FILES[$key]; } unset($default); } } if (array_key_exists('token', $params)) { $redis = Redis::connection(); $uid = $redis->get(ApiCacheKeyEnum::TOKEN_CACHE_KEY . $params['token']); $params['uid'] = $uid ?? 0; unset($params['token']); } unset($params['sign']); return $params; } }
|
- 用到的一些公共函数放到common.php中,这个看习惯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?php
if (!function_exists('make_sign')) { function make_sign($params) { unset($params['sign']); $params['api_key'] = config('api.v1.api_key'); ksort($params); $string_temp = http_build_query($params); return md5($string_temp); } }
if (!function_exists('encrypt_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"; } }
|
- Api服务类实现接口的签名认证和token校验方法
1 2 3 4 5 6 7 8 9 10 11
| <?php
namespace App\Http\Contracts\Api\v1; interface ApiInterface { public function checkSign($params);
public function checkToken($params); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| <?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;
public static function getInstance() { if (is_null(self::$instance)) { self::$instance = new static(); } return self::$instance; }
public function checkSign($params) { 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']; $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); }
$redis->setex(ApiCacheKeyEnum::NONCE_CACHE_KEY . $nonce, 300, $time);
return true; }
public function checkToken($params) {
$action_list = explode('/', \request()->path()); $action = end($action_list); if (stripos($action, '_')) { return true; }
if (!isset($params['token'])) { return Response::fail(ApiResponseEnum::DEFECT_TOKEN); }
$token = $params['token'];
$redis = Redis::connection();
$cache_token = $redis->get(ApiCacheKeyEnum::TOKEN_CACHE_KEY . $token);
if (!$cache_token) { return Response::fail(ApiResponseEnum::INVALID_TOKEN); }
return true; } }
|
- api认证拦截的中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?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') { ApiService::getInstance()->checkSign($params); } ApiService::getInstance()->checkToken($params);
return $next($request); } }
|
四、下面以简单的登录注册为例子
- User模型类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| <?php
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 { 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(); } return $this->checkLogin($user, true); }
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']); $redis = Redis::connection(); $redis->setex(ApiCacheKeyEnum::TOKEN_CACHE_KEY . $token, 86400, $user['id']);
return [ 'token' => $token, 'name' => $user['name'], 'avatar' => $user['avatar'] ]; }
}
````
2. User控制器
```php <?php
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); } }
|
- 配置路由
<?php
//用户路由
Route::group([
'prefix' => 'user',
'namespace' => 'Api\v1',
'middleware' => 'api.intercept'//api认证拦截中间件
], function ($router) {
$router->post('_login', 'User@_login');
$router->post('_register', 'User@_register');
});
到这里api的签名认证就已经设计开发好了!!!感谢观看!!!