Symfony 7.1集成AltCha API遇403错误求助
排查Symfony 7.1集成AltCha API的403错误
问题现象
集成AltCha API时始终收到403状态码,错误信息如下:
{"error":"Invalid API Key.","statusCode":403}
已确认密钥正确且重新生成新密钥,请求头中已包含Referer,但问题依旧。查阅AltCha官方文档说明:
403 Forbidden: The API Key may not match the Referer header. Ensure you send a valid Referer header with an appropriate origin, and that the API Key was created for the same domain.
怀疑本地开发环境的localhost域名是问题诱因。
相关代码
KontaktController代码
<?php namespace App\Controller; use App\Entity\Kontakt; use App\Form\KontaktType; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; use Symfony\Component\Routing\Attribute\Route; use Symfony\Contracts\HttpClient\HttpClientInterface; class KontaktController extends AbstractController { private EntityManagerInterface $em; private HttpClientInterface $httpClient; private string $altchaSecretKey; private string $altchaApiKey; private LoggerInterface $logger; public function __construct( EntityManagerInterface $em, HttpClientInterface $httpClient, string $altchaSecretKey, string $altchaApiKey, LoggerInterface $logger ) { $this->em = $em; $this->httpClient = $httpClient; $this->altchaSecretKey = $altchaSecretKey; $this->altchaApiKey = $altchaApiKey; $this->logger = $logger; } #[Route('/kontakt', name: 'app_kontakt')] public function index(Request $request, MailerInterface $mailer): Response { $title = 'Markus Michalski - Kontakt'; $contact = new Kontakt(); $form = $this->createForm(KontaktType::class, $contact); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $altchaToken = $form->get('altchaToken')->getData(); // AltCha Verifizierung try { $altchaResponse = $this->httpClient->request('POST', 'https://eu.altcha.org/api/v1/challenge/verify', [ 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->altchaSecretKey, 'Referer' => $request->getSchemeAndHttpHost(), ], 'json' => [ 'payload' => $altchaToken, ], ]); $statusCode = $altchaResponse->getStatusCode(); $content = $altchaResponse->getContent(false); if ($statusCode !== 200) { $this->logger->error("AltCha API returned status code $statusCode: $content"); throw new \Exception("AltCha verification failed"); } $altchaResult = $altchaResponse->toArray(); if (!$altchaResult['verified']) { $this->addFlash('error', 'AltCha-Verifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.'); return $this->renderFormWithData($form, $title); } } catch (\Exception $e) { $this->logger->error('AltCha verification failed: ' . $e->getMessage()); $this->addFlash('error', 'Ein Fehler ist bei der Verifizierung aufgetreten. Bitte versuchen Sie es später erneut.'); return $this->renderFormWithData($form, $title); } // Form data processing $contact = $form->getData(); $contact->setTimestamp(new \DateTime('now')); $this->em->persist($contact); $this->em->flush(); $email = new TemplatedEmail(); $email ->from(new Address('XXXX', 'Kontaktformular')) ->to('XXXXX') ->subject('Anfrage über das Kontaktformular') ->htmlTemplate('email/kontaktSelf.html.twig') ->locale('de') ->context(['contact' => $contact, 'title' => $title]); try { $mailer->send($email); } catch (TransportExceptionInterface $exception) { $rep = $this->em->getRepository(Kontakt::class); $updateContact = $rep->findOneBy(['id' => $contact->getId()]); $updateContact->setExeption($exception->getMessage()); $this->em->persist($updateContact); $this->em->flush(); } return $this->render('kontakt/success.html.twig', [ 'title' => $title, 'contact' => $contact, ]); } return $this->renderFormWithData($form, $title); } private function renderFormWithData($form, $title): Response { return $this->render('kontakt/index.html.twig', [ 'title' => $title, 'form' => $form->createView(), 'altchaApiKey' => $this->altchaApiKey, ]); } }
Twig模板代码
{% extends 'base.html.twig' %} {% block body %} <main> <section class="section"> <h1>Kontaktformular</h1> {% for label, messages in app.flashes %} {% for message in messages %} <div class="alert alert-{{ label == 'error' ? 'danger' : label }}"> {{ message }} </div> {% endfor %} {% endfor %} {{ form_start(form) }} {{ form_errors(form) }} <label for="name">{{ field_label(form.name) }}</label> <input type="text" id="name" name="{{ field_name(form.name) }}" required value="{{ field_value(form.name) }}"> {% if form.name.vars.errors|length > 0 %} <div class="contact_error"> {{ form_errors(form.name) }} </div> {% endif %} <label for="email">{{ field_label(form.email) }}</label> <input type="email" id="email" name="{{ field_name(form.email) }}" required value="{{ field_value(form.email) }}"> {% if form.email.vars.errors|length > 0 %} <div class="contact_error"> {{ form_errors(form.email) }} </div> {% endif %} <label for="subject">{{ field_label(form.subject) }}</label> <input type="text" id="subject" name="{{ field_name(form.subject) }}" required value="{{ field_value(form.subject) }}"> {% if form.subject.vars.errors|length > 0 %} <div class="contact_error"> {{ form_errors(form.subject) }} </div> {% endif %} <label for="message">{{ field_label(form.message) }}</label> <textarea id="message" name="{{ field_name(form.message) }}" rows="5" required>{{ field_value(form.message) }}</textarea> {% if form.message.vars.errors|length > 0 %} <div class="contact_error"> {{ form_errors(form.message) }} </div> {% endif %} <div class="agree-terms checkbox-container"> {{ form_widget(form.agreeTerms, {'attr': {'class': 'checkbox-input'}}) }} <span class="checkbox-label">Ich habe die <a href="{{ path('app_datenschutz') }}" target="_blank">Datenschutzbestimmungen</a> zur Kenntnis genommen. </span> </div> {# AltCha Widget hinzufügen #} <altcha-widget challengeurl="https://eu.altcha.org/api/v1/challenge?apiKey={{ altchaApiKey }}" spamfilter ></altcha-widget> {{ form_widget(form.send) }} {{ form_rest(form) }} {{ form_end(form) }} <div class="contact-required-text">Alle Felder sind Pflichtfelder!</div> </section> </main> {% endblock %} {% block javascripts %} {{ parent() }} <script async defer type="module" src="https://eu.altcha.org/js/latest/altcha.min.js"></script> <script> document.addEventListener('DOMContentLoaded', function() { const form = document.querySelector('form[name="kontakt"]'); const altchaWidget = document.querySelector('altcha-widget'); form.addEventListener('submit', function(event) { event.preventDefault(); altchaWidget.verify().then(function(token) { document.getElementById('kontakt_altchaToken').value = token; form.submit(); }).catch(function(error) { console.error('AltCha verification failed:', error); // TODO ErrorMessage }); }); }); </script> {% endblock %}
排查建议
- 核对Referer与API密钥域名:确认
$request->getSchemeAndHttpHost()返回的完整域名(含端口,如http://localhost:8000)与AltCha后台创建API密钥时填写的域名完全一致,包括端口号。 - 区分API Key和Secret Key:前端Widget使用的是API Key,后端验证请求使用的是Secret Key,检查两者是否混淆。
- 本地域名映射测试:在本地hosts文件中将
localhost映射为自定义域名(如local.example.com),在AltCha后台添加该域名,同时修改Symfony访问地址为该自定义域名后重试。 - 打印实际请求头:在日志中输出
$request->getSchemeAndHttpHost()的实际值,确认没有大小写或多余字符。 - 检查API区域一致性:前端Widget和后端验证请求均使用
eu.altcha.org区域,确保区域匹配。
内容的提问来源于stack exchange,提问作者Tavodar
相关产品推荐
相关产品推荐

