安全

身份校验与“防火墙”

You can configure Symfony to authenticate your users using any method you want and to load user information from any source. This is a complex topic, but the Security Cookbook Section has a lot of information about this.

Regardless of your needs, authentication is configured in security.yml, primarily under the firewalls key.

最佳实践

Unless you have two legitimately different authentication systems and users (e.g. form login for the main site and a token system for your API only), we recommend having only one firewall entry with the anonymous key enabled.

Most applications only have one authentication system and one set of users. For this reason, you only need one firewall entry. There are exceptions of course, especially if you have separated web and API sections on your site. But the point is to keep things simple.

Additionally, you should use the anonymous key under your firewall. If you need to require users to be logged in for different sections of your site (or maybe nearly all sections), use the access_control area.

最佳实践

Use the bcrypt encoder for encoding your users’ passwords.

If your users have a password, then we recommend encoding it using the bcrypt encoder, instead of the traditional SHA-512 hashing encoder. The main advantages of bcrypt are the inclusion of a salt value to protect against rainbow table attacks, and its adaptive nature, which allows to make it slower to remain resistant to brute-force search attacks.

With this in mind, here is the authentication setup from our application, which uses a login form to load users from the database:

# app/config/security.yml
security:
    encoders:
        AppBundle\Entity\User: bcrypt

    providers:
        database_users:
            entity: { class: AppBundle:User, property: username }

    firewalls:
        secured_area:
            pattern: ^/
            anonymous: true
            form_login:
                check_path: security_login_check
                login_path: security_login_form

            logout:
                path: security_logout
                target: homepage

# ... access_control exists, but is not shown here

小技巧

The source code for our project contains comments that explain each part.

权限

Symfony gives you several ways to enforce authorization, including the access_control configuration in security.yml, the @Security annotation and using isGranted on the security.authorization_checker service directly.

最佳实践

  • For protecting broad URL patterns, use access_control;
  • Whenever possible, use the @Security annotation;
  • Check security directly on the security.authorization_checker service whenever you have a more complex situation.

There are also different ways to centralize your authorization logic, like with a custom security voter or with ACL.

最佳实践

  • For fine-grained restrictions, define a custom security voter;
  • For restricting access to any object by any user via an admin interface, use the Symfony ACL.

@Security注解

For controlling access on a controller-by-controller basis, use the @Security annotation whenever possible. It’s easy to read and is placed consistently above each action.

In our application, you need the ROLE_ADMIN in order to create a new post. Using @Security, this looks like:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
// ...

/**
 * Displays a form to create a new Post entity.
 *
 * @Route("/new", name="admin_post_new")
 * @Security("has_role('ROLE_ADMIN')")
 */
public function newAction()
{
    // ...
}

使用表达式来实现复杂的访问控制

If your security logic is a little bit more complex, you can use an expression inside @Security. In the following example, a user can only access the controller if their email matches the value returned by the getAuthorEmail method on the Post object:

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("user.getEmail() == post.getAuthorEmail()")
 */
public function editAction(Post $post)
{
    // ...
}

Notice that this requires the use of the ParamConverter, which automatically queries for the Post object and puts it on the $post argument. This is what makes it possible to use the post variable in the expression.

This has one major drawback: an expression in an annotation cannot easily be reused in other parts of the application. Imagine that you want to add a link in a template that will only be seen by authors. Right now you’ll need to repeat the expression code using Twig syntax:

{% if app.user and app.user.email == post.authorEmail %}
    <a href=""> ... </a>
{% endif %}

The easiest solution - if your logic is simple enough - is to add a new method to the Post entity that checks if a given user is its author:

// src/AppBundle/Entity/Post.php
// ...

class Post
{
    // ...

    /**
     * Is the given User the author of this Post?
     *
     * @return bool
     */
    public function isAuthor(User $user = null)
    {
        return $user && $user->getEmail() == $this->getAuthorEmail();
    }
}

Now you can reuse this method both in the template and in the security expression:

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("post.isAuthor(user)")
 */
public function editAction(Post $post)
{
    // ...
}
{% if post.isAuthor(app.user) %}
    <a href=""> ... </a>
{% endif %}

不使用@Security注解来检查权限

The above example with @Security only works because we’re using the ParamConverter, which gives the expression access to the a post variable. If you don’t use this, or have some other more advanced use-case, you can always do the same security check in PHP:

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = $this->getDoctrine()->getRepository('AppBundle:Post')
        ->find($id);

    if (!$post) {
        throw $this->createNotFoundException();
    }

    if (!$post->isAuthor($this->getUser())) {
        throw $this->createAccessDeniedException();
    }

    // ...
}

Security Voters

If your security logic is complex and can’t be centralized into a method like isAuthor(), you should leverage custom voters. These are an order of magnitude easier than ACLs and will give you the flexibility you need in almost all cases.

First, create a voter class. The following example shows a voter that implements the same getAuthorEmail logic you used above:

namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
use Symfony\Component\Security\Core\User\UserInterface;

// AbstractVoter class requires Symfony 2.6 or higher version
class PostVoter extends AbstractVoter
{
    const CREATE = 'create';
    const EDIT   = 'edit';

    protected function getSupportedAttributes()
    {
        return array(self::CREATE, self::EDIT);
    }

    protected function getSupportedClasses()
    {
        return array('AppBundle\Entity\Post');
    }

    protected function isGranted($attribute, $post, $user = null)
    {
        if (!$user instanceof UserInterface) {
            return false;
        }

        if ($attribute === self::CREATE && in_array('ROLE_ADMIN', $user->getRoles(), true)) {
            return true;
        }

        if ($attribute === self::EDIT && $user->getEmail() === $post->getAuthorEmail()) {
            return true;
        }

        return false;
    }
}

To enable the security voter in the application, define a new service:

# app/config/services.yml
services:
    # ...
    post_voter:
        class:      AppBundle\Security\PostVoter
        public:     false
        tags:
           - { name: security.voter }

Now, you can use the voter with the @Security annotation:

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("is_granted('edit', post)")
 */
public function editAction(Post $post)
{
    // ...
}

You can also use this directly with the security.authorization_checker service or via the even easier shortcut in a controller:

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = // query for the post ...

    $this->denyAccessUnlessGranted('edit', $post);

    // or without the shortcut:
    //
    // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
    //    throw $this->createAccessDeniedException();
    // }
}

了解更多

The FOSUserBundle, developed by the Symfony community, adds support for a database-backed user system in Symfony. It also handles common tasks like user registration and forgotten password functionality.

Enable the Remember Me feature to allow your users to stay logged in for a long period of time.

When providing customer support, sometimes it’s necessary to access the application as some other user so that you can reproduce the problem. Symfony provides the ability to impersonate users.

If your company uses a user login method not supported by Symfony, you can develop your own user provider and your own authentication provider.