📜 ⬆️ ⬇️

Symfony2 \ SecurityBundle

Authentication and Authorization The author talks about the Security bundle device (in my opinion, the most difficult thing to understand for symfony novices) and examines an example of its use. The article will be especially useful for those who want to know how their tool works: Symfony2 Security in general and FOSUserBundle in particular - however, it is not suitable for first acquaintance with the framework, because it requires knowledge of some of its components.
The article was published on March 21, 2011, when the final version of symfony2.0 was not yet released, but the principles of the bundle did not change.

The original article, Symfony2 Blog Application Tutorial Part V: Intro to Security, is part of a series of tutorial articles on the example of creating a blog.
There is a direct continuation / addition of the article - “Symfony2 Blog Application Tutorial Part V-2: Testing Secure Pages” , which gives an example of testing the “closed” parts of the application.


Article text


The Security component in symfony2 is very powerful and complex. The example given below will be simple, but it should not be difficult for you to finish it to fit your needs. In production versions of applications it is strongly recommended to use the FOSUserBundle bundle, which can be found here . Its authors are among the developers of the Symfony2 core, and most likely the bundle will become something like “sfGuardPlugin”, only for the second version of the framework. Those familiar with symfony1 will understand me. Since people have problems with the bundle itself, I’ll skip writing tests to speed up the process. Maybe I'll write them later and update the post. [ Update: I did it, see here .]
')
So, our goal is to require anyone who tries to go to the address starting with "/ admin /" to log in through the form. For this we need to do a few things. First you need to register the SecurityBundle that comes with Symfony2. To do this, open AppKernel.php , located in the app directory, find the registerBundles method and add the following to the array with bundles:

new Symfony\Bundle\SecurityBundle\SecurityBundle() 

Now that the bundle is registered, you can get down to business, but before you start writing code, you need to understand the principles of how the Security component works in Symfony2. Conventionally, it can be divided into three subcomponents: Users (Users), Authentication (Authentication) and Authorization (Authorization). Users stores information about the user who works with the application. Authentication checks to see if the user is who they are. Authorization allows and prohibits an authenticated user from performing certain actions, for example, viewing any information, editing it, etc.

Now we are ready to configure the security system in our application. To do this, we specify in the Symfony2 configuration that we should use the Company \ BlogBundle \ Entity \ User entity that we created earlier as the User provider, and also determine how to encrypt passwords, which parts of the application should be protected and which roles the user should have to access them. Open the config.yml file from the app / config directory and add the following:

 ## Security Configuration security: encoders: Company\BlogBundle\Entity\User: algorithm: sha512 encode-as-base64: true iterations: 10 providers: main: entity: { class: BlogBundle:User, property: username } firewalls: main: pattern: /.* form_login: check_path: /login_check login_path: /login logout: true security: true anonymous: true access_control: - { path: /admin/.*, role: ROLE_ADMIN } - { path: /.*, role: IS_AUTHENTICATED_ANONYMOUSLY } 

Let us analyze each of the sections of this file. In encoders, it is determined how user passwords will be encrypted for the Company \ BlogBundle \ Entity \ User entity . We will use the MessageDigestPasswordEncoder supplied with Symfony2, but, of course, you can write your own encryptor. (Note lane - the MessageDigestPasswordEncoder class is located in vendor / symfony / src / Symfony / Component / Security / Core / Encoder.) The following indicates that 10 passes through sha512 should be used as encryption and the result should be represented as a base64 string. For more information about encoders, see here .

The providers section defines where users should be retrieved from. We selected the Doctrine Entity Provider and for this we specified the entity parameter. Other allowed providers are In-Memory Provider and Chain Provider , which you can read about here . In the entity line, you must specify the class and property properties. Class points to a class with an entity representing the user. In the example, this is the User class from the BlogBundle bundle. Property is the name of a column of a table containing user names, and in this case, username.

