支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
你可以配置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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # 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: login
login_path: login
logout:
path: security_logout
target: homepage
# ... access_control exists, but is not shown here |
Tip
我们这个项目的源代码中包含了注释在内,用于解释每一部分。
Symfony给了你几种实施授权的方式,包括security.yml中的access_control
配置,@Security annotation以及直接使用security.authorization_checker
服务的isGranted。
Best Practice
Best Practice
access_control
中使用正则匹配;
@Security
annotation;
security.authorization_checker
服务来检查安全性。
另有不同方式来令你的授权逻辑“中心化”(译注:sf官方文档的centralize一般是指“将内容集中于某种”而便于管理,本站译为“中心化”),比如使用自定义的security voter或者使用ACL。
Best Practice
Best Practice
在控制器里,实施访问控制时,尽量使用@Security
注释。位于action上方的它们,不光容易理解,还容易替换。
在我们的程序中,你需要使用ROLE_ADMIN
授权,才能创建一个新贴子。使用@Security
时,代码会像下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
// ...
/**
* Displays a form to create a new Post entity.
* 显示用于创建Post entity的表单
*
* @Route("/new", name="admin_post_new")
* @Security("has_role('ROLE_ADMIN')")
*/
public function newAction()
{
// ...
} |
如果你的security逻辑相对复杂,你应该使用@Security
中的expression表达式。在下面的例子中,用户若要访问控制器的页面,那么他的email必须匹配Post
对象中的getAuthorEmail
方法所返回的值:
1 2 3 4 5 6 7 8 9 10 11 12 | 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)
{
// ...
} |
注意这时我们利用了ParamConverter,它可以自动查询出所需之Post
对象并将其作为$post
参数(传入控制器)。这便令在表达式中使用$post
变量成为可能。
但这有一个主要缺点:在annotation中定义的表达式,很难被程序的其他部分复用。试想你要在模板中添加一个链接,只有作者本人能看到。此时你必须用Twig语法来重复表达式代码:
最容易的解决办法是 - 如果你的逻辑够简单 - 添加一个新的方法给Post
entity,用于检查当前用户是否是贴子作者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 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();
}
} |
现在你可以在模板和表达式中同时使用这个方法了:
1 2 3 4 5 6 7 8 9 10 11 | 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)
{
// ...
} |
上面用到@Security
的例子,仅在我们使用ParamConverter时才能工作,是它让表达式能够访问post
变量。如果你不使用参数转换或者处于更高端的使用场景中,那么你始终可以利用PHP作完全相同的安全检查:
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 | /**
* @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())) {
$this->denyAccessUnlessGranted('edit', $post);
// or without the shortcut(或者不使用快捷写法):
//
// use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...
//
// if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
// throw $this->createAccessDeniedException();
// }
}
// ...
} |
如果你的Security逻辑比较复杂,而且不能“中心化”到诸如isAuthor()
这样的方法中时,你应该利用自定义voters。这是个比ACLs容易了几何级数倍的选择,却给了你在几乎所有场合所需要的灵活性。
首先要创建一个voter类。以下例程展示了与前例用过的getAuthorEmail
相同的逻辑:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | namespace AppBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
use AppBundle\Entity\Post;
class PostVoter extends Voter
{
const CREATE = 'create';
const EDIT = 'edit';
/**
* @var AccessDecisionManagerInterface
*/
private $decisionManager;
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::CREATE, self::EDIT))) {
return false;
}
if (!$subject instanceof Post) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
/** @var Post */
$post = $subject; // $subject must be a Post instance, thanks to the supports method
if (!$user instanceof UserInterface) {
return false;
}
switch ($attribute) {
case self::CREATE:
// if the user is an admin, allow them to create new posts
// 如果用户是admin,允许创建新贴
if ($this->decisionManager->decide($token, array('ROLE_ADMIN'))) {
return true;
}
break;
case self::EDIT:
// if the user is the author of the post, allow them to edit the posts
// 如果用户是贴子作者,允许编辑贴子
if ($user->getEmail() === $post->getAuthorEmail()) {
return true;
}
break;
}
return false;
}
} |
要在程序中启用security voter,要新建一个服务:
1 2 3 4 5 6 7 8 9 | # app/config/services.yml
services:
# ...
post_voter:
class: AppBundle\Security\PostVoter
arguments: ['@security.access.decision_manager']
public: false
tags:
- { name: security.voter } |
现在,你可以在@Security
注释中使用这个voter了:
1 2 3 4 5 6 7 8 | /**
* @Route("/{id}/edit", name="admin_post_edit")
* @Security("is_granted('edit', post)")
*/
public function editAction(Post $post)
{
// ...
} |
你也可以通过security.authorization_checker
服务直接使用它,或者通过控制器的快捷方法来使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /**
* @Route("/{id}/edit", name="admin_post_edit")
*/
public function editAction($id)
{
$post = ...; // query for the post
$this->denyAccessUnlessGranted('edit', $post);
// or without the shortcut(不使用快捷方法时的代码如下):
//
// use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...
//
// if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
// throw $this->createAccessDeniedException();
// }
} |
由Symfony社区开发的FOSUserBundle,为Symfony添加了对基于数据库的用户系统的支持。同时提供了很多常用功能,比如用户注册或忘记密码等相关操作。
开启Remember Me功能让你的用户可以长期保持登陆状态。
当网站对客户提供支持时,以不同的用户来访问程序是必要的,只有这样你才可以发现(他们遇到的)问题。Symfony提供了令你impersonate users(扮演用户)的能力。
如果你的公司使用的登陆方式不被Symfony支持,你可以开发自定义user provider和自定义authentication provider。
本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。