Symfony/ApiPlatform PUT更新用户密码重复哈希致登录报错
问题产生原因
- ApiPlatform处理PUT更新请求时,会先从数据库读取已存在的User完整实体,再将请求体携带的字段合并到该实体上。当请求体未携带密码字段时,实体的password属性会保留数据库中存储的已完成哈希的旧密码字符串,并非空值。
- 现有
UserDataPersister的哈希判断逻辑存在缺陷:if ($data->getPassword())仅判断password属性是否有值,完全没有区分当前值是用户提交的明文新密码,还是数据库中已存储的哈希值。更新非密码字段时,旧哈希值会被误认为是用户提交的明文密码,被二次哈希后覆盖存储到数据库,最终导致原密码校验失败。 - User实体字段设计不合理:直接将数据库持久化用的
password字段加入user:write序列化组接收用户输入,没有单独拆分「用户提交的明文密码」和「数据库存储的哈希密码」两个独立字段,导致流程中无法判断密码是否真的被用户修改。
修复方案
第一步:调整User实体结构,新增独立明文密码字段
不要直接使用持久化的password字段接收用户输入,新增一个不映射数据库的临时字段专门接收用户提交的明文密码,调整序列化组与校验规则:
<?php // User实体中新增如下内容 use Symfony\Component\Validator\Constraints as Assert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] #[ORM\HasLifecycleCallbacks] // 新增生命周期回调注解 #[UniqueEntity(fields: "email", message: "L'email est déjà utilisé")] #[ApiResource( normalizationContext: ['groups' => ['user:read']], denormalizationContext: ['groups' => ['user:write']], collectionOperations: [ "get" => ["security" => "is_granted('ROLE_ADMIN')"], "post"=> ["security" => ["is_granted('IS_AUTHENTICATED_ANONYMOUSLY')"], ], ], itemOperations: [ "get", "put" => ["security" => "object == user"], "delete" => ["security" => "object == user"], ], )] class User implements UserInterface, PasswordAuthenticatedUserInterface { // ... 其他原有属性保持不变 /** * 临时明文密码,不映射数据库,仅用于接收用户输入 */ #[Groups(["user:write"])] #[Assert\Length(min: 6, max: 4096)] private ?string $plainPassword = null; /** * 数据库存储的哈希密码,移除user:write序列化组和全局NotBlank校验 */ #[ORM\Column(type: 'string')] private $password; // ... 其他原有方法保持不变 // 新增plainPassword的getter/setter public function getPlainPassword(): ?string { return $this->plainPassword; } public function setPlainPassword(?string $plainPassword): self { $this->plainPassword = $plainPassword; return $this; } // 修改eraseCredentials方法,清空临时明文密码 public function eraseCredentials() { $this->plainPassword = null; } // 新增新建用户时的密码必填校验 #[ORM\PrePersist] public function validateNewUserPassword(): void { if ($this->getId() === null && empty($this->plainPassword)) { throw new \InvalidArgumentException('密码不能为空'); } } }
注意:一定要移除原
$password属性上的#[Groups("user:write")]和#[Assert\NotBlank]注解,避免序列化层直接修改持久化的哈希密码字段。
第二步:修改UserDataPersister哈希逻辑
仅当用户主动提交了明文新密码时才执行哈希操作,避免二次哈希已存储的密码:
<?php namespace App\DataPersister; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class UserDataPersister implements DataPersisterInterface { private $entityManager; private $userPasswordHasher; public function __construct(EntityManagerInterface $entityManager, UserPasswordHasherInterface $userPasswordHasher) { $this->entityManager = $entityManager; $this->userPasswordHasher = $userPasswordHasher; } public function supports($data): bool { return $data instanceof User; } /** * @param User $data */ public function persist($data) { // 仅当用户提交了明文新密码时,才执行哈希逻辑 if ($plainPassword = $data->getPlainPassword()) { $data->setPassword( $this->userPasswordHasher->hashPassword($data, $plainPassword) ); // 哈希完成后清空临时明文密码 $data->eraseCredentials(); } $this->entityManager->persist($data); $this->entityManager->flush(); } public function remove($data) { $this->entityManager->remove($data); $this->entityManager->flush(); } }
修复后逻辑说明
- 新建用户时,接口通过
plainPassword字段接收明文密码,后台自动哈希后存入password字段 - 更新用户姓名、头像等非密码字段时,不需要携带任何密码相关参数,持久化层检测到
plainPassword为空,不会触发密码哈希逻辑,数据库中存储的原密码哈希值不会被改动 - 需要修改密码时,传入
plainPassword字段即可,后台会自动哈希新密码覆盖旧值
注意:接口字段调整后,前端注册、修改密码的请求需要将原
password参数替换为plainPassword。
内容的提问来源于stack exchange,提问作者kjod
相关产品推荐
相关产品推荐

