发布于 2015-08-27 16:31:04 | 225 次阅读 | 评论: 0 | 来源: 网络整理
Symfony’s security system is incredibly powerful, but it can also be confusing to set up. In this chapter, you’ll learn how to set up your application’s security step-by-step, from configuring your firewall and how you load users to denying access and fetching the User object. Depending on what you need, sometimes the initial setup can be tough. But once it’s done, Symfony’s security system is both flexible and (hopefully) fun to work with.
Since there’s a lot to talk about, this chapter is organized into a few big sections:
设置 security.yml
文件(身份鉴定)
设置访问权限(权限控制)
获取当前用户对象
These are followed by a number of small (but still captivating) sections, like logging out and encoding user passwords.
The security system is configured in app/config/security.yml
. The default configuration looks like this:
# app/config/security.yml security: providers: in_memory: memory: ~ firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false default: anonymous: ~
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <provider name="in_memory"> <memory /> </provider> <firewall name="dev" pattern="^/(_(profiler|wdt)|css|images|js)/" security=false /> <firewall name="default"> <anonymous /> </firewall> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( 'providers' => array( 'in_memory' => array( 'memory' => array(), ), ), 'firewalls' => array( 'dev' => array( 'pattern' => '^/(_(profiler|wdt)|css|images|js)/', 'security' => false, ), 'default' => array( 'anonymous' => null, ), ), ));
The firewalls
key is the heart of your security configuration. The dev
firewall isn’t important, it just makes sure that Symfony’s development tools - which live under URLs like /_profiler
and /_wdt
aren’t blocked by your security.
小技巧
You can also match a request against other details of the request (e.g. host). For more information and examples read How to Restrict Firewalls to a Specific Request.
All other URLs will be handled by the default
firewall (no pattern
key means it matches all URLs). You can think of the firewall like your security system, and so it usually makes sense to have just one main firewall. But this does not mean that every URL requires authentication - the anonymous
key takes care of this. In fact, if you go to the homepage right now, you’ll have access and you’ll see that you’re “authenticated” as anon.
. Don’t be fooled by the “Yes” next to Authenticated, you’re just an anonymous user:
You’ll learn later how to deny access to certain URLs or controllers.
小技巧
Security is highly configurable and there’s a Security Configuration Reference that shows all of the options with some extra explanation.
The main job of a firewall is to configure how your users will authenticate. Will they use a login form? Http Basic? An API token? All of the above?
Let’s start with Http Basic (the old-school pop-up) and work up from there. To activate this, add the http_basic
key under your firewall:
# app/config/security.yml security: # ... firewalls: # ... default: anonymous: ~ http_basic: ~
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <!-- ... --> <firewall name="default"> <anonymous /> <http-basic /> </firewall> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( // ... 'firewalls' => array( // ... 'default' => array( 'anonymous' => null, 'http_basic' => null, ), ), ));
Simple! To try this, you need to require the user to be logged in to see a page. To make things interesting, create a new page at /admin
. For example, if you use annotations, create something like this:
// src/AppBundle/Controller/DefaultController.php // ... use SensioBundleFrameworkExtraBundleConfigurationRoute; use SymfonyComponentHttpFoundationResponse; class DefaultController extends Controller { /** * @Route("/admin") */ public function adminAction() { return new Response('Admin page!'); } }
Next, add an access_control
entry to security.yml
that requires the user to be logged in to access this URL:
# app/config/security.yml security: # ... firewalls: # ... access_control: # require ROLE_ADMIN for /admin* - { path: ^/admin, roles: ROLE_ADMIN }
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <!-- ... --> <firewall name="default"> <!-- ... --> </firewall> <access-control> <!-- require ROLE_ADMIN for /admin* --> <rule path="^/admin" role="ROLE_ADMIN" /> </access-control> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( // ... 'firewalls' => array( // ... 'default' => array( // ... ), ), 'access_control' => array( // require ROLE_ADMIN for /admin* array('path' => '^/admin', 'role' => 'ROLE_ADMIN'), ), ));
注解
You’ll learn more about this ROLE_ADMIN
thing and denying access later in the 2) Denying Access, Roles and other Authorization section.
Great! Now, if you go to /admin
, you’ll see the HTTP Basic popup:
But who can you login as? Where do users come from?
小技巧
Want to use a traditional login form? Great! See How to Build a Traditional Login Form. What other methods are supported? See the Configuration Reference or build your own.
When you type in your username, Symfony needs to load that user’s information from somewhere. This is called a “user provider”, and you’re in charge of configuring it. Symfony has a built-in way to load users from the database, or you can create your own user provider.
The easiest (but most limited) way, is to configure Symfony to load hardcoded users directly from the security.yml
file itself. This is called an “in memory” provider, but it’s better to think of it as an “in configuration” provider:
# app/config/security.yml security: providers: in_memory: memory: users: ryan: password: ryanpass roles: 'ROLE_USER' admin: password: kitten roles: 'ROLE_ADMIN' # ...
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <provider name="in_memory"> <memory> <user name="ryan" password="ryanpass" roles="ROLE_USER" /> <user name="admin" password="kitten" roles="ROLE_ADMIN" /> </memory> </provider> <!-- ... --> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( 'providers' => array( 'in_memory' => array( 'memory' => array( 'users' => array( 'ryan' => array( 'password' => 'ryanpass', 'roles' => 'ROLE_USER', ), 'admin' => array( 'password' => 'kitten', 'roles' => 'ROLE_ADMIN', ), ), ), ), ), // ... ));
Like with firewalls
, you can have multiple providers
, but you’ll probably only need one. If you do have multiple, you can configure which one provider to use for your firewall under its provider
key (e.g. provider: in_memory
).
Try to login using username admin
and password kitten
. You should see an error!
No encoder has been configured for account “SymfonyComponentSecurityCoreUserUser”
To fix this, add an encoders
key:
# app/config/security.yml security: # ... encoders: SymfonyComponentSecurityCoreUserUser: plaintext # ...
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <!-- ... --> <encoder class="SymfonyComponentSecurityCoreUserUser" algorithm="plaintext" /> <!-- ... --> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( // ... 'encoders' => array( 'SymfonyComponentSecurityCoreUserUser' => 'plaintext', ), // ... ));
User providers load user information and put it into a User
object. If you load users from the database or some other source, you’ll use your own custom User class. But when you use the “in memory” provider, it gives you a SymfonyComponentSecurityCoreUserUser
object.
Whatever your User class is, you need to tell Symfony what algorithm was used to encode the passwords. In this case, the passwords are just plaintext, but in a second, you’ll change this to use bcrypt
.
If you refresh now, you’ll be logged in! The web debug toolbar even tells you who you are and what roles you have:
Because this URL requires ROLE_ADMIN
, if you had logged in as ryan
, this would deny you access. More on that later (通过URL进行权限控制(access_control)).
If you’d like to load your users via the Doctrine ORM, that’s easy! See How to Load Security Users from the Database (the Entity Provider) for all the details.
Whether your users are stored in security.yml
, in a database or somewhere else, you’ll want to encode their passwords. The best algorithm to use is bcrypt
:
# app/config/security.yml security: # ... encoders: SymfonyComponentSecurityCoreUserUser: algorithm: bcrypt cost: 12
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <!-- ... --> <encoder class="SymfonyComponentSecurityCoreUserUser" algorithm="bcrypt" cost="12" /> <!-- ... --> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( // ... 'encoders' => array( 'SymfonyComponentSecurityCoreUserUser' => array( 'algorithm' => 'bcrypt', 'cost' => 12, ) ), // ... ));
警告
If you’re using PHP 5.4 or lower, you’ll need to install the ircmaxell/password-compat
library via Composer in order to be able to use the bcrypt
encoder:
{ "require": { ... "ircmaxell/password-compat": "~1.0.3" } }
Of course, your user’s passwords now need to be encoded with this exact algorithm. For hardcoded users, you can use an online tool, which will give you something like this:
# app/config/security.yml security: # ... providers: in_memory: memory: users: ryan: password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli roles: 'ROLE_USER' admin: password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G roles: 'ROLE_ADMIN'
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <provider name="in_memory"> <memory> <user name="ryan" password="$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli" roles="ROLE_USER" /> <user name="admin" password="$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G" roles="ROLE_ADMIN" /> </memory> </provider> <!-- ... --> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( 'providers' => array( 'in_memory' => array( 'memory' => array( 'users' => array( 'ryan' => array( 'password' => '$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli', 'roles' => 'ROLE_USER', ), 'admin' => array( 'password' => '$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G', 'roles' => 'ROLE_ADMIN', ), ), ), ), ), // ... ));
Everything will now work exactly like before. But if you have dynamic users (e.g. from a database), how can you programmatically encode the password before inserting them into the database? Don’t worry, see 动态加密密码 for details.
小技巧
Supported algorithms for this method depend on your PHP version, but include the algorithms returned by the PHP function hash_algos
as well as a few others (e.g. bcrypt). See the encoders
key in the Security Reference Section for examples.
It’s also possible to use different hashing algorithms on a user-by-user basis. See How to Choose the Password Encoder Algorithm Dynamically for more details.
Congratulations! You now have a working authentication system that uses Http Basic and loads users right from the security.yml
file.
Your next steps depend on your setup:
2.6 新版功能: The security.token_storage
service was introduced in Symfony 2.6. Prior to Symfony 2.6, you had to use the getToken()
method of the security.context
service.
After authentication, the User
object of the current user can be accessed via the security.token_storage
service. From inside a controller, this will look like:
public function indexAction() { if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) { throw $this->createAccessDeniedException(); } $user = $this->getUser(); // the above is a shortcut for this $user = $this->get('security.token_storage')->getToken()->getUser(); }
小技巧
The user will be an object and the class of that object will depend on your user provider.
Now you can call whatever methods are on your User object. For example, if your User object has a getFirstName()
method, you could use that:
use SymfonyComponentHttpFoundationResponse; public function indexAction() { // ... return new Response('Well hi there '.$user->getFirstName()); }
It’s important to check if the user is authenticated first. If they’re not, $user
will either be null
or the string anon.
. Wait, what? Yes, this is a quirk. If you’re not logged in, the user is technically the string anon.
, though the getUser()
controller shortcut converts this to null
for convenience.
The point is this: always check to see if the user is logged in before using the User object, and use the isGranted
method (or access_control) to do this:
// yay! Use this to see if the user is logged in if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) { throw $this->createAccessDeniedException(); } // boo :(. Never check for the User object to see if they're logged in if ($this->getUser()) { }
In a Twig Template this object can be accessed via the app.user key:
{% if is_granted('IS_AUTHENTICATED_FULLY') %} <p>Username: {{ app.user.username }}</p> {% endif %}
<?php if ($view['security']->isGranted('IS_AUTHENTICATED_FULLY')): ?> <p>Username: <?php echo $app->getUser()->getUsername() ?></p> <?php endif; ?>
Usually, you’ll also want your users to be able to log out. Fortunately, the firewall can handle this automatically for you when you activate the logout
config parameter:
# app/config/security.yml security: firewalls: secured_area: # ... logout: path: /logout target: / # ...
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <firewall name="secured_area" pattern="^/"> <!-- ... --> <logout path="/logout" target="/" /> </firewall> <!-- ... --> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( 'firewalls' => array( 'secured_area' => array( // ... 'logout' => array('path' => 'logout', 'target' => '/'), ), ), // ... ));
Next, you’ll need to create a route for this URL (but not a controller):
# app/config/routing.yml logout: path: /logout
<!-- app/config/routing.xml --> <?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> <route id="logout" path="/logout" /> </routes>
// app/config/routing.php use SymfonyComponentRoutingRouteCollection; use SymfonyComponentRoutingRoute; $collection = new RouteCollection(); $collection->add('logout', new Route('/logout', array())); return $collection;
And that’s it! By sending a user to /logout
(or whatever you configure the path
to be), Symfony will un-authenticate the current user.
Once the user has been logged out, they will be redirected to whatever path is defined by the target
parameter above (e.g. the homepage
).
小技巧
If you need to do something more interesting after logging out, you can specify a logout success handler by adding a success_handler
key and pointing it to a service id of a class that implements LogoutSuccessHandlerInterface
. See Security Configuration Reference.
If, for example, you’re storing users in the database, you’ll need to encode the users’ passwords before inserting them. No matter what algorithm you configure for your user object, the hashed password can always be determined in the following way from a controller:
// whatever *your* User object is $user = new AppBundleEntityUser(); $plainPassword = 'ryanpass'; $encoder = $this->container->get('security.password_encoder'); $encoded = $encoder->encodePassword($user, $plainPassword); $user->setPassword($encoded);
2.6 新版功能: The security.password_encoder
service was introduced in Symfony 2.6.
In order for this to work, just make sure that you have the encoder for your user class (e.g. AppBundleEntityUser
) configured under the encoders
key in app/config/security.yml
.
The $encoder
object also has an isPasswordValid
method, which takes the User
object as the first argument and the plain password to check as the second argument.
警告
When you allow a user to submit a plaintext password (e.g. registration form, change password form), you must have validation that guarantees that the password is 4096 characters or fewer. Read more details in How to implement a simple Registration Form.
Instead of associating many roles to users, you can define role inheritance rules by creating a role hierarchy:
# app/config/security.yml security: role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <role id="ROLE_ADMIN">ROLE_USER</role> <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( 'role_hierarchy' => array( 'ROLE_ADMIN' => 'ROLE_USER', 'ROLE_SUPER_ADMIN' => array( 'ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH', ), ), ));
In the above configuration, users with ROLE_ADMIN
role will also have the ROLE_USER
role. The ROLE_SUPER_ADMIN
role has ROLE_ADMIN
, ROLE_ALLOWED_TO_SWITCH
and ROLE_USER
(inherited from ROLE_ADMIN
).
By default, Symfony relies on a cookie (the Session) to persist the security context of the user. But if you use certificates or HTTP authentication for instance, persistence is not needed as credentials are available for each request. In that case, and if you don’t need to store anything else between requests, you can activate the stateless authentication (which means that no cookie will be ever created by Symfony):
# app/config/security.yml security: firewalls: main: http_basic: ~ stateless: true
<!-- app/config/security.xml --> <?xml version="1.0" encoding="UTF-8"?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <config> <firewall stateless="true"> <http-basic /> </firewall> </config> </srv:container>
// app/config/security.php $container->loadFromExtension('security', array( 'firewalls' => array( 'main' => array('http_basic' => array(), 'stateless' => true), ), ));
注解
If you use a form login, Symfony will create a cookie even if you set stateless
to true
.
When using lots of dependencies in your Symfony projects, some of them may contain security vulnerabilities. That’s why Symfony includes a command called security:check
that checks your composer.lock
file to find any known security vulnerability in your installed dependencies:
$ php app/console security:check
A good security practice is to execute this command regularly to be able to update or replace compromised dependencies as soon as possible. Internally, this command uses the public security advisories database published by the FriendsOfPHP organization.
小技巧
The security:check
command terminates with a non-zero exit code if any of your dependencies is affected by a known security vulnerability. Therefore, you can easily integrate it in your build process.
Woh! Nice work! You now know more than the basics of security. The hardest parts are when you have custom requirements: like a custom authentication strategy (e.g. API tokens), complex authorization logic and many other things (because security is complex!).
Fortunately, there are a lot of Security Cookbook Articles aimed at describing many of these situations. Also, see the Security Reference Section. Many of the options don’t have specific details, but seeing the full possible configuration tree may be useful.
祝好运!