验证和防火墙(即,取得用户授信)/Authentication and Firewalls (i.e. Getting the User's Credentials)

你可以配置Symfony,以任何你认可的方式,来验证你的用户,还可以从任何数据源中取得用户的信息。这是个复杂的话题,但是在[Security]指南中(/doc/current/security.html)有很多相关信息。

不管你的需求是什么,authentication都要在sucurity.yml中进行配置,主要在firewall根键下进行。

Best Practice

Best Practice

除非你有两个合理的不同验证系统及其用户(比如一个是主站的表单登陆系统,还有一个是基于API的token系统),我们推荐只使用一个firewall入口 ,并且开启其下的anonymous选项。

多数程序只有一个验证系统,连同其用户。因此,你只需要一个firewall入口(译注:即firewall键的下面只有一个验证入口键,而非多个)。当然也有例外,特别是当你的网站有另一组页面并且要使用API验证接口时。这里的建议只是为了让事情更简单。

除此之外,在防火墙里要使用anonymous键。若你需要让用户在不网站的不同地方进行登陆(或者说干脆在所有功能区都可以登陆)的话,可在access_control根键下进行配置。

Best Practice

Best Practice

使用bcrypt encoder为用户的密码进行加密。

如果用户使用密码,我们推荐使用bcrypt加密方式,而不是使用传统的SHA-512加密法。bcrypt的主要优势在于,它包括了一个salt 值来保护密码免于受到“彩虹表”攻击(rainbow table attack),而且其适应性颇佳,能令暴力破解的过程变得愈发之长。

基于这种考虑,下面是我们的程序和验证有关的配置,使用了表单登陆,并且从数据库中取得用户:

  1. # app/config/security.yml
  2. security:
  3. encoders:
  4. AppBundle\Entity\User: bcrypt
  5. providers:
  6. database_users:
  7. entity: { class: AppBundle:User, property: username }
  8. firewalls:
  9. secured_area:
  10. pattern: ^/
  11. anonymous: true
  12. form_login:
  13. check_path: login
  14. login_path: login
  15. logout:
  16. path: security_logout
  17. target: homepage
  18.  
  19. # ... access_control exists, but is not shown here

Tip

我们这个项目的源代码中包含了注释在内,用于解释每一部分。

授权(即,拒绝访问)/Authorization (i.e. Denying Access)

Symfony给了你几种实施授权的方式,包括security.yml中的access_control配置,@Security annotation以及直接使用security.authorization_checker服务的isGranted

Best Practice

Best Practice

  • 为了保护泛URL内容,在access_control中使用正则匹配;
  • 尽最大可能使用@Security annotation;
  • 一旦遇到复杂状况,直接利用security.authorization_checker服务来检查安全性。

另有不同方式来令你的授权逻辑“中心化”(译注:sf官方文档的centralize一般是指“将内容集中于某种”而便于管理,本站译为“中心化”),比如使用自定义的security voter或者使用ACL。

Best Practice

Best Practice

  • 对于精细化(fine-grained)的访问控制,应使用自定义的security voter;
  • 若要通过Admin后台界面来针对任意 对象进行访问控制,使用Symfony ACL。

@Security Annotation

在控制器里,实施访问控制时,尽量使用@Security注释。位于action上方的它们,不光容易理解,还容易替换。

在我们的程序中,你需要使用ROLE_ADMIN授权,才能创建一个新贴子。使用@Security时,代码会像下面这样:

  1. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  2. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  3. // ...
  4.  
  5. /**
  6. * Displays a form to create a new Post entity.
  7. * 显示用于创建Post entity的表单
  8. *
  9. * @Route("/new", name="admin_post_new")
  10. * @Security("has_role('ROLE_ADMIN')")
  11. */
  12. public function newAction()
  13. {
  14. // ...
  15. }

对于复杂安全限制使用表达式

如果你的security逻辑相对复杂,你应该使用@Security中的expression表达式。在下面的例子中,用户若要访问控制器的页面,那么他的email必须匹配Post对象中的getAuthorEmail方法所返回的值:

  1. use AppBundle\Entity\Post;
  2. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  3. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  4.  
  5. /**
  6. * @Route("/{id}/edit", name="admin_post_edit")
  7. * @Security("user.getEmail() == post.getAuthorEmail()")
  8. */
  9. public function editAction(Post $post)
  10. {
  11. // ...
  12. }

注意这时我们利用了ParamConverter,它可以自动查询出所需之Post对象并将其作为$post参数(传入控制器)。这便令在表达式中使用$post变量成为可能。

但这有一个主要缺点:在annotation中定义的表达式,很难被程序的其他部分复用。试想你要在模板中添加一个链接,只有作者本人能看到。此时你必须用Twig语法来重复表达式代码:

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

最容易的解决办法是 - 如果你的逻辑够简单 - 添加一个新的方法给Post entity,用于检查当前用户是否是贴子作者:

  1. // src/AppBundle/Entity/Post.php
  2. // ...
  3.  
  4. class Post
  5. {
  6. // ...
  7.  
  8. /**
  9. * Is the given User the author of this Post?
  10. * 当前用户是否是贴子作者?
  11. *
  12. * @return bool
  13. */
  14. public function isAuthor(User $user = null)
  15. {
  16. return $user && $user->getEmail() == $this->getAuthorEmail();
  17. }
  18. }

现在你可以在模板和表达式中同时使用这个方法了:

  1. use AppBundle\Entity\Post;
  2. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  3.  
  4. /**
  5. * @Route("/{id}/edit", name="admin_post_edit")
  6. * @Security("post.isAuthor(user)")
  7. */
  8. public function editAction(Post $post)
  9. {
  10. // ...
  11. }
  1. {% if post.isAuthor(app.user) %}
  2. <a href=""> ... </a>
  3. {% endif %}

不使用@Security来检查权限

上面用到@Security的例子,仅在我们使用ParamConverter时才能工作,是它让表达式能够访问post变量。如果你不使用参数转换或者处于更高端的使用场景中,那么你始终可以利用PHP作完全相同的安全检查:

  1. /**
  2. * @Route("/{id}/edit", name="admin_post_edit")
  3. */
  4. public function editAction($id)
  5. {
  6. $post = $this->getDoctrine()->getRepository('AppBundle:Post')
  7. ->find($id);
  8.  
  9. if (!$post) {
  10. throw $this->createNotFoundException();
  11. }
  12.  
  13. if (!$post->isAuthor($this->getUser())) {
  14. $this->denyAccessUnlessGranted('edit', $post);
  15.  
  16. // or without the shortcut(或者不使用快捷写法):
  17. //
  18. // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  19. // ...
  20. //
  21. // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
  22. // throw $this->createAccessDeniedException();
  23. // }
  24. }
  25.  
  26. // ...
  27. }

Security Voters

如果你的Security逻辑比较复杂,而且不能“中心化”到诸如isAuthor()这样的方法中时,你应该利用自定义voters。这是个比ACLs容易了几何级数倍的选择,却给了你在几乎所有场合所需要的灵活性。

首先要创建一个voter类。以下例程展示了与前例用过的getAuthorEmail相同的逻辑:

  1. namespace AppBundle\Security;
  2.  
  3. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  4. use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
  5. use Symfony\Component\Security\Core\Authorization\Voter\Voter;
  6. use Symfony\Component\Security\Core\User\UserInterface;
  7. use AppBundle\Entity\Post;
  8.  
  9. class PostVoter extends Voter
  10. {
  11. const CREATE = 'create';
  12. const EDIT = 'edit';
  13.  
  14. /**
  15. * @var AccessDecisionManagerInterface
  16. */
  17. private $decisionManager;
  18.  
  19. public function __construct(AccessDecisionManagerInterface $decisionManager)
  20. {
  21. $this->decisionManager = $decisionManager;
  22. }
  23.  
  24. protected function supports($attribute, $subject)
  25. {
  26. if (!in_array($attribute, array(self::CREATE, self::EDIT))) {
  27. return false;
  28. }
  29.  
  30. if (!$subject instanceof Post) {
  31. return false;
  32. }
  33.  
  34. return true;
  35. }
  36.  
  37. protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
  38. {
  39. $user = $token->getUser();
  40. /** @var Post */
  41. $post = $subject; // $subject must be a Post instance, thanks to the supports method
  42.  
  43. if (!$user instanceof UserInterface) {
  44. return false;
  45. }
  46.  
  47. switch ($attribute) {
  48. case self::CREATE:
  49. // if the user is an admin, allow them to create new posts
  50. // 如果用户是admin,允许创建新贴
  51. if ($this->decisionManager->decide($token, array('ROLE_ADMIN'))) {
  52. return true;
  53. }
  54.  
  55. break;
  56. case self::EDIT:
  57. // if the user is the author of the post, allow them to edit the posts
  58. // 如果用户是贴子作者,允许编辑贴子
  59. if ($user->getEmail() === $post->getAuthorEmail()) {
  60. return true;
  61. }
  62.  
  63. break;
  64. }
  65.  
  66. return false;
  67. }
  68. }

要在程序中启用security voter,要新建一个服务:

  1. # app/config/services.yml
  2. services:
  3. # ...
  4. post_voter:
  5. class: AppBundle\Security\PostVoter
  6. arguments: ['@security.access.decision_manager']
  7. public: false
  8. tags:
  9. - { name: security.voter }

现在,你可以在@Security注释中使用这个voter了:

  1. /**
  2. * @Route("/{id}/edit", name="admin_post_edit")
  3. * @Security("is_granted('edit', post)")
  4. */
  5. public function editAction(Post $post)
  6. {
  7. // ...
  8. }

你也可以通过security.authorization_checker服务直接使用它,或者通过控制器的快捷方法来使用:

  1. /**
  2. * @Route("/{id}/edit", name="admin_post_edit")
  3. */
  4. public function editAction($id)
  5. {
  6. $post = ...; // query for the post
  7.  
  8. $this->denyAccessUnlessGranted('edit', $post);
  9.  
  10. // or without the shortcut(不使用快捷方法时的代码如下):
  11. //
  12. // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  13. // ...
  14. //
  15. // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
  16. // throw $this->createAccessDeniedException();
  17. // }
  18. }

了解更多

由Symfony社区开发的FOSUserBundle,为Symfony添加了对基于数据库的用户系统的支持。同时提供了很多常用功能,比如用户注册或忘记密码等相关操作。

开启Remember Me功能让你的用户可以长期保持登陆状态。

当网站对客户提供支持时,以不同的用户来访问程序是必要的,只有这样你才可以发现(他们遇到的)问题。Symfony提供了令你impersonate users(扮演用户)的能力。

如果你的公司使用的登陆方式不被Symfony支持,你可以开发自定义user provider自定义authentication provider