Authorization in Symfony2 is implemented through the Firewall system. It consists of "listeners" (English listeners) who expect the core.security event and redirect the request, taking into account what rights the user has. In the firewalls section, the route patterns (eng. Routes) are defined, on which it is necessary to hang “listeners” (eng. Listeners). Today, to protect the application, it is recommended to specify one firewall that serves all routes, and then use the access_control section to allow or deny access depending on user roles. In our example, in the pattern field of the firewalls section it is determined that it is necessary to “listen” to all routes. The form_login field indicates that authentication will take place through the form. For other authentication methods, see here . Inside form_login, we have specified routes for the login_path (the location of the login form) and check_path (the location of the form handler). In addition, form_login has many other settings that you can read about here .

Finally, the last section is access_control . Entries in it define the route patterns and roles needed to access them. In our example, for routes that begin with "/ admin /", the role ROLE_ADMIN is required, for all others IS_AUTHENTICATED_ANONYMOUSLY is enough (in Symfony2, all users have this role by default).

So, now that the security configuration file has been updated, we need to change the classes of our entity so that they implement the interfaces that SecurityBundle requires. In our example, we need to add the UserInterface interface implementation to the User entity . You also need to create a Role class that implements the RoleInterface interface. After that, to upload new information to the database, we need to change our fixtures.

