serendipity_hq / bundle-users
一个帮助在Symfony应用程序中管理用户的Symfony扩展包。
Requires
- php: ^8.1
- ext-json: *
- doctrine/orm: ^2.7 | ^3.0
- nesbot/carbon: ^2.0 || ^3.0
- symfony/console: ~5.4|~6.4|~7.0
- symfony/form: ~5.4|~6.4|~7.0
- symfony/property-access: ~5.4|~6.4|~7.0
- symfony/string: ~5.4|~6.4|~7.0
- symfony/validator: ~5.4|~6.4|~7.0
- thecodingmachine/safe: ^1.0|^2.0
Requires (Dev)
- ext-ast: *
- bamarni/composer-bin-plugin: ^1.4
- phpstan/phpstan: 1.10.59
- phpstan/phpstan-doctrine: 1.3.62
- phpstan/phpstan-phpunit: 1.3.16
- phpstan/phpstan-symfony: 1.3.7
- rector/rector: 1.0.1
- roave/security-advisories: dev-master
- serendipity_hq/rector-config: ^1.0
- symfony/browser-kit: ~5.4|~6.4|~7.0
- symfony/css-selector: ~5.4|~6.4|~7.0
- symfony/debug-bundle: ~5.4|~6.4|~7.0
- symfony/monolog-bundle: ^2|^3
- symfony/phpunit-bridge: ~5.4|~6.0 || ^6.0|^7.0
- symfony/security-core: ~5.4|~6.4|~7.0
- symfony/stopwatch: ~5.4|~6.4|~7.0
- symfony/twig-bundle: ~5.4|~6.4|~7.0
- symfony/var-dumper: ~5.4|~6.4|~7.0
- symfony/web-profiler-bundle: ~5.4|~6.4|~7.0
- thecodingmachine/phpstan-safe-rule: 1.2.0
This package is auto-updated.
Last update: 2024-09-09 20:52:19 UTC
README
Serendipity HQ 用户扩展包
帮助管理Symfony应用程序中的用户。
当前状态
功能
提供一些实用工具,使得在Symfony应用程序中管理用户更加容易,在Symfony内置用户管理的基础上。
你喜欢这个扩展包吗?
留下一个 ★
或者运行
composer global require symfony/thanks && composer thanks
以感谢你当前项目中使用的所有库,包括这个!
文档
起点总是Symfony的文档。
一旦你配置了UserInterface
实体,配置了应用程序的安全性并构建了登录表单,就是时候创建你的第一个用户了,甚至在构建注册表单之前。
为了使用户管理更容易,SerendipityHQ Users Bundle
提供了一个命令shq:users:create
,允许从命令行创建用户。
它几乎可以即插即用:你只需要稍微调整一下由Symfony自动生成的实体。
安装Serendipity HQ用户扩展包
要安装该扩展包,运行
composer req serendipity_hq/bundle-users
然后在你的bundles.php
中激活该扩展包
<?php
// config/bundles.php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
+ SerendipityHQ\Bundle\UsersBundle\SHQUsersBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
];
使用shq:users:create
命令
此命令非常有用,因为它允许你在创建注册表单之前在你的应用程序中创建用户。
这样你就可以立即测试登录功能,也可以自动化应用程序中的某些任务,比如在开发环境中重置它,而无需每次都从注册表单创建新用户。
要使用该命令,你必须做一件简单的事情:在你的UserInterface
实体中实现HasPlainPasswordInterface
及其实现特质。
HasPlainPasswordInterface
使得可能获得一系列优势:我们将在稍后看到它们。
目前,请确保你永远不会将明文密码保存到数据库中:它仅在UserInterface
对象的生命周期中有效,并允许你的应用程序实现一些基本功能。
目前,让我们实现接口和特质。
- 打开你的
UserInterface
实体(位于src/App/Entity/User.php
); - 实现接口
\SerendipityHQ\Bundle\UsersBundle\Model\Property\HasPlainPasswordInterface
- 使用特质
SerendipityHQ\Bundle\UsersBundle\Model\Property\HasPlainPasswordTrait
这些修改完成后,你的实体应该看起来像这样
<?php declare(strict_types = 1); /* * This file is part of Trust Back Me. * * Copyright (c) Adamo Aerendir Crespi <hello@aerendir.me>. * * This code is to consider private and non disclosable to anyone for whatever reason. * Every right on this code is reserved. * * For the full copyright and license information, please view the LICENSE file that * was distributed with this source code. */ namespace App\Entity; use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; + use SerendipityHQ\Bundle\UsersBundle\Property\HasPlainPasswordInterface; + use SerendipityHQ\Bundle\UsersBundle\Property\HasPlainPasswordTrait; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Entity(repositoryClass=UserRepository::class) * @ORM\Table(name="tbme_users") */ + class User implements UserInterface, HasPlainPasswordInterface { + use HasPlainPasswordTrait; // Here the remaining code of your entity // ... }
现在使用命令行创建你的第一个用户
Aerendir@SerendipityHQ % bin/console shq:user:create Aerendir 1234
Create user
===========
Password for user Aerendir: 1234
[OK] User Aerendir created.
Aerendir
(第一个参数),是主属性值(在文件 config/packages/security.yaml
中的 security.providers.[your_user_provider].entity.property
设置的值);1234
(第二个参数),是分配给用户的密码。
你现在可以测试应用的登录功能了:访问 http://your_app_url/login
并提供你刚刚创建的用户凭据。
现在登录功能正常,我们可以进一步了解 HasPlainPasswordInterface
的用途。
HasPlainPasswordInterface
的用途
接口 HasPlainPasswordInterface
激活了由 Serendipity HQ Users Bundle 提供的 Doctrine 监听器。
这个监听器读取明文密码(由特质 HasPlainPasswordTrait
管理)并自动进行编码。
这样你将能够在用户对象的整个生命周期中访问明文密码
- 你不需要关心密码的加密;
- 你可以使用明文密码来做你喜欢的事情:通过电子邮件发送给用户,在页面上显示,或者做你喜欢的任何事情。
注意:Serendipity HQ Users Bundle 从不会调用 UserInterface::ereaseCredentials()
方法:这是你应用的责任。
实现个人资料展示页面
下一步是让用户能够查看其个人资料。
你需要
- 一个
UserProfileController
- 一个模板来展示当前的个人资料
1. 创建 UserProfileController
<?php // src/Controller/UserProfileController.php declare(strict_types = 1); /* * This file is part of Trust Back Me. * * Copyright (c) Adamo Aerendir Crespi <hello@aerendir.me>. * * This code is to consider private and non disclosable to anyone for whatever reason. * Every right on this code is reserved. * * For the full copyright and license information, please view the LICENSE file that * was distributed with this source code. */ namespace App\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; /** * @Security("is_granted('ROLE_USER')", statusCode=403) */ final class UserProfileController extends AbstractController { /** * @Route("/me/", name="user_profile") */ public function show(): Response { return $this->render('user/profile.html.twig', [ 'user' => $this->getUser(), ]); } }
2. 创建模板 user/profile.html.twig
以下是渲染个人资料的模板代码
{# templates/user/profile.html.twig #} {% extends 'base.html.twig' %} {% block title %}Your profile{% endblock %} {% block body %} <h1 class="text-center">Hello {{ user.username }}</h1> {% endblock %}
这同样很简单,你可以根据你的应用程序进行定制。
实现个人资料编辑页面
为了创建一个编辑个人资料的页面,我们需要
- 一个
UserType
表单类型 - 在
UserProfileController
中的一个路由 - 一个用于展示表单的模板
创建 UserType
表单类型
使用 MakerBundle 来完成这个任务
Aerendir@Archimede bundle-users % bin/console make:form The name of the form class (e.g. VictoriousPizzaType): > UserType The name of Entity or fully qualified model class name that the new form will be bound to (empty for none): > User created: src/Form/UserType.php Success! Next: Add fields to your form and start using it. Find the documentation at https://symfony.com.cn/doc/current/forms.html
非常简单,只需要几秒钟!
2. 在 UserProfileController
中创建路由
让我们在 UserProfileController
中使用刚刚创建的表单类型
// src/Controller/UserProfileController.php + use App\Form\UserType; final class UserProfileController extends AbstractController { ... + /** + * @Route("/profile/edit", name="user_profile_edit") + */ + public function edit(Request $request): Response + { + /** @var User $user */ + $user = $this->getUser(); + $form = $this->getFormFactory()->create(UserType::class, $user, [ + 'action' => $this->generateUrl('user_profile_edit'), + 'method' => 'POST', + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + $url = $this->generateUrl('user_profile'); + + return new RedirectResponse($url); + } + + return $this->render('user/edit.html.twig', [ + 'form' => $form->createView(), + ]); + } ... }
3. 创建模板
创建文件 templates/user/edit.html.twig
{# templates/user/edit.html.twig #} {% extends 'base.html.twig' %} {% block title %}Edit your profile{% endblock %} {% block body %} {{ form(form) }} {% endblock %}
真的非常简单!
实现密码编辑
现在我们将开始看到 Symfony HQ Users Bundle 的有用性!
我们需要实现用户更改其密码的功能。
为了实现这个功能,我们需要
- 一个表单,让用户能够更改其密码
- 一个
UserPasswordController
- 一个用于展示表单的模板
1. 创建表单 ChangePasswordType
嘿,这里你不需要做任何事情!
Serendipity HQ Users Bundle 预置了一个更改密码的表单。
在这里找到它:src/Form/Type/UserPasswordChangeType.php
它提供了三个字段
old_password
plainPassword
plainPassword
的确认
底层,它使用 Symfony 提供的 RepeatedType
来确保新密码和确认密码相等。
多亏了接口 HasPlainPasswordInterface
,表单可以自动由 Serendipity HQ Users Bundle 处理。
一切都很简单!
现在我们需要一个路由。
2. 创建 UserPasswordController::changePassword()
像往常一样,使用 make
命令
Aerendir@Archimede bundle-users % symfony console make:controller Choose a name for your controller class (e.g. AgreeablePizzaController): > UserPasswordController created: src/Controller/UserPasswordController.php created: templates/user_password/index.html.twig Success! Next: Open your new controller class and add some pages!
命令创建了控制器及其模板。
打开控制器并移除index()
路由。
然后添加路由changePassword()
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; + use SerendipityHQ\Bundle\UsersBundle\Manager\PasswordManager; + use SerendipityHQ\Bundle\UsersBundle\Property\HasPlainPasswordInterface; + use SerendipityHQ\Bundle\UsersBundle\SHQUsersBundle; + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Contracts\Translation\TranslatorInterface; class UserPasswordController extends AbstractController { + private PasswordManager $passwordManager; + + public function __construct(PasswordManager $passwordManager) + { + $this->passwordManager = $passwordManager; + } - /** - * @Route("/user/password", name="user_password") - */ - public function index() - { - return $this->render('user_password_test/index.html.twig', [ - 'controller_name' => 'UserPasswordTestController', - ]); - } + /** + * @Route("/profile/password", name="user_password_change") + * @Security("is_granted('ROLE_USER')", statusCode=403) + */ + public function changePassword(Request $request, TranslatorInterface $translator): Response + { + /** @var HasPlainPasswordInterface $user */ + $user = $this->getUser(); + $form = $this->passwordManager->getPasswordHelper()->createFormPasswordChange($user); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + $this->addFlash('success', $translator->trans('user.password.change_password.success', [], 'shq_users')); + $url = $this->generateUrl('user_profile'); + + return new RedirectResponse($url); + } + + return $this->render('user/password/password_change.html.twig', [ + 'form' => $form->createView(), + ]); + } }
这里有一些需要注意的事项
- 使用注解
@Security
:如果当前用户未登录,该路由将返回一个Symfony\Component\Security\Core\Exception\AccessDeniedException
; - 路由的名称必须与Routes::PASSWORD_CHANGE中的相同;
- 变量
$user
被指定为类型HasPlainPasswordInterface
:实际上,PasswordHelper::createFormPasswordChange()
方法接受类型为HasPlainPasswordInterface
的变量; - 如果表单已提交且有效,我们只需调用
EntityManagerInterface::flush()
:实际上,表单将自动使用提供的新明文密码更新$user
;
一切准备就绪:现在您的用户可以更改他们的密码了!
请记住,在您的应用中某处添加此页面的链接(通常在个人资料页或用户菜单中)。
现在我们需要让用户在忘记密码的情况下重置他们的密码。
实现密码重置
重置密码的流程如下
- 显示一个页面,用户可以提供他的/她的主要标识符(用户名、电子邮件、电话号码等.);
- 生成一个唯一的令牌并发送给用户(通过电子邮件、短信或其他您喜欢的渠道)
- 验证令牌,并向用户显示一个允许设置新密码的表单。
在本例中,我们将假设以下情况
- 用户的唯一标识符是电子邮件;
- 令牌将通过电子邮件发送。
要实现整个流程,我们需要
- 三个路由来管理三个步骤(重置请求、发送确认链接、重置页面)
- 相应的模板(用于路由和电子邮件)
- 一个表示令牌的实体
- 一个发送电子邮件的监听器
让我们开始吧!
创建重置请求
为了能够请求重置令牌,我们需要
- 一个表单类型
- 一个路由
- 一个模板
首先在UserPasswordController
中添加方法resetRequest()
// src/Controller/ class UserPasswordController extends AbstractController { /** * @Route("password-reset", name="user_password_reset_request") */ public function resetRequest(Request $request): Response { $form = $this->passwordManager->getPasswordHelper()->createFormPasswordResetRequest(); $form->handleRequest($request); // Listen for the event PasswordResetTokenCreatedEvent or for the event PasswordResetTokenCreationFailedEvent // If the user was found, then process the request. // If the user is not found, do nothing to avoid disclosing if // the user exists or not (for security) if ( $form->isSubmitted() && $form->isValid() && $this->passwordManager->handleResetRequest($request, $form) ) { $this->getDoctrine()->getManager()->flush(); return $this->redirectToRoute('user_password_reset_check_email'); } return $this->render('App/user/password/reset_request.html.twig', [ 'form' => $form->createView(), ]); } }
如您所见,路由非常简单。
这里有一些需要注意的事项
- 路由的名称必须与Routes::PASSWORD_RESET_REQUEST中的相同;
- 一旦我们确定表单没有错误,我们就通过Serendipity HQ Users Bundle处理请求,然后我们简单地提交更改到数据库:实际上,该包负责所有操作:我们只需提交它们(记住:该包永远不会提交Doctrine!)。
注意,我们不需要构建任何表单:它是由Serendipity HQ Users Bundle构建的。
现在我们需要创建模板:这是应用程序中非常定制的内容,因此我们需要自己创建它。
{# templates/user/password/reset_request.html.twig #} <h1>Reset your password</h1> {{ form_start(form) }} {{ form_row(form.primaryEmail) }} <div> <small> Enter your email address and we we will send you a link to reset your password. </small> </div> <button class="btn btn-primary">Send password reset email</button> {{ form_end(form) }}
我们准备好了。
现在是我们处理这个请求的时候了。
创建确认页面
创建方法UserPasswordController::resetRequestReceived()
/** * Confirmation page after a user has requested a password reset. * * @Route("/password-reset/requested", name="user_password_reset_request_received") */ public function resetRequestReceived(Request $request): Response { // Prevent users from directly accessing this page if (!$this->passwordManager->getPasswordResetHelper()->canAccessPageCheckYourEmail($request)) { return $this->redirectToRoute('user_password_reset_request'); } return $this->render('App/user/password/check_email.html.twig', [ 'tokenLifetime' => PasswordResetHelper::RESET_TOKEN_LIFETIME, ]); }
这个方法也非常简单。
您需要注意到的是对canAccessPageCheckYourEmail
的调用:此方法将读取当前(匿名)用户的会话,并检查是否在UserPasswordController::resetRequest()
中通过调用PasswordManager::handleResetRequest()
预先设置了值。
现在我们需要创建最后一个路由,这个路由将实际上让用户能够设置他们自己的新密码。
创建重置页面
创建方法UserPasswordController::reset()
/** * @Route("/password-reset/reset/{token}", name="user_password_reset_reset_password") */ public function reset(Request $request, EncoderFactoryInterface $passwordEncoderFactory, string $token = null): Response { if ($token) { // We store the token in session and remove it from the URL, to avoid the URL being // loaded in a browser and potentially leaking the token to 3rd party JavaScript. $this->passwordManager->getPasswordResetHelper()->storeTokenInSession($request, $token); return $this->redirectToRoute('user_password_reset_reset_password'); } $token = $this->passwordManager->getPasswordResetHelper()->getTokenFromSession($request); if (null === $token) { throw $this->createNotFoundException('No reset password token found in the URL or in the session.'); } try { /** * findUserByPublicToken also validates the token and throws exceptions on failed validation. * @var HasPlainPasswordInterface $user */ $user = $this->passwordManager->findUserByPublicToken($token); } catch (PasswordResetException $e) { $this->addFlash('user_password_reset_error', sprintf( 'There was a problem validating your reset request - %s', $e->getMessage() )); return $this->redirectToRoute('user_password_reset_request'); } $form = $this->passwordManager->getPasswordHelper()->createFormPasswordReset(); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->passwordManager->handleReset($token, $user, $form, $request); $this->getDoctrine()->getManager()->flush(); return $this->redirectToRoute('auth_login'); } return $this->render('App/user/password/reset_password.html.twig', [ 'form' => $form->createView(), ]); }
此方法比其他方法要长一些:它需要做很多事情!
代码已注释,所以它所做的是什么很清楚。
需要注意的唯一一点是,再次是调用方法 EntityManager::flush()
:同样,包永远不会刷新数据库:这是我们的责任。
如果您现在尝试重置密码,您将收到错误,而且实际上没有代码向用户发送令牌。
我们需要实现这两个缺失的部分。
创建 PasswordResetToken
实体
首先,我们需要创建将代表重置令牌的实体。
创建实体 PasswordResetToken
// src/Entity/PasswordResetToken.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use SerendipityHQ\Bundle\UsersBundle\Model\Property\PasswordResetTokenInterface; use SerendipityHQ\Bundle\UsersBundle\Model\Property\PasswordResetTokenTrait; use SerendipityHQ\Bundle\UsersBundle\Repository\PasswordResetTokenRepository; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Entity(repositoryClass=PasswordResetTokenRepository::class) * @ORM\Table(name="tbme_users_password_reset_tokens") */ class PasswordResetToken implements PasswordResetTokenInterface { use PasswordResetTokenTrait; /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @var UserInterface * @ORM\ManyToOne(targetEntity=User::class) * @ORM\JoinColumn(nullable=false) */ private $user; public function __construct(UserInterface $user) { $this->user = $user; } public function getId(): ?int { return $this->id; } public function getUser(): UserInterface { return $this->user; } }
此类必须实现接口 PasswordResetTokenInterface
,并将由 UserPasswordController::resetRequest()
中的 handleResetRequest()
方法使用。
您可以按需自定义它。
您可能想自定义的两件事是
- 您在
ManyToOne
关系中的User
类; - 实体的命名空间。
在这种情况下,包使用的默认命名空间是 App\Entity\PasswordResetToken
,但您可以更改它(以及实体类的名称)通过在配置中设置参数 shq_users.token_class
。
创建订阅者以向用户发送电子邮件
要实际向用户发送电子邮件,我们需要一个订阅者,它监听事件 PasswordResetTokenCreatedEvent
。
创建它
<?php // src/Subscriber/SecurityPasswordResetSubscriber.php namespace App\Subscriber; use SerendipityHQ\Bundle\UsersBundle\Event\PasswordResetTokenCreatedEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; /** * Send reminders to ask the feedback releasing. */ final class SecurityPasswordResetSubscriber implements EventSubscriberInterface { private MailerInterface $mailer; public function __construct(MailerInterface $mailer) { $this->mailer = $mailer; } /** * {@inheritdoc} */ public static function getSubscribedEvents(): array { return [ PasswordResetTokenCreatedEvent::class => 'onPasswordResetTokenCreated' ]; } public function onPasswordResetTokenCreated(PasswordResetTokenCreatedEvent $event) { $user = $event->getUser(); $token = $event->getToken(); $email = (new TemplatedEmail()) // @todo Use values from ENV config ->from(new Address('hello@trustback.me', 'TrustBack.Me')) ->to($user->getEmail()) // @todo translate this message ->subject('Your password reset request') ->htmlTemplate('App/user/password/reset_email.html.twig') ->context(['token' => $token]); $this->mailer->send($email); } }
现在我们完成了!
尝试重置您的密码,所有都应该按预期工作。
如果有什么地方不工作,请打开一个问题。
其他有用功能
-
UsersManager
(由 SHQUsersBundle 提供) -
事件(由包触发的事件:它们在 src/Events 中)
-
命令(可用命令的说明)(如何扩展命令)
-
创建注册表单
-
创建登录表单
-
创建密码重置表单
使用管理器创建用户
create()
方法触发 UserCreatedEvent
事件。
您可以监听它,如果您愿意,可以设置 UserCreatedEvent::stopPropagation()
。
如果 true === UserCreatedEvent::isPropagationStopped()
,则管理器不会将用户持久化到数据库中,但仍然会返回它。
这样,您可以决定做什么:创建一个新用户,自行持久化,等等。
管理器永远不会调用 EntityManager::flush()
:您总是负责调用它并决定何时以及是否这样做。
UserInterface::eraseCredentials()
方法永远不会由包调用:这是您的责任,因为我们不知道您是否还需要它们(例如,向注册用户发送带有纯文本密码的电子邮件)。
创建注册表单
当使用 Symfony 的 make 命令时,生成的表单类型有一个名为 plainPassword
的字段,它具有选项 mapped = false
。
在 User
实体中实现特质 HasPlainPassword
,然后从表单类型中删除选项。
这样,User
实体将有一个由特质提供的 plainPassword
字段,表单将绑定表单字段到此属性,并且 Doctrine 监听器将自动编码密码。
此外,修改控制器以不再编码密码。
如何创建用于管理用户的命令
管理密码重置
- 创建 repo
PasswordResetTokenRepository
并实现接口PasswordResetTokenRepositoryInterface
- 创建控制器
PasswordController
(使用哪些方法/路由?)
处理垃圾回收
你喜欢这个扩展包吗?
留下一个 ★
或者运行
composer global require symfony/thanks && composer thanks
以感谢你当前项目中使用的所有库,包括这个!