You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:27:18