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

Symfony应用中邮箱验证链接过期后获取新链接的实现

实现Symfony邮箱验证链接过期后的重发功能

我已经在Symfony应用中完成安全流程搭建,所有功能均正常运行,但不清楚当邮箱验证链接过期时,如何实现「请求新链接」的功能。以下是我的相关代码,寻求实现方法。


Security配置

security:
    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            login_throttling:
                max_attempts: 3
            logout:
                path: app_logout
            form_login:
                login_path: app_login
                check_path: app_login
                default_target_path: app_member
            remember_me:
                secret:   '%kernel.secret%' # required
                lifetime: 604800 # 1 week in seconds

    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/member, roles: ROLE_USER }

RegistrationController代码

namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Repository\UserRepository;
use App\Security\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface;
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\Mime\Address;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;

class RegistrationController extends AbstractController
{
    private EmailVerifier $emailVerifier;

    public function __construct(EmailVerifier $emailVerifier)
    {
        $this->emailVerifier = $emailVerifier;
    }

    #[Route('/register', name: 'app_register')]
    public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response
    {
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // encode the plain password
            $user->setPassword(
                $userPasswordHasher->hashPassword(
                    $user,
                    $form->get('plainPassword')->getData()
                )
            );

            $entityManager->persist($user);
            $entityManager->flush();

            // generate a signed url and email it to the user
            $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
                (new TemplatedEmail())
                    ->from(new Address('info@anteriority.ch', 'Account Confirmation'))
                    ->to($user->getEmail())
                    ->subject('Please Confirm your Email')
                    ->htmlTemplate('registration/confirmation_email.html.twig')
            );
            // do anything else you need here, like send an email

            return $this->redirectToRoute('app_login');
        }

        return $this->render('registration/register.html.twig', [
            'registrationForm' => $form->createView(),
        ]);
    }

    #[Route('/verify/email', name: 'app_verify_email')]
    public function verifyUserEmail(Request $request, TranslatorInterface $translator, UserRepository $userRepository): Response
    {
        $id = $request->query->get('id');

        if (null === $id) {
            return $this->redirectToRoute('app_register');
        }

        $user = $userRepository->find($id);

        if (null === $user) {
            return $this->redirectToRoute('app_register');
        }

        // validate email confirmation link, sets User::isVerified=true and persists
        try {
            $this->emailVerifier->handleEmailConfirmation($request, $user);
        } catch (VerifyEmailExceptionInterface $exception) {
            $this->addFlash('verify_email_error', $translator->trans($exception->getReason(), [], 'VerifyEmailBundle'));

            return $this->redirectToRoute('app_register');
        }

        // @TODO Change the redirect on success and handle or remove the flash message in your templates
        $this->addFlash('success', 'Your email address has been verified.');

        return $this->redirectToRoute('app_register');
    }
}

EmailVerifier代码

<?php

namespace App\Security;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;

class EmailVerifier
{
    public function __construct(
        private VerifyEmailHelperInterface $verifyEmailHelper,
        private MailerInterface $mailer,
        private EntityManagerInterface $entityManager
    ) {
    }

    public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
    {
        $signatureComponents = $this->verifyEmailHelper->generateSignature(
            $verifyEmailRouteName,
            $user->getId(),
            $user->getEmail(),
            ['id' => $user->getId()]
        );

        $context = $email->getContext();
        $context['signedUrl'] = $signatureComponents->getSignedUrl();
        $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
        $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();

        $email->context($context);

        $this->mailer->send($email);
    }

    /**
     * @throws VerifyEmailExceptionInterface
     */
    public function handleEmailConfirmation(Request $request, UserInterface $user): void
    {
        $this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getEmail());

        $roles = [];
        $roles[] = "ROLE_MEMBER";
        $user->setRoles($roles);
        $user->setIsVerified(true);

        $this->entityManager->persist($user);
        $this->entityManager->flush();
    }
}

实现步骤

1. 添加重发验证邮件的路由与方法

在RegistrationController中新增处理重发请求的代码:

// 记得引入必要的表单类
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Email;

#[Route('/resend-verification', name: 'app_resend_verification')]
public function resendVerification(Request $request, UserRepository $userRepository, TranslatorInterface $translator): Response
{
    // 创建邮箱输入表单
    $form = $this->createFormBuilder()
        ->add('email', EmailType::class, [
            'label' => '输入你的邮箱',
            'constraints' => [
                new NotBlank(['message' => '请输入邮箱地址']),
                new Email(['message' => '请输入有效的邮箱地址']),
            ]
        ])
        ->add('submit', SubmitType::class, ['label' => '重新发送验证链接'])
        ->getForm();

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $email = $form->get('email')->getData();
        $user = $userRepository->findOneBy(['email' => $email]);

        // 仅向未验证的用户发送新链接
        if (!$user || $user->isVerified()) {
            $this->addFlash('error', $translator->trans('用户不存在或已完成邮箱验证'));
            return $this->redirectToRoute('app_resend_verification');
        }

        // 调用已有的发送方法生成新的签名链接
        $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
            (new TemplatedEmail())
                ->from(new Address('info@anteriority.ch', 'Account Confirmation'))
                ->to($user->getEmail())
                ->subject('重新发送:请验证你的邮箱')
                ->htmlTemplate('registration/confirmation_email.html.twig')
        );

        $this->addFlash('success', '新的验证链接已发送至你的邮箱');
        return $this->redirectToRoute('app_login');
    }

    return $this->render('registration/resend_verification.html.twig', [
        'resendForm' => $form->createView(),
    ]);
}

2. 创建重发页面模板

在templates/registration/目录下创建resend_verification.html.twig:

{% extends 'base.html.twig' %}

{% block title %}重新发送验证链接{% endblock %}

{% block body %}
    <h1>重新发送邮箱验证链接</h1>

    {% for flash_error in app.flashes('error') %}
        <div class="alert alert-danger">{{ flash_error }}</div>
    {% endfor %}
    {% for flash_success in app.flashes('success') %}
        <div class="alert alert-success">{{ flash_success }}</div>
    {% endfor %}

    {{ form_start(resendForm) }}
        {{ form_widget(resendForm) }}
        <button type="submit" class="btn btn-primary">发送链接</button>
    {{ form_end(resendForm) }}
{% endblock %}

3. 修改验证失败后的跳转逻辑

在verifyUserEmail方法的异常捕获块中,重定向到重发页面:

catch (VerifyEmailExceptionInterface $exception) {
    $this->addFlash('verify_email_error', $translator->trans($exception->getReason(), [], 'VerifyEmailBundle'));

    // 替换原跳转,指向重发页面
    return $this->redirectToRoute('app_resend_verification');
}

4. 添加入口链接

在登录页面或注册成功页面添加重发入口,比如在login.html.twig中:

<p>还未验证邮箱?<a href="{{ path('app_resend_verification') }}">点击获取新的验证链接</a></p>

额外配置(可选)

若需要调整验证链接的过期时间,可修改config/packages/symfony_casts_verify_email.yaml:

symfony_casts_verify_email:
    lifetime: 3600 # 单位:秒,默认1小时,可根据需求调整

内容的提问来源于stack exchange,提问作者thicre

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:09:51