简体   繁体   English

重定向到页面时,会话过期

[英]Session expires when redirect to a page

In my application, once a user logs in, taken to the home page where he can view his details. 在我的应用程序中,一旦用户登录,便会转到主页,在其中可以查看其详细信息。 There is a button "Edit Profile" where the user will be taken to a page where he can edit the data. 有一个“编辑配置文件”按钮,在该按钮中,用户将被带到可以编辑数据的页面。 Once the editing successes, he is redirected back to the home page. 一旦编辑成功,他将被重定向回主页。 But here, it is redirected to the login page. 但是在这里,它被重定向到登录页面。 I think the session is expired unexpectedly. 我认为该会话意外过期。 How to overcome this issue? 如何克服这个问题?

// This is my update info controller //这是我的更新信息控制器

/**
 * @Route("/update/{id}", name="update")
 * @param $id
 * @param Request $request
 * @param UserPasswordEncoderInterface $passwordEncoder
 * @param UserInterface $loggedUser
 * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
 */

public function updateUser($id,Request $request, UserPasswordEncoderInterface $passwordEncoder, UrlGeneratorInterface $urlGenerator){

    $loggedUser = $this->get('security.token_storage')->getToken()->getUser()->getId();

    if ($id == $loggedUser){
        $em = $this->getDoctrine()->getManager();
        $conn =$em->getConnection();
        $user = $em->find(User::class,$id);

        $form = $this->createForm(RegisterType::class,$user, [
            'validation_groups' => ['update'],
        ]);


        $form->handleRequest($request);

        if($form->isSubmitted() && $form->isValid()) {

            $file = $request->files->get('register')['image'];
            if($file){
                $fileName = md5(uniqid()).'.'.$file->guessExtension();
                $file->move(
                    $this->getParameter('uploads_dir'), $fileName
                );
                $user->setImage($fileName);
            }

            if($user->getPassword() !="") {
                $user->setPassword($passwordEncoder->encodePassword($user,$user->getPassword()));

                $sql = '
                    UPDATE user
                    SET first_name = :firstName, last_name = :lastName, id_number = :idNumber, phone_number = :phoneNumber, address = :address, password = :password
                    WHERE id= :id
    ';
                $stmt = $conn->prepare($sql);
                $stmt->execute(['firstName' => $user->getFirstName(),
                    'lastName' => $user->getLastName(),
                    'idNumber' => $user->getIdNumber(),
                    'phoneNumber' => $user->getPhoneNumber(),
                    'address' => $user->getAddress(),
                    'password' => $user->getPassword(),
                    'id' => $id]);
            } else {
                $sql = '
                    UPDATE user
                    SET first_name = :firstName, last_name = :lastName, id_number = :idNumber, phone_number = :phoneNumber, address = :address
                    WHERE id= :id
    ';
                $stmt = $conn->prepare($sql);
                $stmt->execute(['firstName' => $user->getFirstName(),
                    'lastName' => $user->getLastName(),
                    'idNumber' => $user->getIdNumber(),
                    'phoneNumber' => $user->getPhoneNumber(),
                    'address' => $user->getAddress(),
                    'id' => $id]);
            }



            return new RedirectResponse($urlGenerator->generate('home'));
        }
    } else {
        return new RedirectResponse($urlGenerator->generate('home'));
    }

    return $this->render('register/update.html.twig', [
        'form'=>$form->createView(),
    ]);
}

// This is RegisterType form //这是RegisterType形式

class RegisterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email',EmailType::class,[
                'label'=>'Email',
                'required' => false,
                'attr'=>['placeholder'=>"Email"]
            ])
            ->add('password',RepeatedType::class,[
                'type' => PasswordType::class,
                'invalid_message' => 'The password fields must match.',
                'required' => false,
                'options' => ['attr' => ['class' => 'password-field']],
                'first_options'  => ['label' => 'Password','attr'=>['placeholder'=>"Password"]],
                'second_options' => ['label' => 'Confirm Password','attr'=>['placeholder'=>"Confirm Password"]],
            ])
            ->add('firstName',TextType::class,['label'=>'First Name',  'attr'=>['placeholder'=>"First Name"]])
            ->add('lastName',TextType::class,['label'=>'Last Name','attr'=>['placeholder'=>"Last Name"]])
            ->add('address',TextareaType::class,['required' => false,'label'=>'Address','attr'=>['placeholder'=>"Address"]])
            ->add('idNumber',TextType::class,['label'=>'NIC Number','attr'=>['placeholder'=>"NIC Number"]])
            ->add('phoneNumber',TelType::class,['label'=>'Phone Number','attr'=>['placeholder'=>"Phone Number"]])
            ->add('image',FileType::class,['label'=>'Photo','required'=>false,'attr'=>['hidden'=>"hidden", 'accept'=>"image/jpeg, image/png"]])
            ->add('save',SubmitType::class,[
                'label'=>'Register',
                'attr' => [
                    'class'=>"btn btn-outline-success float-right"
                ]
            ])

        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}

