PHP如何实现后端中转层设计(接口转发,图片上传转发) # 前言 公司旧有业务平台分为好多个,每个都使用不同的语言,不同的数据库去实现。 对于公司内部员工,由于要记好多平台账号,每个平台账号又要分配不同权限,对于运维和使用者都是苦不感言。 结合以上情况,公司希望我这边带头负责一个项目搭建,集成各平台的业务实现到一个总的erp平台,并且回收各平台账号与权限控制,改由新erp平台统一分发。 也就是说实现该需求需要 1. 需要有一个中转层转发各业务平台的接口,并统一格式给前端。 2. 图片转发非ajax key-value形式,如何转发二进制流。 3. 回收了各业务平台的权限控制体系,那么需要在中转层设计一套能符合数据级粒度的权限控制体系。 4. 用户信息如何传递给各业务平台? 5. 和各业务平台如何保持安全性连接。 # 调研 上网查阅资料,一篇文章写道: > 应用中间层主要设计要考虑如下几个方面就好了。 (1)web应用层对外网开放,应用中间层对内网开放。web应用层和应用中间层之间通过内网交互。 (2)应用中间层直接数据库,web应用层不直连数据库。就是说web应用层和数据库之间,用中间层进行隔离了。说明下,这里的应用中间层,不是指我们平常用到的hibernat数据库中间件,那是两个不同的概念。这里说的应用中间件,是指的我们WEB架构上设计时,作的一种分层隔离设计,是一种逻辑层的分隔,不是物理层面的。 (3)应用中间层,我们可以采用RMI,Hessian.Burlap,Httpinvoker,Webservice发布一些接口出来。然后将相应用到的javabean和interface发布成jar包,然后提供给web应用层调用即可。 (4)为了防止应用中间层和web应用层之间在传输的过程中,数据会被篡改,双方采用签名(3DS,AES,SHA256,RSA都行),验签即可。 这是一篇java大神写的中间层设计。 结合我们目前的业务情况 1. 我们使用thinkphp5.1框架。 2. 中转层和前端使用jwt无状态token保持连接,token中包含用户信息。 3. 中转层使用钉钉集合auth数据级粒度权限控制实现权限控制。 4. 路由和token验证采用中间件检测。 经过大量讨论,基本已有实现思路。 # 实现 ## 如何和各业务平台保持安全连接并传递用户信息? 前端在登录时,会向中转层请求jwttoken,之后token中携带用户信息进行访问,用户信息不在是明文形式传递。 既然可以和前端使用此方式,那么各业务平台是否也可以使用相同的秘钥来解密此token获得用户信息? 答案是肯定的,只要中转层向各业务平台转发此前端携带的token,业务平台能拿到用户信息即代表此次请求安全。 **jwt生成参考我的这篇文章 http://chenls.me/blog/details.html?id=32** ## 如何实现接口转发? 接口转发可采用框架自动的miss路由实现,当访问路由符合一定规则,自动进入路由转发方法检测是否有对应的转发规则,如无,抛出异常。 ```php /** * 格式 * 前端访问路由 => to erp路由 * method 类型 * middleware 中间件 * upload 是否是上传文件类型 */ return [ //产品列表 '/erp/products/index' => [ 'to' => 'erp/products/index', 'method' => ['get'], 'middleware' => ['check', 'auth'], 'upload' => false, ], ]; ``` 路由注册只要在erp内的接口,进入控制器检测是否有对应的转发规则。 ```php Route::group('erp', function () { Route::rule('/', 'erp/Index/index'); Route::miss('erp/Repeat/repost'); })->allowCrossDomain(); ``` ```php protected static $url = "XXX"; protected $route; //处理中间件 protected $middleware = []; public function initialize() { $this->route = Config::pull('route'); //每一小时清除临时文件存储位置 if (!Cache::has('app_delfile_flag')) { $this->delFile(Env::get('runtime_path') . "http://static.chenls.me/chenls/uploads/"); Cache::set('app_delfile_flag', Carbon::now(), 3600); } $route = $this->route; $routekeys = array_keys($route); $baseUrl = strtolower(Request::baseUrl()); if (in_array($baseUrl, $routekeys)) { $route = $route[$baseUrl]; //处理中间件 $this->middleware = $route['middleware']; } } /** * 转发接口 */ public function repost() { //转发erp接口 $route = $this->route; $routekeys = array_keys($route); $routekeys = array_map('strtolower', $routekeys); $baseUrl = strtolower(Request::baseUrl()); if (!in_array($baseUrl, $routekeys)) { return ajaxReturn(-1, "未知接口"); } foreach ($route as $key => $value) { if (strtolower($key) == $baseUrl) { $route = $route[$key]; break; } } $methodArray = array_map('strtolower', $route['method']); if (!in_array(strtolower(Request::method()), $methodArray)) { return ajaxReturn(-1, '接口请求类型错误'); } $param = empty(Request::query()) ? "" : "?" . Request::query(); $url = self::$url . $route['to'] . $param; $token = Request::header('authorization'); $header = array(); $header[] = 'Authorization:' . $token; //上传文件 if ($route['upload']) { //普通接口转发 $postData = ""; if (strtolower(Request::method()) == 'post') { $postData = Request::post(); } //临时存储文件 $file = Request::file('file'); $save_path = Env::get('runtime_path') . "http://static.chenls.me/chenls/uploads/"; $result = $file->move($save_path); $infoPath = str_replace('\\', '/', $result->getSaveName()); $postData['mypath'] = $save_path . $infoPath; $reback = curl_file_request($url, $postData, $header); } else { //普通接口转发 $postData = ""; if (strtolower(Request::method()) == 'post') { $postData = Request::post(); } $reback = curl_request($url, $postData, $header); } $rebackArray = json_decode($reback, true); //如果接口报错,则原样返回报错信息 if (!$rebackArray) { return response($reback, 500, [], 'html'); } if ($rebackArray['code'] != 0) { $rebackArray['code'] = -1; } return $rebackArray; } /** * 递归临时删除文件夹下所有存储的文件 */ protected function deLFile($path) { if (is_dir($path)) { $p = scandir($path); foreach ($p as $val) { if ($val != "." && $val != "..") { if (is_dir($path . $val)) { $this->deLFile($path . $val . '/'); @rmdir($path . $val . '/'); } else { unlink($path . $val); } } } } } ``` ```php /** * 模拟curl请求 * 1:访问的URL,参数2:post数据(不填则为GET),参数3:请求头 */ function curl_request($url, $post = '', $header = "") { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)'); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($curl, CURLOPT_AUTOREFERER, 1); if ($post) { curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($post)); } if ($header) { curl_setopt($curl, CURLOPT_HTTPHEADER, $header); } curl_setopt($curl, CURLOPT_TIMEOUT, 10); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($curl); if (curl_errno($curl)) { return ajaxReturn(-1, '系统错误' . curl_error($curl)); } curl_close($curl); return $data; } ``` ## 如何实现图片/文件上传接口转发 中心思想是先保存文件,然后读取为二进制流转发。 具体可见我的下一篇文章 http://chenls.me/blog/details.html?id=33