用户认证 #

一、认证概述 #

1.1 认证流程 #

text
┌─────────────────────────────────────────────────────┐
│                  认证流程                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│   1. 用户提交凭证                                   │
│        ↓                                            │
│   2. 防火墙拦截请求                                 │
│        ↓                                            │
│   3. 认证器验证凭证                                 │
│        ↓                                            │
│   4. 用户提供者加载用户                             │
│        ↓                                            │
│   5. 创建认证令牌                                   │
│        ↓                                            │
│   6. 存储到安全上下文                               │
│                                                     │
└─────────────────────────────────────────────────────┘

1.2 认证方式 #

方式 适用场景
表单登录 传统Web应用
HTTP Basic API简单认证
API Token API认证
JWT 无状态API
OAuth 第三方登录

二、表单登录 #

2.1 配置表单登录 #

yaml
# config/packages/security.yaml
security:
    firewalls:
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                default_target_path: app_home
                always_use_default_target_path: false
                username_parameter: email
                password_parameter: password
                csrf_parameter: _csrf_token
                csrf_token_id: authenticate

2.2 登录控制器 #

php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        if ($this->getUser()) {
            return $this->redirectToRoute('app_home');
        }

        $error = $authenticationUtils->getLastAuthenticationError();
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/login.html.twig', [
            'last_username' => $lastUsername,
            'error' => $error,
        ]);
    }

    #[Route('/logout', name: 'app_logout')]
    public function logout(): void
    {
        throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
    }
}

2.3 登录模板 #

twig
{# templates/security/login.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}登录{% endblock %}

{% block body %}
<form method="post">
    {% if error %}
        <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    {% if app.user %}
        <div class="mb-3">
            已登录: {{ app.user.userIdentifier }}, 
            <a href="{{ path('app_logout') }}">退出</a>
        </div>
    {% endif %}

    <h1 class="h3 mb-3 font-weight-normal">登录</h1>
    
    <div class="form-group">
        <label for="inputEmail">邮箱</label>
        <input type="email" value="{{ last_username }}" name="email" id="inputEmail" 
               class="form-control" autocomplete="email" required autofocus>
    </div>
    
    <div class="form-group">
        <label for="inputPassword">密码</label>
        <input type="password" name="password" id="inputPassword" 
               class="form-control" autocomplete="current-password" required>
    </div>

    <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

    <button class="btn btn-lg btn-primary" type="submit">登录</button>
</form>
{% endblock %}

三、注册功能 #

3.1 注册控制器 #

php
<?php

namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;

class RegistrationController extends AbstractController
{
    public function __construct(
        private 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()) {
            $user->setPassword(
                $userPasswordHasher->hashPassword(
                    $user,
                    $form->get('plainPassword')->getData()
                )
            );

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

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

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

3.2 注册表单 #

php
<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'constraints' => [
                    new NotBlank(['message' => '请输入姓名']),
                ],
            ])
            ->add('email', EmailType::class, [
                'constraints' => [
                    new NotBlank(['message' => '请输入邮箱']),
                ],
            ])
            ->add('plainPassword', PasswordType::class, [
                'mapped' => false,
                'attr' => ['autocomplete' => 'new-password'],
                'constraints' => [
                    new NotBlank(['message' => '请输入密码']),
                    new Length([
                        'min' => 8,
                        'minMessage' => '密码至少{{ limit }}个字符',
                    ]),
                ],
            ])
            ->add('agreeTerms', CheckboxType::class, [
                'mapped' => false,
                'constraints' => [
                    new IsTrue(['message' => '请同意条款']),
                ],
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}

四、记住我功能 #

4.1 配置记住我 #

yaml
# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 604800
                path: /
                domain: ~
                secure: true
                httponly: true
                samesite: lax
                name: REMEMBERME

4.2 登录表单添加记住我 #

twig
<form method="post">
    {# 其他字段 #}
    
    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" name="_remember_me"> 记住我
        </label>
    </div>
    
    <button type="submit">登录</button>
</form>

五、登出功能 #

5.1 配置登出 #

yaml
# config/packages/security.yaml
security:
    firewalls:
        main:
            logout:
                path: app_logout
                target: app_home
                invalidate_session: true
                delete_cookies:
                    REMEMBERME: { path: /, domain: ~ }

5.2 登出链接 #

twig
<a href="{{ path('app_logout') }}">退出登录</a>

六、API Token认证 #

6.1 配置API Token #

yaml
# config/packages/security.yaml
security:
    providers:
        api_user_provider:
            entity:
                class: App\Entity\User
                property: apiToken

    firewalls:
        api:
            pattern: ^/api/
            stateless: true
            custom_authenticators:
                - App\Security\ApiTokenAuthenticator

6.2 自定义认证器 #

php
<?php

namespace App\Security;

use App\Repository\UserRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class ApiTokenAuthenticator extends AbstractAuthenticator
{
    public function __construct(
        private UserRepository $userRepository
    ) {}

    public function supports(Request $request): ?bool
    {
        return $request->headers->has('X-API-Token');
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('X-API-Token');
        
        if (null === $apiToken) {
            throw new CustomUserMessageAuthenticationException('No API token provided');
        }

        return new SelfValidatingPassport(new UserBadge($apiToken, function (string $apiToken) {
            return $this->userRepository->findOneBy(['apiToken' => $apiToken]);
        }));
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        return new JsonResponse(
            ['message' => strtr($exception->getMessageKey(), $exception->getMessageData())],
            Response::HTTP_UNAUTHORIZED
        );
    }
}

七、JWT认证 #

7.1 安装JWT库 #

bash
composer require lexik/jwt-authentication-bundle

7.2 生成密钥 #

bash
mkdir -p config/jwt
openssl genrsa -out config/jwt/private.pem -aes256 4096
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem

7.3 配置JWT #

yaml
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600

7.4 配置防火墙 #

yaml
# config/packages/security.yaml
security:
    firewalls:
        api:
            pattern: ^/api/
            stateless: true
            jwt: ~

7.5 登录获取Token #

php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use App\Entity\User;

class ApiLoginController extends AbstractController
{
    #[Route('/api/login', name: 'api_login', methods: ['POST'])]
    public function login(#[CurrentUser] ?User $user): JsonResponse
    {
        if (null === $user) {
            return $this->json([
                'message' => 'missing credentials',
            ], Response::HTTP_UNAUTHORIZED);
        }

        return $this->json([
            'user' => $user->getUserIdentifier(),
        ]);
    }
}

八、用户切换 #

8.1 配置用户切换 #

yaml
# config/packages/security.yaml
security:
    firewalls:
        main:
            switch_user: true

8.2 使用用户切换 #

twig
{# 切换到其他用户 #}
<a href="?_switch_user=user@example.com">切换用户</a>

{# 退出切换 #}
<a href="?_switch_user=_exit">退出切换</a>

九、认证事件 #

9.1 监听认证事件 #

php
<?php

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\Event\LogoutEvent;

class SecuritySubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            LoginSuccessEvent::class => 'onLoginSuccess',
            LogoutEvent::class => 'onLogout',
        ];
    }

    public function onLoginSuccess(LoginSuccessEvent $event): void
    {
        $user = $event->getUser();
        // 记录登录日志
    }

    public function onLogout(LogoutEvent $event): void
    {
        // 记录登出日志
    }
}

十、总结 #

本章学习了:

  • 认证流程概述
  • 表单登录配置
  • 注册功能实现
  • 记住我功能
  • 登出功能
  • API Token认证
  • JWT认证
  • 用户切换
  • 认证事件

下一章将学习 权限控制

最后更新:2026-03-28