**/ namespace app\api\middleware; use think\facade\Config; use think\exception\HttpResponseException; use think\Response; class Allow { public function handle($request, \Closure $next) { // 处理跨域 header('Access-Control-Allow-Origin: *'); header('Access-Control-Max-Age: 1800'); header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE'); header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With,access-token,token,adid,clickid,client'); // 第一次预请求不管 if (strtoupper($request->method()) == "OPTIONS") { exit; } // 对GET请求进行签名验证 if (strtoupper($request->method()) == "GET") { $this->verifySignature($request); } // 处理跨域 // 后置中间件 $response = $next($request); return $response; } /** * 验证请求签名 * @param \think\Request $request * @return void */ protected function verifySignature($request) { // 获取所有GET参数 $params = $request->get(); // 获取当前请求路径 $path = $request->pathinfo(); // 白名单路径检查 - 不需要验证域名的路径 $whitelistPaths = $this->getWhitelistPaths(); foreach ($whitelistPaths as $whitePath) { // 支持简单的路径匹配,如 'notify/*' 匹配所有通知路径 if ($this->pathMatch($whitePath, $path)) { // 白名单路径,跳过域名检查 // \think\facade\Log::info('白名单路径访问: ' . $path . ', 域名: ' . $); return; } } // 如果请求中携带is_test=true参数,则跳过签名验证 if (isset($params['is_test']) && $params['is_test'] === 'true') { return; } // 检查是否有必要的签名参数 if (!isset($params['timestamp']) || !isset($params['sign'])) { $this->error('缺少必要的签名参数'); } // 检查请求时间戳是否在合理范围内(例如5分钟内) if (time() - intval($params['timestamp']) > 300) { $this->error('请求已过期'); } // 从请求中获取签名 $requestSign = $params['sign']; //移除url unset($params['s']); // 从参数中移除签名,用于生成本地签名 unset($params['sign']); // 按照键名对参数进行排序 ksort($params); // 组合参数为字符串 $signStr = ''; foreach ($params as $key => $value) { $signStr .= $key . '=' . $value . '&'; } // 获取当前请求的域名和时间戳,组合为密钥 $host = $request->host(); $timestamp = $params['timestamp']; $appSecret = $host . $timestamp; // 添加密钥 $signStr = rtrim($signStr, '&') . $appSecret; // 生成本地签名(使用MD5签名算法) $localSign = md5($signStr); // 比对签名 if ($requestSign !== $localSign) { $this->error('签名验证失败'); } } /** * 返回错误信息 * @param string $msg 错误信息 * @param int $code 错误码 * @return void */ protected function error($msg, $code = 0) { $result = [ 'status' => $code, 'msg' => $msg, 'data' => null ]; $response = Response::create($result, 'json', $code); throw new HttpResponseException($response); } /** * 中间件结束调度 * @param \think\Response $response * @return void */ public function end(\think\Response $response) { } /** * 获取路径白名单 * * @return array 白名单路径列表 */ protected function getWhitelistPaths() { // 1. 默认白名单路径(如支付回调通知等) $defaultWhitelist = [ 'notify/*', // 支付回调等通知 'health', // 健康检查 'debug', // 调试接口 'generate_urllinks', ]; // 2. 从配置文件中获取白名单路径 try { $configWhitelist = Config::get('api.whitelist_paths', []); if (!empty($configWhitelist) && is_array($configWhitelist)) { return array_merge($defaultWhitelist, $configWhitelist); } } catch (\Exception $e) { \think\facade\Log::error('获取API白名单路径配置失败: ' . $e->getMessage()); } return $defaultWhitelist; } /** * 路径匹配检查 * * @param string $pattern 白名单路径模式 * @param string $path 请求路径 * @return bool 是否匹配 */ protected function pathMatch($pattern, $path) { // 完全匹配 if ($pattern === $path) { return true; } // 通配符匹配 (例如: 'notify/*') if (strpos($pattern, '*') !== false) { $pattern = str_replace('*', '.*', $pattern); $pattern = '/^' . str_replace('/', '\/', $pattern) . '$/i'; return preg_match($pattern, $path); } return false; } }