Monthly Archives: Mayıs 2014

Genel

Symfony2: Kullanıcı Doğrulama İşleminin Veritabanı Üzerinden Gerçekleştirilmesi

Kullanıcı doğrulama ve yetkilendirme ile ilgili edindiğim ufak tecrübeleri özet halinde sizlerle paylaşmak istedim.

Burada anlatacaklarımın tamamı tüm açılığı ile symfony2 belgeleri arasında yeralmaktadır.

Composer ile Symfony2 standart paketi kurduğunuzda güvenlik yapılandırma ayarlarını içeren app/config/security.yml dosyasındaki security bölümünde, aşağıdaki gibi bir yapılandırma ile karşılaşırsınız:

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
...   

Gelin şimdi kısım kısım yukarıdaki güvenlik yapılandırmasını inceleyelim.

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

Yukarıdaki yapılandırma, symfony temel kullanıcı implementasyonu olan Symfony\Component\Security\Core\User\User için, parola kodlama metodolojisinin düz metin şeklinde olacağını sölyüyor.

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

Bu kısımda ise kullanıcıların sahip olabileceği roller ve bu roler arasındaki hiyerarşi tanımlaması gerçekleştirilmiş.

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

Bu kısım, symfony2’nin oturum açma işlemi sırasında kullanıcı bilgilerini temin edebileceği sağlayıcılar ile ilgili tanımlamaları içeriyor. Yukarıdaki yapılandırma bize, in_memory isimli memory tipindeki sağlayıcının, users listesindeki kullanıcıları içerdiğini söylüyor. Sağlayıcının memory tipinde olması ise kullanıcı doğrulama işleminin konfigürasyondaki users listesinde yeralan kullanıcıların bilgileri doğrultusunda gerçekleştirileceği anlamına gelir.

Symfony, size birden fazla sağlayıcı ile çalışabilme olanağını sunar. İlk sağlayıcı her zaman varsayılan olarak kabul edilir.

Symfony2’nin kullanıcı doğrulama işlemini veritabanındaki kayıtları kullanarak sağlayabilmesi için, Doctrine üzerinden veritabanına ulaşabilen kullanıcı tanımlı yeni bir sağlayıcı geliştirilmesi gerekmektedir. Dilerseniz hemen aşağıdaki işlem adımlarını birlikte uygulayarak kendi sağlayıcımızı geliştirelim.

1. Kullanıcı kaydını temsil edecek User isimli yeni doctrine entity sınıfını oluşturalım.
src/Acme/DemoBundle/Entity/User.php:

