用户认证 #
一、认证概述 #
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