发布于 2015-08-27 16:23:56 | 221 次阅读 | 评论: 0 | 来源: 网络整理
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: AppBundleEntityUser: 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.
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 SensioBundleFrameworkExtraBundleConfigurationRoute; use SensioBundleFrameworkExtraBundleConfigurationSecurity; // ... /** * 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 AppBundleEntityPost; use SensioBundleFrameworkExtraBundleConfigurationRoute; use SensioBundleFrameworkExtraBundleConfigurationSecurity; /** * @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 AppBundleEntityPost; use SensioBundleFrameworkExtraBundleConfigurationSecurity; /** * @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 %}
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(); } // ... }
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 AppBundleSecurity; use SymfonyComponentSecurityCoreAuthorizationVoterAbstractVoter; use SymfonyComponentSecurityCoreUserUserInterface; // 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('AppBundleEntityPost'); } 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: AppBundleSecurityPostVoter 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.