嘿哥们,在程序中你可能遇到像这样的事情,“比如,你烤了一些饼干,但是不想让别人吃,只能你自己享用”,也就是说你希望能够知道当前用户是否能编辑、删除或者查看自己的东西。我们就可以充分利用symfony的特殊功能:security voters。
今天的应用:DeliciousCookie
我们使用一个页面展示所有的饼干,并使用用户名ryan和密码为cookie的用户登录,并且每一个饼干都有一个“nom”按钮,我点击它就等于我吃掉了他,他会从数据库中删除。真是高科技,哈哈。
这个应用程序非常的简单:我们有一个AppBundle当然里面还有一个DeliciousCookie实体。这个DeliciousCookie里有一个bakerUsername属性,他来存储究竟是谁制作的这个饼干。为了让这个程序尽量简单,我们没有使用User实体,而是在security创建一个username字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// src/AppBundle/Entity/DeliciousCookie.php // ... /** @ORM\Entity */ class DeliciousCookie { // ... /** * @ORM\Column() */ private $flavor; /** * @ORM\Column(name="baker_username") */ private $bakerUsername; // ... } |
现在的程序,每个人都可以吃这个饼干,不管谁做的。但是我们的目标是让你吃到你做的饼干。该CookieController里面会输出饼干列表页面,并谁在里面点击,就删除数据库里的信息,并设置提示信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// src/AppBundle/Controller/CookieController.php // ... class CookieController extends Controller { /** @Route("/cookies", name="cookie_list") */ public function indexAction() { $cookies = $this->getDoctrine() ->getRepository('AppBundle:DeliciousCookie') ->findAll(); return $this->render('Cookie/index.html.twig', array( 'cookies' => $cookies, )); } /** * @Route("/cookies/nom/{id}", name="cookie_nom") * @Method("POST") */ public function nomAction(Request $request, $id) { $em = $this->getDoctrine()->getManager(); $cookie = $em->getRepository('AppBundle:DeliciousCookie') ->find($id); // ... $em->remove($cookie); $em->flush(); // some flash-setting stuff... return $this->redirect($url); } } |
唯一有趣的是security.yml。我们有两个硬编码用户:ryan和leanna,并且access_control会让登录后的用户查看/cookies页面.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# app/config/security.yml security: # ... providers: in_memory: memory: users: ryan: { password: cookie, roles: 'ROLE_COOKIE_ENJOYER' } leanna: { password: cookie, roles: 'ROLE_COOKIE_MONSTER' } firewalls: default: pattern: ^/ anonymous: ~ form_login: ~ logout: ~ access_control: - { path: ^/cookies, roles: IS_AUTHENTICATED_FULLY } |
禁止访问:简单方法
防止我吃别人的饼干是很简单的。首先,要做的就是在controller中写逻辑。所以你在这里这样做就行:如果你现在登录的名字不等于饼干作者名字,就会抛出一个AccessDeniedException提示“你不能吃它”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/AppBundle/Controller/CookieController.php // ... public function nomAction(Request $request, $id) { $em = $this->getDoctrine()->getManager(); $cookie = $em->getRepository('AppBundle:DeliciousCookie') ->find($id); // ... if ($cookie->getBakerUsername() != $this->getUser()->getUsername()) { throw $this->createAccessDeniedException( 'You did not bake this delicious cookie!' ); } // ... } |
现在,如果你偷吃别人的饼干就会被抓。在生产环境下,只会提示一个403错误页面。
我们还需要在twig中写一些逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{# app/Resources/views/Cookie/index.html.twig #} {# ... #} {% for cookie in cookies %} {# ... #} {% if cookie.bakerUsername == app.user.username %} <form action="{{ path('cookie_nom', {'id': cookie.id}) }}" method="POST"> <button type="submit" class="btn">NOM! <i class="glyphicon glyphicon-cutlery"></i></button> </form> {% endif %} {# ... #} {% endfor %} |
你可以看到,我们隐藏了不属于我的NOM按钮。但是在安全写安全逻辑时,特别是保护饼干的逻辑时,我们不想它在你的程序中重复使用,因为如果你改变一些东西,你很可能忘记更新这一部分,你可能会出大问题。
///////////////////////////
创建一个Voter
因此,Voter的目标是让我们集中在逻辑,所以我们只有在一个地方写这些。你需要创建一个Security目录里面有一个CookieVoter。我们将使用symfony2.6配备的一个梦幻般的类AbstractVoter。如果你使用symfony2.5或者更低,你其实可以从互联网上找到这个类,只不过现在他内置了。现在加入声明并extends CookieVoter。
PHP告诉我,我需要填写三个抽象方法,来完成非常棒的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// src/AppBundle/Security/CookieVoter.php namespace AppBundle\Security; use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; use Symfony\Component\Security\Core\User\UserInterface; class CookieVoter extends AbstractVoter { protected function getSupportedClasses() { // todo } protected function getSupportedAttributes() { // todo } protected function isGranted($attribute, $object, $user = null) { // todo } } |
什么是Voter他能做什么?
因此,让我们回头看看,因为我并没有告诉你什么是这些Voter要做的。首先当我们完成这些代码时,我希望他们看起来很规整。而不是混乱的手工逻辑,这需要我们修改之前的代码,使用isGranted函数,传入NOM参数他很重要他会让我明白应该做什么,然后,传入$cookie对象作为第二个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/AppBundle/Controller/CookieController.php // ... public function nomAction(Request $request, $id) { $em = $this->getDoctrine()->getManager(); $cookie = $em->getRepository('AppBundle:DeliciousCookie') ->find($id); // ... if (!$this->isGranted('NOM', $cookie)) { throw $this->createAccessDeniedException( 'You did not bake this delicious cookie!' ); } // ... } |
这个isGranted在symfony2.6被引入,他所做的就是不用使用security.context服务,直接调用isGranted就可以。因此,要根据你的symfony版本来判断,是否手动使用security.context服务。
在幕后,当你使用isGranted功能,symfony会召唤出一堆的Voter,并要求他们每一个人告诉我们是否应该访问。例如,当你传入ROLE_SOMETHING到isGranted就像ROLE_USER,有一个RoleVoter类它试图找出当前用户是否能作用到你访问的有关类。
然而,这些人并不知道,你可以发明这些字符串,作为这些人的操作标签。因此,在这种情况下,我发明了NOM,并且我们要添加一个新的Voter到系统,就是跟symfony说:“嘿symfony,每当这个NOM属性传入isGranted就告诉我”,为了让他工作,我们需要填写getSupportedClasses和getSupportedAttributes功能
填写CookieVoter
首先,在getSupportedClasses,他要返回DeliciousCookie类字符串;
1 2 3 4 5 6 7 |
// src/AppBundle/Security/CookieVoter.php // ... protected function getSupportedClasses() { return array('AppBundle\Entity\DeliciousCookie'); } |
他告诉symfony,我们传入DeliciousCookie对象到isGranted。我们会在getSupportedAttributes里做同样的事情,让他返回一个数组里面有NOM字符串。
1 2 3 4 5 6 7 |
// src/AppBundle/Security/CookieVoter.php // ... protected function getSupportedAttributes() { return array('NOM'); } |
他告诉symfony,我们把NOM传入isGranted。每当上面的两个函数成立,isGranted这个底层函数将被调用。
现在我们使用var_dump准备打印一下,属性对象和用户;
1 2 3 4 5 6 7 |
// src/AppBundle/Security/CookieVoter.php // ... protected function isGranted($attribute, $object, $user = null) { var_dump($attribute, $object, $user);die; } |
注册和标注你的Voter
我们的Voter类已经蓄势待发。但是symfony还不能自动找到他。要告诉symfony我们的新Voter要注册成一个服务,并给他一个特殊标签;
我们有一个app/config/services.yml文件他会被添加到config.yml文件,我们要在这个文件中添加服务:
1 2 3 4 5 6 |
# app/config/services.yml services: app_cookie_voter: class: AppBundle\Security\CookieVoter tags: - { name: security.voter } |
这个名字并不重要,尽量保持短小。
重要的是tags部分,你需要添加tag名字security.voter。
让我们试试吧!当我们刷新,我们的看到有东西输出,证明是我们的Voter被调用。
保护您的饼干逻辑
现在事情开始变的酷了。因为我们理论上有ACCESS_CONTROL,这个Voter不应该能够进来,除非用户已经登录,为了保险起见我们还是使用is_object检查一下,看看是否有用户记录:
1 2 3 4 5 6 7 8 9 10 11 |
// src/AppBundle/Security/CookieVoter.php // ... protected function isGranted($attribute, $object, $user = null) { if (!is_object($user)) { return false; } // ... todo } |
请记住,这样做是因为在symfony2如果你是匿名用户他会是一个字符串。从这里开始就是纯逻辑:如果Baker’s等于当前用户名,我们就让他进入。否则我们就让他滚蛋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/AppBundle/Security/CookieVoter.php // ... protected function isGranted($attribute, $object, $user = null) { if (!is_object($user)) { return false; } if ($object->getBakerUsername() == $user->getUsername()) { return true; } return false; } |
因此,让我们刷新“NOM”请求,他工作了!我们登录Ryan,需要点击按钮才能知道Ryan的饼干才能吃,体验不好。现在我们进入twig模板,使用is_granted函数,做同样的事情。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{# app/Resources/views/Cookie/index.html.twig #} {# ... #} {% for cookie in cookies %} {# ... #} {% if is_granted('NOM', cookie) %} <form action="{{ path('cookie_nom', {'id': cookie.id}) }}" method="POST"> <button type="submit" class="btn">NOM! <i class="glyphicon glyphicon-cutlery"></i></button> </form> {% endif %} {# ... #} {% endfor %} |
现在刷新,你看到了你期望的效果。
给ROLE_COOKIE_MONSTER角色的用户特殊访问
现在我们让事情变的困难一些。在security.yml中给leanna一个特殊的用户角色ROLE_COOKIE_MONSTER:
1 2 3 4 5 6 7 8 9 10 |
# app/config/security.yml security: # ... providers: in_memory: memory: users: ryan: { password: cookie, roles: 'ROLE_COOKIE_ENJOYER' } leanna: { password: cookie, roles: 'ROLE_COOKIE_MONSTER' } |
如果你有这个角色,你可以吃任何人的饼干,即使你一个饼干都没有做。似乎你是一个混蛋,当我们看看尝试一下。
要做到这一点,我们基本上要告诉isGranted从安全中获取角色。现在,我们无法获得安全的信息,所以我们要使用依赖注入。我们要注入security.context。唯一的问题是,因为我们的内部体系,如果我们试图注入安全系统到这里会得到一个循环的依赖。相反,我们可以注入整个容器,是的这样不怎么好,但是这种情况下,没有办法,当然也不会给我们带来什么伤害:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/AppBundle/Security/CookieVoter.php // ... class CookieVoter extends AbstractVoter { private $container; public function __construct(ContainerInterface $container) { $this->container = $container; } // ... } |
回到services.yml添加一个参数键,使用依赖把@service_container(容器)注入到__construct函数。
1 2 3 4 5 6 7 |
# app/config/services.yml services: app_cookie_voter: class: AppBundle\Security\CookieVoter arguments: ["@service_container"] tags: - { name: security.voter } |
下一步赶快去isGranted写逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/AppBundle/Security/CookieVoter.php // ... protected function isGranted($attribute, $object, $user = null) { if (!is_object($user)) { return false; } // in 2.5 and earlier, use this: // $this->container->get('security.context'); // security.context exists in 2.6, but is deprecated $authChecker = $this->container->get('security.authorization_checker'); if ($authChecker->isGranted('ROLE_COOKIE_MONSTER')) { return true; } if ($object->getBakerUsername() == $user->getUsername()) { return true; } return false; } |
现在我们使用symfony2.6的全新服务security.authorization_checker。如果使用旧版本,就要用security.context,当然你不必担心,因为symfony2.6仍然存在,直至symfony3.0才会消失。这样我们就可以使用相同的isGranted功能,检测用户角色为ROLE_COOKIE_MONSTER的用户是否能够访问。
我们尝试一下区别,现在我们登录ryan,没有什么变化。我们退出,登录leanna,真的可以随便吃。
添加多个操作(NOM,DONATE[捐赠])到一个Voter
我还想要一些疯狂的事情发生。让我们捐出我们的饼干给朋友。现在我知道这很疯狂,为什么我做的饼干要捐给别人?我们就是尝试一下。实际上没有这样的逻辑代码,但是没关系。我们进入index.html.twig添加一个链接,我们只是去体验一下,正常的隐藏和显示是否正确。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{# app/Resources/views/Cookie/index.html.twig #} {# ... #} {% for cookie in cookies %} {# ... #} <td> {% if is_granted('DONATE', cookie) %} <a href="">Donate</a> {% endif %} </td> {# ... #} {% endfor %} |
就像之前一样我又发明了DONATE字符串。如果我们不做别的,只是刷新,你会发现链接显示不出来。如果没有这样的属性,在我们的Voter中就会返回false。我们要使用getSupportedAttributes功能。
让我们更新他加入一个DONATE
1 2 3 4 5 6 7 |
// src/AppBundle/Security/CookieVoter.php // ... protected function getSupportedAttributes() { return array('NOM', 'DONATE'); } |
现在isGranted可以处理两个不同的属性NOM和DONATE了。我们更新逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
protected function isGranted($attribute, $object, $user = null) { if (!is_object($user)) { return false; } $authChecker = $this->container->get('security.authorization_checker'); switch ($attribute) { case 'NOM': if ($authChecker->isGranted('ROLE_COOKIE_MONSTER')) { return true; } if ($object->getBakerUsername() == $user->getUsername()) { return true; } return false; case 'DONATE': // todo ... } return false; } |
在我们的例子中,因为巧克力饼干最好吃,所以我们就放弃不带巧克力的饼干。所以我们要写这些逻辑,要是出现了巧克力这个词我们就,不送人了,但是如果没有,你可以把他捐赠
1 2 3 4 5 6 |
switch ($attribute) { case 'NOM': // ... case 'DONATE': return stripos($object->getFlavor(), 'chocolate') === false; } |
在这个函数底部,我们还存在一个return false。他在技术上没有道理。如果我们在isGranted中传入NOM和DONATE以外的参数,symfony也不会调用我们的Voter因为这个getSupportedAttributes。
所以在这里你可以放任何东西,我喜欢抛出一个异常。但是你会没事的:
1 2 3 4 5 6 7 8 9 10 |
protected function isGranted($attribute, $object, $user = null) { // ... switch ($attribute) { // ... } throw new \LogicException('How did we get here!?'); } |
酷,让我们来看看饼干,你可以赠予了。这一次我们看到了捐赠链接不属于巧克力饼干。那就完美了。
让我们用一些常量
我们有NOM和DONATE赤裸的字符串在程序中,他不是很好的方式,我们应该用一个常量代替。以为我们创建两个常量ATTRIBUTE_NOM和ATTRIBUTE_DONATE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// src/AppBundle/Security/CookieVoter.php // ... class CookieVoter extends AbstractVoter { const ATTRIBUTE_NOM = 'NOM'; const ATTRIBUTE_DONATE = 'DONATE'; // ... protected function getSupportedAttributes() { return array(self::ATTRIBUTE_NOM, self::ATTRIBUTE_DONATE); } // ... protected function isGranted($attribute, $object, $user = null) { // ... switch ($attribute) { case self::ATTRIBUTE_NOM: // ... case self::ATTRIBUTE_DONATE: // ... } throw new \LogicException('How did we get here!?'); } } |
然后,我们把他使用在getSupportedAttributes和isGranted中。
我们的CookieController也可以使用这个常量:
1 2 3 4 5 6 7 8 |
// src/AppBundle/Controller/CookieController.php // ... if (!$this->isGranted(CookieVoter::ATTRIBUTE_NOM, $cookie)) { throw $this->createAccessDeniedException( 'You did not bake this delicious cookie!' ); } |
是的,你也可以使用twig的constant()函数来在模板中调用这些常量,对于我来说放到twig中有点丑,所以我就写到这里
去吧Security Voters!
当你需要弄清楚,如果用户访问一些特殊的对象,是否可以,你就需要security voters来解决。她有着我喜欢的功能之一,她可以很简单的制作模板逻辑和控制器逻辑。
symfony也有ACL系统,但是它令人难以置信的复杂,如果你有非常复杂的授权要求,我才建议你使用它。如果你只想用几行代码,就弄清楚,用户是否能访问这些,在Voter就可以,不必使用ACL。