不带 SonataUserBundle 的 SonataAdmin
这里我们将解释如何使用 Symfony Guard 而不是 SonataUserBundle 来使用 SonataAdmin。
本简短指南中使用的代码可以在这里找到,并支持不同的 Symfony 版本。
方法
我们需要设置的内容
- 用户实体
- 用户提供器
- AdminLoginForm
- AdminLoginAuthenticator
- AdminLoginController
- 在 security.yaml 中设置防火墙
- 类似于 Sonata 风格的登录模板
用户实体
这代表您的安全用户,您可以在这里阅读更多相关信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity]
class User implements UserInterface
{
#[ORM\Id]
#[ORM\Column(type: Types::INTEGER)]
#[ORM\GeneratedValue]
private ?int $id = null;
#[ORM\Column(type: Types::STRING, unique: true)]
private ?string $email = null;
#[ORM\Column(type: Types::STRING, nullable: true)]
private ?string $password = null;
#[ORM\Column(type: Types::ARRAY)]
private array $roles = [];
}
用户提供器
这代表您的用户提供器,它将用于加载您的安全用户,在这里阅读更多相关信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
final class UserProvider implements UserProviderInterface
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function loadUserByUsername($email): User
{
$user = $this->findOneUserBy(['email' => $email]);
if (!$user) {
throw new UsernameNotFoundException(
sprintf(
'User with "%s" email does not exist.',
$email
)
);
}
return $user;
}
private function findOneUserBy(array $options): ?User
{
return $this->entityManager
->getRepository(User::class)
->findOneBy($options);
}
public function refreshUser(UserInterface $user): User
{
assert($user instanceof User);
if (null === $reloadedUser = $this->findOneUserBy(['id' => $user->getId()])) {
throw new UsernameNotFoundException(sprintf(
'User with ID "%s" could not be reloaded.',
$user->getId()
));
}
return $reloadedUser;
}
public function supportsClass($class): bool
{
return $class === User::class;
}
}
AdminLoginForm
一个用于验证我们数据的小型登录表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
final class AdminLoginForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class)
->add('password', PasswordType::class);
}
}
AdminLoginAuthenticator
这代表您的自定义身份验证系统,在这里阅读更多相关信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
namespace App\Security;
use App\Form\AdminLoginForm;
use App\Entity\User;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\AuthenticatorInterface;
final class AdminLoginAuthenticator extends AbstractFormLoginAuthenticator implements AuthenticatorInterface
{
private FormFactoryInterface $formFactory;
private RouterInterface $router;
private UserPasswordEncoderInterface $passwordEncoder;
public function __construct(
FormFactoryInterface $formFactory,
RouterInterface $router,
UserPasswordEncoderInterface $passwordEncoder
) {
$this->formFactory = $formFactory;
$this->router = $router;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request): bool
{
return $request->attributes->get('_route') === 'admin_login' && $request->isMethod('POST');
}
public function getCredentials(Request $request): array
{
$form = $this->formFactory->create(AdminLoginForm::class);
$form->handleRequest($request);
$data = $form->getData();
$request->getSession()->set(
Security::LAST_USERNAME,
$data['email']
);
return $data;
}
public function getUser($credentials, UserProviderInterface $userProvider): UserInterface
{
return $userProvider->loadUserByUsername($credentials['email']);
}
public function checkCredentials($credentials, UserInterface $user): bool
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): RedirectResponse
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->router->generate('admin_login'));
}
protected function getLoginUrl(): string
{
return $this->router->generate('admin_login');
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): RedirectResponse
{
return new RedirectResponse($this->router->generate('sonata_admin_dashboard'));
}
}
AdminLoginController
一个控制器,用于渲染登录表单。注销被有意留空,因为这将由 Symfony 处理,但我们仍然需要注册该路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Form\AdminLoginForm;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\HttpFoundation\Response;
final class AdminLoginController extends AbstractController
{
private AuthenticationUtils $authenticationUtils;
public function __construct(AuthenticationUtils $authenticationUtils)
{
$this->authenticationUtils = $authenticationUtils;
}
#[Route('/admin/login', name: 'admin_login')]
public function loginAction(): Response
{
$form = $this->createForm(AdminLoginForm::class, [
'email' => $this->authenticationUtils->getLastUsername()
]);
return $this->render('security/login.html.twig', [
'last_username' => $this->authenticationUtils->getLastUsername(),
'form' => $form->createView(),
'error' => $this->authenticationUtils->getLastAuthenticationError(),
]);
}
#[Route('/admin/logout', name: 'admin_logout')]
public function logoutAction(): void
{
// Left empty intentionally because this will be handled by Symfony.
}
}
在 security.yaml
中设置防火墙
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
# config/packages/security.yaml
security:
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
SONATA:
- ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT
encoders:
App\Entity\User: auto # use bcrypt if you are using "symfony/security-bundle" < 4.3
providers:
users:
id: App\Security\UserProvider
firewalls:
# Disabling the security for the web debug toolbar, the profiler and Assetic.
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# -> custom firewall for the admin area of the URL
admin:
pattern: /admin(.*)
form_login:
provider: users
login_path: admin_login
use_forward: false
check_path: admin_login
failure_path: null
logout:
path: admin_logout
target: admin_login
anonymous: true
guard:
authenticators:
- App\Security\AdminLoginAuthenticator
main:
anonymous: ~
access_control:
- { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: [ROLE_ADMIN, ROLE_SONATA_ADMIN] }
- { path: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
类似于 Sonata 风格的登录模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
{# templates/security/login.html.twig #}
{% extends '@SonataAdmin/standard_layout.html.twig' %}
{% block sonata_nav %}
{% endblock sonata_nav %}
{% block logo %}
{% endblock logo %}
{% block sonata_left_side %}
{% endblock sonata_left_side %}
{% block body_attributes %}class="sonata-bc login-page"{% endblock %}
{% block sonata_wrapper %}
<div class="login-box">
<div class="login-logo">
<a href="{{ path('sonata_admin_dashboard') }}">
<span>Login</span>
</a>
</div>
<div class="login-box-body">
{% block sonata_user_login_form %}
{% block sonata_user_login_error %}
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
{% endblock %}
{% for label, flashes in app.session.flashbag.all %}
{% for flash in flashes %}
<div class="alert alert-{{ label }}">
{{ flash }}
</div>
{% endfor %}
{% endfor %}
<p class="login-box-msg">{{ 'Authentication'|trans }}</p>
<form action="{{ path("admin_login") }}" method="post" role="form">
{{ form_row(form._token) }}
<div class="form-group has-feedback">
<input type="text" class="form-control" id="username" name="{{ form.email.vars.full_name }}" value="{{ last_username }}" required="required" placeholder="Email"/>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" id="password" name="{{ form.password.vars.full_name }}" required="required" placeholder="Password"/>
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">Login</button>
</div>
</div>
</form>
{% endblock %}
</div>
</div>
{% endblock sonata_wrapper %}
登录表单将如下所示

这项工作,包括代码示例,均根据 Creative Commons BY-SA 3.0 许可协议获得许可。