
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; class Role implements RoleInterface { protected $id; protected $name; protected $createdAt; public function getId() { return $this->id; } public function getName() { return $this->name; } public function setName($value) { $this->name = $value; } public function getCreatedAt() { return $this->createdAt; } public function __construct() { $this->createdAt = new \DateTime(); } 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; class User implements UserInterface {
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) {
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!