// This is my User Class //这是我的用户类

class User implements UserInterface{

/**
 * @ORM\Id()
 * @ORM\GeneratedValue()
 * @ORM\Column(type="integer")
 */
private $id;

/**
     * @ORM\Column(type="string", length=180, unique=true)
 * @Assert\Email()
 * @Assert\NotBlank()
 */
private $email;

/**
 * @ORM\Column(type="json")
 */
private $roles = [];

/**
 * @var string The hashed password
 * @ORM\Column(type="string")
 * @Assert\NotBlank()
 */
private $password;

/**
 * @ORM\Column(type="string",length=255)
 * @Assert\NotBlank(groups={"update"})
 *
 */
private $firstName;

/**
 * @ORM\Column(type="string",length=255)
 * @Assert\NotBlank(groups={"update"})
 */
private $lastName;

/**
 * @ORM\Column(type="string",length=255,nullable=true)
 *
 */
private $image;

/**
 * @ORM\Column(type="string", nullable=true)
 */
private $address;

/**
 * @ORM\Column(type="string",length=10)
 * @Assert\Length("10",groups={"update"})
 */
private $phoneNumber;

/**
 * @ORM\Column(type="string",length=10)
 * @Assert\NotBlank(groups={"update"})
 * @Assert\Length("10",groups={"update"})
 */
private $idNumber;

/**
 * @ORM\OneToMany(targetEntity="App\Entity\Vehicle", mappedBy="user")
 */
private $vehicle;

/**
 * @ORM\OneToOne(targetEntity="App\Entity\Account", inversedBy="user")
 */
private $account;


public function __construct()
{
    $this->vehicle = new ArrayCollection();
}

public function getId(): ?int
{
    return $this->id;
}

public function getEmail()
{
    return $this->email;
}

public function setEmail( $email): self
{
    $this->email = $email;

    return $this;
}

/**
 * A visual identifier that represents this user.
 *
 * @see UserInterface
 */
public function getUsername()
{
    return (string) $this->email;
}

public function getRoles(): ?array
{
    return $this->roles;
}

public function setRoles(array $roles): self
{
    $this->roles = $roles;

    return $this;
}

public function getPassword()
{
    return $this->password;
}

public function setPassword($password): self
{
    $this->password = $password;

    return $this;
}

/**
 * @see UserInterface
 */
public function getSalt()
{
    // not needed when using the "bcrypt" algorithm in security.yaml
}

/**
 * @see UserInterface
 */
public function eraseCredentials()
{
    // If you store any temporary, sensitive data on the user, clear it here
    // $this->plainPassword = null;
}

public function getFirstName()
{
    return $this->firstName;
}

public function setFirstName( $firstName): self
{
    $this->firstName = $firstName;

    return $this;
}

public function getLastName()
{
    return $this->lastName;
}

public function setLastName( $lastName): self
{
    $this->lastName = $lastName;

    return $this;
}

public function getImage(): ?string
{
    return $this->image;
}

public function setImage(string $image): self
{
    $this->image = $image;

    return $this;
}

public function getAddress()
{
    return $this->address;
}

public function setAddress( $address): self
{
    $this->address = $address;

    return $this;
}

public function getIdNumber()
{
    return $this->idNumber;
}

public function setIdNumber( $idNumber): self
{
    $this->idNumber = $idNumber;

    return $this;
}

public function getPhoneNumber()
{
    return $this->phoneNumber;
}

public function setPhoneNumber( $phoneNumber): self
{
    $this->phoneNumber = $phoneNumber;

    return $this;
}

/**
 * @return Collection|Vehicle[]
 */
public function getVehicle(): Collection
{
    return $this->vehicle;
}

public function addVehicle(Vehicle $vehicle): self
{
    if (!$this->vehicle->contains($vehicle)) {
        $this->vehicle[] = $vehicle;
        $vehicle->setUser($this);
    }

    return $this;
}

public function removeVehicle(Vehicle $vehicle): self
{
    if ($this->vehicle->contains($vehicle)) {
        $this->vehicle->removeElement($vehicle);
        // set the owning side to null (unless already changed)
        if ($vehicle->getUser() === $this) {
            $vehicle->setUser(null);
        }
    }

    return $this;
}

public function getAccount(): ?Account
{
    return $this->account;
}

public function setAccount(?Account $account): self
{
    $this->account = $account;

    return $this;
}

} }

