Symfony6调用HaveIBeenPwned API请求结构异常求助
问题:Symfony 6调用HaveIBeenPwned API请求结构问题排查
我正在开发一个基于Symfony 6的网站,通过调用HaveIBeenPwned API获取账户泄露信息,但遇到了请求结构相关的问题,无法定位具体原因。
相关代码
\src\Controller\BreachDirectoryController.php
<?php namespace App\Controller; use Psr\Log\LoggerInterface; use App\FormType\EmailCheckType; use Symfony\Component\Validator\Validation; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Validator\Constraints\Email as EmailConstraint; class BreachDirectoryController extends AbstractController { private $client; private $apiKey; private $logger; // 构造函数,注入HTTP客户端、API密钥和日志服务 public function __construct(HttpClientInterface $client, string $apiKey, LoggerInterface $logger) { $this->client = $client; $this->apiKey = $apiKey; $this->logger = $logger; $this->logger->info('BreachDirectory控制器已初始化。'); } // 首页路由 #[Route('/', name: 'app_homepage')] public function homepage(Request $request): Response { $this->logger->info('访问首页。'); // 创建并处理表单 $form = $this->createForm(EmailCheckType::class); $form->handleRequest($request); $this->logger->debug('表单已处理。'); // 检查表单提交及有效性 if ($form->isSubmitted() && $form->isValid()) { $email = $form->get('email')->getData(); $this->logger->info("提交的邮箱为:{$email}"); // 重定向到结果页面,邮箱编码到URL中 $url = $this->generateUrl('results', [ 'email_check[email]' => $email, ]); return $this->redirect($url); } // 渲染首页及表单 return $this->render('breach_directory/homepage.html.twig', [ 'form' => $form->createView(), ]); } // 邮箱验证后结果页面路由 #[Route('/results', name: 'results', methods: ["GET"])] public function checkEmail(Request $request): Response { $this->logger->info('访问结果页面。'); // 从请求数据中获取邮箱 $formData = $request->query->get('email_check'); dump($formData); $email = $formData['email'] ?? null; // 若未提供邮箱,记录警告日志 if (empty($email)) { $this->logger->warning("未提供用于验证的邮箱。"); return new Response('未提供用于验证的邮箱。', Response::HTTP_BAD_REQUEST); } // 记录正在验证的邮箱 $this->logger->info("正在验证邮箱:{$email}"); // 验证邮箱格式 $validator = Validation::createValidator(); $violations = $validator->validate($email, [new EmailConstraint()]); if (0 !== count($violations)) { // 若条件成立,则存在验证问题 // 返回邮箱无效的错误信息 // 可在此添加日志查看验证违规详情 $this->logger->warning("验证错误:" . $violations[0]->getMessage()); return new Response('邮箱地址无效。', Response::HTTP_BAD_REQUEST); } // try块处理API请求及潜在错误 try { // 构建API请求URL $url = 'https://haveibeenpwned.com/api/v3/breachedaccount/' . urlencode($email); $this->logger->info("请求URL:{$url}"); // 定义HTTP请求头 $headers = [ 'hibp-api-key' => $this->apiKey, 'user-agent' => 'CheckIt', // 可根据需要添加其他请求头 ]; // 添加'Accept'请求头 $headers['Accept'] = 'application/json'; $this->logger->debug("HTTP请求:" . json_encode([ 'method' => 'GET', 'url' => $url, 'headers' => $headers, ])); // 请求前日志 $this->logger->debug("请求头:" . json_encode($headers)); $this->logger->debug("API密钥:" . $this->apiKey); // 发送带请求头的HTTP请求 $response = $this->client->request('GET', $url, [ 'headers' => $headers, ]); // 记录HTTP响应状态码 $statusCode = $response->getStatusCode(); $this->logger->info("响应状态码:{$statusCode}"); // 根据状态码处理响应 if ($statusCode === 200) { $data = json_decode($response->getContent(), true); return $this->render('breach_directory/results.html.twig', [ 'email' => $email, 'breaches' => $data, ]); } elseif ($statusCode === 404) { return $this->render('breach_directory/results.html.twig', [ 'email' => $email, 'breaches' => [], ]); } else { $this->logger->error("意外状态码:{$statusCode}"); return new Response('从HaveIBeenPwned API收到意外响应。', Response::HTTP_INTERNAL_SERVER_ERROR); } } catch (\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface $e) { $statusCode = $e->getResponse()->getStatusCode(); $this->logger->error("ClientException,状态码{$statusCode}:" . $e->getMessage()); return new Response('与HaveIBeenPwned API通信时出错。', Response::HTTP_INTERNAL_SERVER_ERROR); } catch (\Exception $e) { $this->logger->critical("通用异常:" . $e->getMessage()); return new Response('与HaveIBeenPwned API通信时出错。', Response::HTTP_INTERNAL_SERVER_ERROR); } } }
\src\FormType\EmailCheckType.php
<?php namespace App\FormType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Form\Extension\Core\Type\EmailType; class EmailCheckType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('email', EmailType::class, [ 'label' => '邮箱地址:', 'required' => true, 'attr' => [ 'class' => 'form-control', ], ]); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'constraints' => [ new Email([ 'message' => '邮箱地址"{{ value }}"无效。', ]), ], ]); } }
排查要点
- URL编码优化:替换
urlencode为rawurlencode,后者对特殊字符的编码更符合API要求,比如+会被编码为%2B,避免与空格的编码混淆。 - 请求头合规性检查:
- 确认
hibp-api-key是从HaveIBeenPwned官网获取的有效密钥,且未过期 - 完善
user-agent字段,API要求UA需包含应用名称和联系方式,比如改为CheckIt/1.0 (your-email@example.com),避免因UA过于简单被拦截
- 确认
- 异常与状态码处理:
- 在
ClientException捕获块中添加响应内容日志,获取API返回的具体错误信息,比如密钥无效、权限不足等:catch (\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface $e) { $response = $e->getResponse(); $statusCode = $response->getStatusCode(); $content = $response->getContent(false); $this->logger->error("ClientException,状态码{$statusCode}:{$e->getMessage()},响应内容:{$content}"); return new Response('与HaveIBeenPwned API通信时出错。', Response::HTTP_INTERNAL_SERVER_ERROR); } - 新增对401(未授权)、403(禁止访问)状态码的处理,这些通常是密钥或UA配置错误导致的
- 在
- 参数传递简化:重定向时直接使用
'email' => $email作为参数,在checkEmail方法中通过$request->query->get('email')获取,避免数组解析的潜在问题 - 冗余验证移除:表单已经完成邮箱格式验证,
checkEmail中的重复验证可以删除,或者注入Symfony的ValidatorInterface服务替代手动创建实例
内容的提问来源于stack exchange,提问作者Pierre-Arthur DEMENGEL
相关产品推荐
相关产品推荐