roles = new \Doctrine\Common\Collections\ArrayCollection();
        $this->isActive = true;
        $this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16,
            36);
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username
     *
     * @param string $username
     * @return User
     */
    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

    /**
     * Get username
     *
     * @return string 
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * Set email
     *
     * @param string $email
     * @return User
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

    /**
     * Get email
     *
     * @return string 
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Set password
     *
     * @param string $password
     * @return User
     */
    public function setPassword($password)
    {
        $this->password = $password;

        return $this;
    }

    /**
     * Get password
     *
     * @return string 
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * Set fullname
     *
     * @param string $fullname
     * @return User
     */
    public function setFullname($fullname)
    {
        $this->fullname = $fullname;

        return $this;
    }

    /**
     * Get fullname
     *
     * @return string 
     */
    public function getFullname()
    {
        return $this->fullname;
    }

    /**
     * Set isActive
     *
     * @param boolean $isActive
     * @return User
     */
    public function setIsActive($isActive)
    {
        $this->isActive = $isActive;

        return $this;
    }

    /**
     * Get isActive
     *
     * @return boolean 
     */
    public function getIsActive()
    {
        return $this->isActive;
    }

    /**
     * Set salt
     *
     * @param string $salt
     * @return User
     */
    public function setSalt($salt)
    {
        $this->salt = $salt;

        return $this;
    }

    /**
     * Get salt
     *
     * @return string 
     */
    public function getSalt()
    {
        return $this->salt;
    }

    /**
     * Add roles
     *
     * @param \Acme\DemoBundle\Entity\Role $roles
     * @return User
     */
    public function addRole(\Acme\DemoBundle\Entity\Role $roles)
    {
        $this->roles[] = $roles;

        return $this;
    }

    /**
     * Remove roles
     *
     * @param \Acme\DemoBundle\Entity\Role $roles
     */
    public function removeRole(\Acme\DemoBundle\Entity\Role $roles)
    {
        $this->roles->removeElement($roles);
    }

    /**
     * Get roles
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getRoles()
    {
        return $this->roles->toArray();
    }

    /**
     * Removes sensitive data from the user.
     *
     * This is important if, at any given point, sensitive information like
     * the plain-text password is stored on this object.
     */
    public function eraseCredentials()
    {
        // TODO: Implement eraseCredentials() method.
    }

    /**
     * (PHP 5 >= 5.1.0)
* String representation of object * @link http://php.net/manual/en/serializable.serialize.php * @return string the string representation of the object or null */ public function serialize() { return serialize(array( $this->id, $this->username, $this->password, // see section on salt below $this->salt, )); } /** * (PHP 5 >= 5.1.0)
* Constructs the object * @link http://php.net/manual/en/serializable.unserialize.php * @param string $serialized

* The string representation of the object. *

* @return void */ public function unserialize($serialized) { list ( $this->id, $this->username, $this->password, // see section on salt below $this->salt ) = unserialize($serialized); } }

2. Kullanıcının rolünü temsil edecek Role isimli yeni bir doctrine entitiy sınıfı oluşturalım.
app/src/Acme/DemoBundle/Entity/Role.php:

id;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return Role
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }
    /**
     * Constructor
     */
    public function __construct()
    {
        $this->users = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Set role
     *
     * @param string $role
     * @return Role
     */
    public function setRole($role)
    {
        $this->role = $role;

        return $this;
    }

    /**
     * Get role
     *
     * @return string 
     */
    public function getRole()
    {
        return $this->role;
    }

    /**
     * Add users
     *
     * @param \Acme\DemoBundle\Entity\User $users
     * @return Role
     */
    public function addUser(\Acme\DemoBundle\Entity\User $users)
    {
        $this->users[] = $users;

        return $this;
    }

    /**
     * Remove users
     *
     * @param \Acme\DemoBundle\Entity\User $users
     */
    public function removeUser(\Acme\DemoBundle\Entity\User $users)
    {
        $this->users->removeElement($users);
    }

    /**
     * Get users
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getUsers()
    {
        return $this->users;
    }
}

3. Symfony nin sağlayıcı olarak kabul edeceği, güvenlik bileşeninin veritabanındaki kullanıcı listesine ulaşmasını sağlayak yeni bir Doctrine deposunu tanımlayalım.
Acme/DemoBundle/Repository/UserRepository.php:

createQueryBuilder("u")
                ->select("u, r")
                ->leftJoin("u.roles", "r")
                ->where("u.username = :username OR u.email = :email")
                ->setParameter("username", $username)
                ->setParameter("email", $username)
                ->getQuery();
        try {
            $user = $query->getSingleResult();
        } catch (NoResultException $e) {
            $message = sprintf("Unable to find the specified user: %s", $username);
            throw new UsernameNotFoundException($message, 0, $e);
        } catch (BadCredentialsException $e) {
            $message = sprintf("Unable to find the specified user: %s", $username);
            throw new UsernameNotFoundException($message, 0, $e);
        }
        return $user;
    }

    /**
     * Refreshes the user for the account interface.
     *
     * It is up to the implementation to decide if the user data should be
     * totally reloaded (e.g. from the database), or if the UserInterface
     * object can just be merged into some internal array of users / identity
     * map.
     * @param UserInterface $user
     *
     * @return UserInterface
     *
     * @throws UnsupportedUserException if the account is not supported
     */
    public function refreshUser(UserInterface $user)
    {
        $class = get_class($user);
        if(!$this->supportsClass($class)) {
            throw new UnsupportedUserException(
                sprintf(
                    "Instance of %s is not supported",
                    $class
                )
            );
        }
        return $this->find($user->getId());
    }

    /**
     * Whether this provider supports the given user class
     *
     * @param string $class
     *
     * @return Boolean
     */
    public function supportsClass($class)
    {
        return $this->getEntityName() == $class
            || is_subclass_of($class, $this->getEntityName());
    }
}

4. Kullanıcılara ait parolaların veritabanında şifrelenmiş şekilde barındırılabilmesi için app/config/security.yml güvenlik yapılandırma dosyasındaki encoders bölümüne aşağıdaki ayarları girelim. Bu ayarlar, kullanıcı parolalarının veritabanında sha512 algoritması ile şifrelenmesini sağlayacaktır.
app/config/security.yml:

security:
...
    encoders:
...
        Acme\DemoBundle\Entity\User:
          algorithm: sha512
          iterations: 10
...

5. Aynı dosyadaki providers isimli listeye biraz önce oluşturduğumuz doctrine deposunu sağlayıcı olarak tanıtacak ayarları ekleyelim.
app/config/security.yml:

security:
...
    providers:
...
        user_db:
            entity:
                class: AcmeDemoBundle:User
                property: username
...

6. Yine aynı yapılandırma dosyasındaki firewall isimli bölümde oturum açma işleminin yeni oluşturduğumuz sağlayıcı üzerinden gerçekleşmesini sağlayacak ayarları ekleyelim.
app/config/security.yml:

security:
...
    firewalls:
...
        secured_area:
            provider: user_db # bu bölümü ekliyoruz.
            pattern:    ^/.*
            form_login:
                check_path: authenticate
                login_path: login
            logout:
                path:   logout
                target: home
...            

7. Yeni oluşturduğumuz doctrine entitiy sınıfları nedeniyle veritabanı şemamızı güncellemek için aşağıdaki shell komutunu çalıştıralım:

$ php app/console doctrine:schema:update --force

8. Son olarak ROLE_USER ve ROLE_ADMIN isimli kullanıcı rollerini yaratacak aşağıdaki SQL betiğini çalıştıralım.

INSERT INTO acme_demo_role (role, name) VALUES ('ROLE_ADMIN', 'ROLE_ADMIN'), ('ROLE_USER', 'ROLE_USER');

Uygulamamız artık kullanıcı doğrulama işlemini veritabanı üzerinden gerçekleştirmeye hazır. Ancak kullanıcı parolalarının sha512 ile kodlanması gerektiğinden kullanıcı yaratma işlemini yine symfony ye yaptırmak gerekiyor.

Aslına bakarsanız doğrulama ve yetkilendirme işlemlerini yapabilmek adına FosUserBundle gibi çok daha pratik çözümler mevcut. Ancak ben de henüz öğrenme aşamasında olduğum için başlangıçta pratik olarak kullanıcı yaratabilmek adına aşağıdaki gibi bir konsol komutu işleyicisi yazmak durumunda kaldım.
app/src/Acme/DemoBundle/Command/UserCreator.php:

setName('create:user')
            ->setDescription('This ocmmand creates a new user.')
            ->addOption('username', null, InputOption::VALUE_REQUIRED, 'username of user.')
            ->addOption('fullname', null, InputOption::VALUE_REQUIRED, 'Fullname of user.')
            ->addOption('email', null, InputOption::VALUE_REQUIRED, 'Email of user.')
            ->addOption('password', null, InputOption::VALUE_REQUIRED, 'Password of user.')
            ->addOption('role', null, InputOption::VALUE_REQUIRED, 'Role of user.');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {
            $factory = $this->getContainer()->get('security.encoder_factory');
            $user = new User();
            $encoder = $factory->getEncoder($user);
            $user->setSalt(md5(time()));
            $password = $encoder->encodePassword($input->getOption('password'), $user->getSalt());
            $user->setUsername($input->getOption('username'));
            $user->setPassword($password);
            $user->setEmail($input->getOption('email'));
            $user->setFullname($input->getOption('fullname'));
            $user->setIsActive(1);
            $role = $this->getContainer()
                ->get('doctrine')
                ->getRepository('YdCatalogBundle:Role')
                ->findOneBy(array('role' => $input->getOption('role')));
            $user->addRole($role);

            $em = $this->getContainer()->get('doctrine')->getEntityManager();
            $em->persist($user);
            $em->flush();
            $output->writeln('User is created');
        } catch(Exception $e) {
            $output->writeLn('User is not created');
        }
    }
}

Dilerseniz sizler de bu basit komut dosyasını kullanarak veritabanında yeni bir kullanıcı yaratabilirsiniz:

$ php app/console create:user --username=ibrahim.gunduz --password=123456 --email=ibrahimgunduz34@gmail.com --fullname='İbrahim Gündüz' --role=ROLE_ADMIN