Assuming you are using the default voter and entity security user provider. 假设您正在使用默认的投票者和实体安全用户提供程序。

This should apply for Symfony 3.4+, but knowing which version of Symfony you are using, would grant other approaches. 这应该适用于Symfony 3.4+,但是知道您使用的是哪个版本的Symfony,将可以使用其他方法。

At the end of every request (unless your firewall is stateless), your User object is serialized to the session. 在每个请求结束时(除非您的防火墙是无状态的),您的User对象都被序列化到会话。 At the beginning of the next request, it's deserialized and then passed to your user provider to "refresh" it (eg Doctrine queries for a fresh user). 在下一个请求的开始,它会反序列化,然后传递给您的用户提供程序以“刷新”它(例如,对于新用户的Doctrine查询)。

Then, the two User objects (the original from the session and the refreshed User object) are "compared" to see if they are "equal". 然后,将两个“用户”对象(会话中的原始对象和刷新的“用户”对象)进行“比较”,以查看它们是否“相等”。 By default, the core AbstractToken class compares the return values of the getPassword(), getSalt() and getUsername() methods. 默认情况下,核心AbstractToken类比较getPassword(),getSalt()和getUsername()方法的返回值。 If any of these are different, your user will be logged out. 如果其中任何一个不同,您的用户将被注销。 This is a security measure to make sure that malicious users can be de-authenticated if core user data changes. 这是一种安全措施,可确保如果核心用户数据发生更改,则可以取消对恶意用户的身份验证。

However, in some cases, this process can cause unexpected authentication problems. 但是,在某些情况下,此过程可能会导致意外的身份验证问题。 If you're having problems authenticating, it could be that you are authenticating successfully, but you immediately lose authentication after the first redirect. 如果您在身份验证时遇到问题,则可能是您已成功进行身份验证,但在第一次重定向后立即失去了身份验证。

Source: https://symfony.com/doc/current/security/user_provider.html#understanding-how-users-are-refreshed-from-the-session 资料来源: https : //symfony.com/doc/current/security/user_provider.html#understanding-how-users-are-refreshed-from-the-session

The issue appears to be caused by 该问题似乎是由于

$user->setPassword($passwordEncoder->encodePassword($user,$user->getPassword()));

Which will generate a new hashed password from the submitted password, invalidating the user state, even if it is identical. 即使提交的密码相同,也会从提交的密码中生成新的哈希密码,从而使用户状态无效。

You would need to store the user's plain-text password, and validate if it has changed, and apply the password changes only if it changed. 您将需要存储用户的纯文本密码,并验证它是否已更改,并且仅在密码更改时应用密码更改。

Additionally your image form setting is not valid, since your User::$image requires a string, but the form will upload a File object (causing an invalid Entity state or calling File::__toString and changing the image). 另外,您的image表单设置无效,因为您的User::$image需要一个字符串,但是表单将上传File对象(导致无效的Entity状态或调用File::__toString并更改图片)。 You should use a separate property for the image upload and manually draw the current image in your view or consider using a data transformer in your Form rather than in your controller to handle the state change. 您应该为图像上传使用单独的属性,并在视图中手动绘制当前图像,或者考虑在Form中而不是在控制器中使用数据转换器来处理状态更改。 See: https://symfony.com/doc/current/form/data_transformers.html 参见: https : //symfony.com/doc/current/form/data_transformers.html

Replace your current password and image form fields with the plainPassword and uploadImage fields. 将您当前的passwordimage表单字段替换为plainPassworduploadImage字段。

class RegisterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            //...
            ->add('plainPassword',RepeatedType::class,[
                'type' => PasswordType::class,
                'invalid_message' => 'The password fields must match.',
                'required' => false,
                'options' => ['attr' => ['class' => 'password-field']],
                'first_options'  => ['label' => 'Password','attr'=>['placeholder'=>"Password"]],
                'second_options' => ['label' => 'Confirm Password','attr'=>['placeholder'=>"Confirm Password"]],
            ])
            ->add('uploadImage',FileType::class,['label'=>'Photo','required'=>false,'attr'=>['hidden'=>"hidden", 'accept'=>"image/jpeg, image/png"]]);
      //...
}

You should also seriously consider using a DTO , instead of the direct User entity from Doctrine to manage your data, to prevent an invalid entity state. 您还应该认真考虑使用DTO代替Doctrine的直接User实体来管理数据,以防止出现无效的实体状态。

