You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 23:52:03