Create a Role.php file in the src / Company / BlogBundle / Entity directory with the following contents:

 namespace Company\BlogBundle\Entity; use Symfony\Component\Security\Core\Role\RoleInterface; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="role") */ class Role implements RoleInterface { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") * * @var integer $id */ protected $id; /** * @ORM\Column(type="string", length="255") * * @var string $name */ protected $name; /** * @ORM\Column(type="datetime", name="created_at") * * @var DateTime $createdAt */ protected $createdAt; /** *   id. * * @return integer The id. */ public function getId() { return $this->id; } /** *    . * * @return string The name. */ public function getName() { return $this->name; } /** *    . * * @param string $value The name. */ public function setName($value) { $this->name = $value; } /** *     . * * @return DateTime A DateTime object. */ public function getCreatedAt() { return $this->createdAt; } /** *   */ public function __construct() { $this->createdAt = new \DateTime(); } /** *  ,   RoleInterface. * * @return string The role. */ public function getRole() { return $this->getName(); } } 

The essence of Role is pretty simple. It has only one property - name , which contains the name of the role. The class also implements the RoleInterface interface through the support for the getRole method, which returns the name of the role. Now, after creating the Role class, you need to tweak the User entity. Open the file User.php from
src / Company / BlogBundle / Entity and change the code as follows:

 namespace Company\BlogBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Entity * @ORM\Table(name="user") */ class User implements UserInterface { // ... /** * @ORM\Column(type="string", length="255") * * @var string username */ protected $username; /** * @ORM\Column(type="string", length="255") * * @var string password */ protected $password; /** * @ORM\Column(type="string", length="255") * * @var string salt */ protected $salt; /** * @ORM\ManyToMany(targetEntity="Role") * @ORM\JoinTable(name="user_role", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")} * ) * * @var ArrayCollection $userRoles */ protected $userRoles; // ... /** *    . * * @return string The username. */ public function getUsername() { return $this->username; } /** *    . * * @param string $value The username. */ public function setUsername($value) { $this->username = $value; } /** *   . * * @return string The password. */ public function getPassword() { return $this->password; } /** *   . * * @param string $value The password. */ public function setPassword($value) { $this->password = $value; } /** *     . * * @return string The salt. */ public function getSalt() { return $this->salt; } /** *     . * * @param string $value The salt. */ public function setSalt($value) { $this->salt = $value; } /** *    . * * @return ArrayCollection A Doctrine ArrayCollection */ public function getUserRoles() { return $this->userRoles; } /** *   User */ public function __construct() { $this->posts = new ArrayCollection(); $this->userRoles = new ArrayCollection(); $this->createdAt = new \DateTime(); } /** *   . */ public function eraseCredentials() { } /** *    . * * @return array An array of Role objects */ public function getRoles() { return $this->getUserRoles()->toArray(); } /** *        *       . * * @param UserInterface $user The user * @return boolean True if equal, false othwerwise. */ public function equals(UserInterface $user) { return md5($this->getUsername()) == md5($user->getUsername()); } // ... } 

Those interested can download this class here . As you can see, in the essence of User, there is nothing particularly complicated, just the implementation of the UserInterface interface and the creation of a many-to-many relationship with the Role entity. There is also the AdvancedUserInterface interface, which provides more functionality, but its consideration is beyond the scope of this article. You can read more about it here .

Now you need to change the fixtures, with which we can update the information in the database. Open the FixtureLoader.php file from the src / Company / BlogBundle / DataFixtures / ORM directory and make the following changes:

 namespace Company\BlogBundle\DataFixtures\ORM; use Doctrine\Common\DataFixtures\FixtureInterface; use Company\BlogBundle\Entity\Category; use Company\BlogBundle\Entity\Post; use Company\BlogBundle\Entity\Tag; use Company\BlogBundle\Entity\User; use Company\BlogBundle\Entity\Role; use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; class FixtureLoader implements FixtureInterface { public function load($manager) { //   ROLE_ADMIN $role = new Role(); $role->setName('ROLE_ADMIN'); $manager->persist($role); //   $user = new User(); $user->setFirstName('John'); $user->setLastName('Doe'); $user->setEmail('john@example.com'); $user->setUsername('john.doe'); $user->setSalt(md5(time())); //      , //       $encoder = new MessageDigestPasswordEncoder('sha512', true, 10); $password = $encoder->encodePassword('admin', $user->getSalt()); $user->setPassword($password); $user->getUserRoles()->add($role); $manager->persist($user); // ... } 

Thus, a new role was created with the name ROLE_ADMIN. A user has also been added with the username john.doe and an arbitrary password salt. The only new functionality here is password encryption. If you remember, in the security.encoders section of the configuration file we set up the use of MessageDigestPasswordEncoder . Here we create an instance of this class and pass the same parameters into it as specified in security.encoders , then encrypt the admin password and set the result as the new password of the user being created. When encrypting a password, you need to specify the same parameters as in the Security component configuration, because otherwise we will not be able to authenticate correctly.

Before updating the routing and creating new controllers and views, you need to run several commands in the console. Open the terminal and go to the root directory of our project. First, we update the database structure to match the entities.

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


Next, clear the database and reload the information using the updated fixtures.

 php app/console doctrine:data:load 

At this stage we have installed and updated entities, data and security configuration. Now let's proceed to changing the routing and creating several controllers and views to implement the login form. Open the routing.yml file from the src / Company / BlogBundle / Resources / config directory and add the following routes to its beginning :

 _security_login: pattern: /login defaults: { _controller: BlogBundle:Security:login } _security_check: pattern: /login_check _security_logout: pattern: /logout admin_home: pattern: /admin/ defaults: { _controller: BlogBundle:Admin:index } 

By this we have added several special routes that will be used both by us and by the Security component. You may have wondered why we did not define controllers for the two routes. Remember that I wrote about the work of the Security component, waiting for the core.security event? From this it follows that the controllers for these routes will never be executed, because the Security component intercepts requests and processes them itself. Therefore, we can easily use the _security_check and _security_logout routes in our templates. The admin_home route has also been added, which is the main page of the admin section of our application. Since we specified in the security.access_control configuration section that only users with the ROLE_ADMIN role can access routes starting with "/ admin /", the admin_home route is protected. Soon we will try to enter it.

You probably noticed that we need to create two new controllers - SecurityController and AdminController , so let's get started. Create a file in the src / Company / BlogBundle / Controller directory with the name AdminController.php and the following content:

 namespace Company\BlogBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class AdminController extends Controller { public function indexAction() { return $this->render('BlogBundle:Admin:index.html.twig'); } } 

Nothing unusual. Just processing the template index.html.twig , which we will now describe. To do this, create a new Admin folder in the src / Company / BlogBunde / Resources / views directory, inside which we will create the index.html.twig file - the template for the main page of the admin section.

 {% extends "BlogBundle::layout.html.twig" %} {% block title %} symfony2  |  |  {% endblock %} {% block content %} <h2>     , {{ app.user.username }}! </h2> {% endblock %} 

The only thing you should pay attention to is the variable template app.user , which allows you to access the current user in the application. So, in our example, the app.user variable corresponds to an instance of the Company \ BlogBundle \ Entity \ User class.

Now that we have secure pages, let's create a SecurityController controller. To do this, create a SecurityController.php file in the src / Company / BlogBundle / Controller directory with the following content:

 namespace Company\BlogBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Security\Core\SecurityContext; class SecurityController extends Controller { public function loginAction() { if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR); } else { $error = $this->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR); } return $this->render('BlogBundle:Security:login.html.twig', array( 'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME), 'error' => $error )); } } 

We created a loginAction action that corresponds to the _security_login route. Inside it is checked for errors and then the login.html.twig template is given as an answer. Checking for errors may seem a bit strange, but in fact we are only trying to find out how we got into this action - by direct link or were redirected. Depending on this, we get the generated exception.

The Security component assumes all verification of user credentials, however we must create a template that accepts certain parameters. In order for the Security component to validate the data correctly, it is necessary that the form has the _username and _password fields and is also bound to the _security_check route. Let's do this by creating a Security folder in the src / Company / BlogBunde / Resources / views directory, in which, in turn, we will create a login.html.twig file with the following content:

 {% extends "BlogBundle::layout.html.twig" %} {% block title %} symfony2  |  {% endblock %} {% block content %} {% if error %} <div class="error">{{ error.message }}</div> {% endif %} <form action="{{ path('_security_check') }}" method="POST"> <table> <tr> <td> <label for="username">:</label> </td> <td> <input type="text" id="username" name="_username" value="{{ last_username }}" /> </td> </tr> <tr> <td> <label for="password">:</label> </td> <td> <input type="password" id="password" name="_password" /> </td> </tr> </table> <input type="submit" name="login" value="" /> </form> {% endblock %} 

Everything should be clear here. When sending data from a form on the _security_check route, the Security component will intercept the request and authenticate itself. After successful authentication, the user will be redirected to the original address, otherwise - to the page with the login form.

Now let's create a "Logout" link that will only be shown to logged in users. Open the file layout.html.twig in the src / Company / BlogBundle / Resources / views directory and make the following changes:

 // ... {% block body %} <div id="container"> <header class="clearfix"> <h1> symfony2  </h1> <nav> <ul> <li> <a href="{{ path('show_page', { 'page' : 'about' }) }}">   </a> </li> {% if is_granted('IS_AUTHENTICATED_FULLY') %} <li> <a href="{{ path('_security_logout') }}">  </a> </li> {% endif %} </ul> </nav> </header> // ... 

We used the twig is_granted function to test the user for a particular role. In our example, this is the IS_AUTHENTICATED_FULLY role — a special role assigned to users authenticated by the Security component. If the user has this role, then add a link to the _security_logout route created above in the navigation menu.

Now, in principle, we can test our secure page, but before that, let's clear the cache. For this there is a console command:

 php app/console cache:clear 

Now try to login to "/ admin /". You should be redirected to a page with a login form that looks something like this (click to enlarge):



Enter john.doe in the "Login" field and admin in the "Password" field. After you send the credentials, you should be redirected to the main page of the administrator section, which looks something like this (click to enlarge):



Also in the menu with navigation should appear link "Exit". If you go through it, then we will be transferred to the main page of the application, and at the same time we will break up. Fuh! You did a great job! For my part, I put a lot of effort into this article and went a long way of trial and error! As a result, we have a protected section of the application and the corresponding route prefix (comment. - "/ admin / *"). Once again, in production versions of applications, it is strongly recommended to use the FOSUserBundle, which is much more functional than what we have created in this article. I hope that everything was written clearly, but if something still needs additional explanations, then let me know.

From translator


Under this topic, it is useful to read the Security section of official documentation. The articles from the Cookbook: Access Control Lists (ACLs) and Advanced ACL Concepts deserve special attention.

I collect information about what was or is not clear in Symfony2 for you personally, as well as links to good articles about Symfony2.0 (preferably written after the release of the framework). We will raise the rating of habrablog Symfony Frameworks together!

The book of complaints and suggestions for the translation itself is here - pluseg . I will be glad to any review!

Source: https://habr.com/ru/post/128159/


All Articles