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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 06:47:01