redis = (new \app\common\server\RedisHelper())->getRedis(); } /** * 处理请求签名验证 * * @param \think\Request $request * @param \Closure $next * @return \think\Response */ public function handle($request, \Closure $next) { // 获取当前请求路径 $path = $request->pathinfo(); // 检查是否在白名单内 if ($this->isWhitelistedPath($path, $request)) { return $next($request); } // 根据请求方法进行签名验证 $method = strtoupper($request->method()); if ($method == "GET") { $this->verifySignature($request, $request->get()); } elseif ($method == "POST") { $this->verifySignature($request, $request->post()); } // 继续执行下一个中间件 $response = $next($request); return $response; } /** * 检查请求路径是否在白名单中 * * @param string $path 请求路径 * @param \think\Request $request 请求对象 * @return bool 是否在白名单中 */ protected function isWhitelistedPath($path, $request) { // 检查是否有内部标识 $params = $request->param(); if (isset($params['is_test']) && $params['is_test'] === 'true') { return true; } // 获取白名单路径 $whitelistPaths = $this->getWhitelistPaths(); // 检查路径是否在白名单内 foreach ($whitelistPaths as $whitePath) { if ($this->pathMatch($whitePath, $path)) { return true; } } // 检查IP白名单 $ipWhitelist = $this->getIpWhitelist(); $clientIp = $request->ip(); if (in_array($clientIp, $ipWhitelist)) { return true; } return false; } /** * 验证请求签名 * @param \think\Request $request 请求对象 * @param array $params 请求参数 * @return void */ protected function verifySignature($request, $params) { // 检查是否有必要的签名参数 if (!isset($params['timestamp']) || !isset($params['sign']) || !isset($params['nonce'])) { $this->error('缺少必要的签名参数'); } // 检查时间戳是否在允许范围内(1分钟误差) $timestamp = intval($params['timestamp']); $now = time(); if (abs($now - $timestamp) > self::TIMESTAMP_TOLERANCE) { $this->error('请求时间戳超出允许范围'); } // 检查nonce是否被使用过(防重放攻击) $nonce = $params['nonce']; $nonceKey = self::REDIS_KEY_PREFIX . $nonce; if ($this->redis->exists($nonceKey)) { $this->error('无效的请求(nonce已被使用)'); } // 记录nonce到Redis,有效期10分钟(足够覆盖时间戳可接受的误差范围) $this->redis->setex($nonceKey, self::NONCE_EXPIRE_TIME, $timestamp); // 从请求中获取签名 $requestSign = $params['sign']; // 拷贝参数,移除不需要的参数 $signParams = $params; unset($signParams['s']); // 移除URL参数 unset($signParams['sign']); // 移除签名参数 // 按照键名对参数进行排序 ksort($signParams); // 组合参数为字符串 $signStr = ''; foreach ($signParams as $key => $value) { // 处理数组或对象类型的参数 if (is_array($value) || is_object($value)) { $value = json_encode($value, JSON_UNESCAPED_UNICODE); } $signStr .= $key . '=' . $value . '&'; } // 获取当前请求的域名和时间戳,组合为密钥 $host = $request->host(); $timestamp = $signParams['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); } /** * 获取路径白名单 * * @return array 白名单路径列表 */ protected function getWhitelistPaths() { // 1. 默认白名单路径(如支付回调通知等) $defaultWhitelist = [ 'notify/*', // 支付回调等通知 'health', // 健康检查 'debug', // 调试接口 'generate_urllinks', 'webhook/*', // 添加webhook路径 'internal/*', // 内部接口 ]; // 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; } /** * 获取IP白名单 * * @return array IP白名单列表 */ protected function getIpWhitelist() { // 默认IP白名单 $defaultIpWhitelist = [ '127.0.0.1', // 本地回环地址 '::1', // IPv6本地回环地址 ]; // 从配置文件中获取IP白名单 try { $configIpWhitelist = Config::get('api.ip_whitelist', []); if (!empty($configIpWhitelist) && is_array($configIpWhitelist)) { return array_merge($defaultIpWhitelist, $configIpWhitelist); } } catch (\Exception $e) { \think\facade\Log::error('获取API白名单IP配置失败: ' . $e->getMessage()); } return $defaultIpWhitelist; } /** * 路径匹配检查 * * @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; } /** * 中间件结束调度 * * @param \think\Response $response * @return void */ public function end(\think\Response $response) { // 可以在这里对响应做额外处理 } }