Then create the properties and getter/setter methods in you User entity, to store the form values. 然后在用户实体中创建属性和getter / setter方法,以存储表单值。

class User implements UserInterface
{
      /**
       * @var string
       */
      private $plainPassword = '';

      /**
       * @var File|null
       */
      private $uploadImage;


      public function getPlainPassword(): string
      {
          return $this->plainPassword;
      }

      public function setPlainPassword(string $plainPassword): void
      {
          $this->plainPassword = $plainPassword;
      }

      /**
       * @see UserInterface
       */
      public function eraseCredentials()
      {
          $this->plainPassword = null;
      }

      public function getUploadImage(): ?File
      {
          return $this->uploadImage;
      }

      public function setUploadImage(?File $file): void
      {
          $this->uploadImage = $file;
      }

      //...
}

Since you're using the Entity manager and the RegisterType field, you can remove the manual update queries. 由于您使用的是实体管理器和RegisterType字段,因此可以删除手动更新查询。 Since the $form->handleRequest() will be applying the changes directly to the User object. 因为$form->handleRequest()会将更改直接应用到User对象。 I also suggest using the Paramconverter to benefit from the entity Dependency Injection for the User object. 我还建议使用Paramconverter来受益于User对象的实体依赖注入。

/**
 * @Route("/{user}/update", name="update", requirements={ "user":"\d+" }, methods={"GET","POST"})
 * @param User $user
 * @param Request $request
 * @param UserPasswordEncoderInterface $passwordEncoder
 * @param UserInterface $loggedUser
 * @return Response
 */
public function updateUser(User $user, Request $request, UserPasswordEncoderInterface $passwordEncoder, UrlGeneratorInterface $urlGenerator): Response
{    
     $loggedinUser = $this->getUser(); //helper from ControllerTrait
     if ($loggedinUser && loggedinUser->getId() === $user->getId()) {
         $form = $this->createForm(RegisterType::class,$user, [
            'validation_groups' => ['update'],
         ]);
         $currentImage = $user->getImage();
         $form->handleRequest($request);
         if ($form->isSubmitted() && $form->isValid()) {
             if ($file = $user->getUploadImage()) {
                 //this logic should be moved to the Form using a data transformer
                 $fileName = md5(uniqid()).'.'.$file->guessExtension();
                 $file->move(
                     $this->getParameter('uploads_dir'), $fileName
                 );
                 $user->setImage($fileName);
             }
             if ('' !== $user->getPlainPassword() && !$passwordEncoder->isPasswordValid($user->getPassword(), $user->getPlainPassword())) {
                 //change password only when changed
                 $user->setPassword($passwordEncoder->encodePassword($user, $user->getPlainPassword()));
                 $user->eraseCredentials();
             }
             $em = $this->getDoctrine()->getManager();
             $em->flush();

             return new RedirectResponse($urlGenerator->generate('home'));
         }

         return $this->render('register/update.html.twig', [
              'form'=>$form->createView(),
        ]);
    }

    return new RedirectResponse($urlGenerator->generate('home'));
}

If you are using Symfony < 4.1, you will need to implement \\Serializable and add the serialize and unserialize methods to your User class, otherwise your entire User object will be serialized and invalidated on any change. 如果您使用的是Symfony <4.1,则需要实现\\Serializable ,并将serialize和反unserialize方法添加到User类,否则整个User对象将被序列化,并且在进行任何更改时都将无效。

class User implements UserInterface, \Serializable
{
   //... 

    /** @see \Serializable::serialize() */
    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            //$this->roles //(optional)
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            //$this->roles //(optional)
        ) = unserialize($serialized, array('allowed_classes' => false));
    }
}

Use a temporary field to hold the plaintext password in the form pre-encoding/pre-hashing (see: https://symfony.com/doc/4.0/doctrine/registration_form.html#registration-password-max - field is called plainPassword or similar). 使用临时字段以预编码/预散列的形式保存纯文本密码(请参阅: https ://symfony.com/doc/4.0/doctrine/registration_form.html#registration-password-max-该字段称为plainPassword或类似)。

I suspect that there is some unexpected behaviour when setting an empty password, which might invalidate session cache (symfony stores some user data to determine, if the user must be reloaded from database, and if relevant data changed, user might be logged out). 我怀疑设置空密码时会出现一些意外行为,这可能会使会话缓存无效(symfony存储一些用户数据来确定是否必须从数据库中重新加载用户,并且如果相关数据已更改,则用户可能会注销)。 The redirection alone should definitely not logout a user. 仅重定向绝对不应注销用户。

Hopefully, this will be enough. 希望这足够了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM