怎样从数据库加载安全用户
Symfony的安全系统( security system )可以从任何地方加载用户 – 比如从数据库、从 Active Directory 或是 OAuth服务 等。本文向你展示如何通过Doctrine entity从数据库中加载用户信息。
概述 ¶
如果你需要一个登录表单,并且把用户存到某种类型的数据库中,那么你应该考虑使用 FOSUserBundle,它帮助你建立 User
对象,还提供了常见任务所需的的路由和控制器,包括登录、注册、找回密码等。
通过Doctrine entity加载用户需要2个基本步骤:
创建你的User Entity
配置 security.yml 加载你的Entity
之后,你可以通过 禁用不活跃用户,使用自定义查询 和 把用户序列化到session 了解更多。
1)创建你的User Entity ¶
此刻,假设你在 AppBundle
中已经有了一个 User
entity,包含如下字段:id
,username
,password
,email
和 isActive
:
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Table(name="app_users")
* @ORM\Entity(repositoryClass="AppBundle\Entity\UserRepository")
*/
class User implements UserInterface, \Serializable
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
* @ORM\Column(type="string", length=64)
*/
private $password;
/**
* @ORM\Column(type="string", length=60, unique=true)
*/
private $email;
/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
public function __construct()
{
$this->isActive = true;
// may not be needed, see section on salt below
// 非必须,参考下文中的salt讲解
// $this->salt = md5(uniqid(null, true));
}
public function getUsername()
{
return $this->username;
}
public function getSalt()
{
// you *may* need a real salt depending on your encoder
// see section on salt below
// 根据你的encoder,你 *可能* 需要一个真正的 salt,参考下文中的salt讲解
return null;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
/** @see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
// 参考下文中的salt讲解
// $this->salt,
));
}
/** @see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// 参考下文中的salt讲解
// $this->salt
) = unserialize($serialized);
}
} |
为了让事情简单化,一些getter和setter方法没有显示出来。但是你可以使用下面的命令 生成 它们:
1 |
$ php bin/console doctrine:generate:entities AppBundle/Entity/User |
接下来,确保 创建了数据库表:
1 |
$ php bin/console doctrine:schema:update --force |
什么是UserInterface? ¶
到目前为止,这只是一个普通的entity。但要在security系统中使用它,就必须实现 UserInterface
接口。这强制(user)类要有以下五种方法:
getRoles()
getPassword()
getSalt()
getUsername()
eraseCredentials()
每个方法的具体内容,参考 UserInterface
。
序列化和反序列化方法能做什么? ¶
每一次请求结束,User对象被序列化到session中。下一次请求时,它被反序列化。要帮助 PHP 正确做到这一点,你需要实现 Serializable
接口。但你毋须序列化每一样东西:只需要几个字段(就是上面那些。如果你决定实现 AdvancedUserInterface 接口,还会有几个额外字段)。每次请求,id
用于从数据库中查询出最新的User对象。
想了解更多?见下文 了解序列化和用户是怎样保存在session中的。
2)配置security.yml以加载你的entity ¶
现在,你有了一个实现了 UserInterface
接口的User
entity,你只需要在 security.yml
中对symfony的security系统进行告之。
本例中,用户将通过Http Basic认证来输入用户名和密码。Symfony将查询和用户名相匹配的User
entity,然后检查密码(马上会深入密码部分):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# app/config/security.yml
security:
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
# ...
providers:
our_db_provider:
entity:
class: AppBundle:User
property: username
# if you're using multiple entity managers
# 如果你使用了多个 entity managers,那么:
# manager_name: customer
firewalls:
main:
pattern: ^/
http_basic: ~
provider: our_db_provider
# ... |
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 |
<!-- 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="AppBundle\Entity\User" algorithm="bcrypt" />
<!-- ... -->
<provider name="our_db_provider">
<!-- if you're using multiple entity managers, add:
manager-name="customer" -->
<entity class="AppBundle:User" property="username" />
</provider>
<firewall name="main" pattern="^/" provider="our_db_provider">
<http-basic />
</firewall>
<!-- ... -->
</config>
</srv:container> |
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 |
// app/config/security.php
$container->loadFromExtension('security', array(
'encoders' => array(
'AppBundle\Entity\User' => array(
'algorithm' => 'bcrypt',
),
),
// ...
'providers' => array(
'our_db_provider' => array(
'entity' => array(
'class' => 'AppBundle:User',
'property' => 'username',
),
),
),
'firewalls' => array(
'main' => array(
'pattern' => '^/',
'http_basic' => null,
'provider' => 'our_db_provider',
),
),
// ...
)); |
首先,encoders
部分告诉Symfony应预期“数据库中的密码将使用 bcrypt
加密”。第二,providers
部分创建了一个名为 our_db_provider
的“user provider”,它知道从你的 AppBundle:User
entity 中利用 username
属性进行查询。our_db_provider
名称并不重要:它仅需匹配firewall下面的 provider
键的值。或者,如果你没在防火墙下设置 provider
键,则自动使用第一个 “user provider”(的键名)。
创建你的第一个用户 ¶
要添加用户,你需要实现一个 注册表单 或者添加一些 fixtures。这是只是一个普通的entity,所以没有什么高级技巧,除了 你需要对每位用户的密码进行加密之外。不用担心,Symfony会给了你一个服务(service )来完成此事,参考 如何手动对密码加密 以了解细节。
下面是从 Mysql 中导出的 app_users
表,包含了用户 admin
和密码 admin
(已加密)。
1 2 3 4 5 6 |
$ mysql> SELECT * FROM app_users;
+----+----------+--------------------------------------------------------------+--------------------+-----------+
| id | username | password | email | is_active |
+----+----------+--------------------------------------------------------------+--------------------+-----------+
| 1 | admin | $2a$08$jHZj/wJfcVKlIwr5AvR78euJxYK7Ku5kURNhNx.7.CSIJ3Pq6LEPC | admin@example.com | 1 |
+----+----------+--------------------------------------------------------------+--------------------+-----------+ |
禁用不活跃用户(AdvancedUserInterface) ¶
如果用户的 isActive
属性被设置成 false
(即 is_active
在数据库中是 0
),用户仍然可以正常登陆网站。这比较灵活。
要排除不活跃的用户,你要把 User
类修改为去实现 AdvancedUserInterface
接口。它继承自 UserInterface
,所以你只需此新接口(中的方法)。
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 |
// src/AppBundle/Entity/User.php
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
// ...
class User implements AdvancedUserInterface, \Serializable
{
// ...
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isCredentialsNonExpired()
{
return true;
}
public function isEnabled()
{
return $this->isActive;
}
// serialize and unserialize must be updated - see below
// 序列化和反序列化必须要更新 - 见下
public function serialize()
{
return serialize(array(
// ...
$this->isActive
));
}
public function unserialize($serialized)
{
list (
// ...
$this->isActive
) = unserialize($serialized);
}
} |
AdvancedUserInterface
接口添加了四个额外的方法来验证账户状态:
-
isAccountNonExpired()
检查用户账户是否过期; -
isAccountNonLocked()
检查用户是否被锁定; -
isCredentialsNonExpired()
检查用户凭证(密码)是否已过期; -
isEnabled()
检查用户是否已启用。
如果他们中的任何一个 返回的是 false
,用户将不允许登录进来。你可以选择使用所有这些“已入库”的属性,或者挑选你需要的(本例中,只有 isActive
被从库中取出)。
那么,这些方法之间的区别是什么?每个方法会返回一个不同的错误信息(当你在登录模板中进一步定制这些信息时,它们皆可被翻译)。
如果你使用了 AdvancedUserInterface
,还必须把上述方法要用到的属性(例如isActive
)给添加到 serialize()
和 unserialize()
方法中去。如果你不 这样做,你的用户可能无法从每次请求中session中被正确反序列化。
恭喜!你的“从数据库加载”之security系统已配置完毕!接下来,去添加一个真正的 登录表单 来代替HTTP Basic吧,或者去阅读其他话题。
使用自定义查询加载用户 ¶
如果一个用户既可以通过用户名,又可以通过邮箱来登录那就太好了,因为二者在数据库中都是唯一的。不幸的是,原生的entity provider仅能通过用户的单一属性来进行查询。
要实现它,你的 UserRepository
需要去实现一个特殊的 UserLoaderInterface
。这个接口只需要一个方法:loadUserByUsername($username)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/AppBundle/Entity/UserRepository.php
namespace AppBundle\Entity;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository implements UserLoaderInterface
{
public function loadUserByUsername($username)
{
return $this->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
->getOneOrNullResult();
}
} |
别忘了将 repository 类添加到 entity映射定义。
最后,直接在 security.yml
中移除user provider的 property
键。
1 2 3 4 5 6 7 8 |
# app/config/security.yml
security:
# ...
providers:
our_db_provider:
entity:
class: AppBundle:User |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!-- 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="our_db_provider">
<entity class="AppBundle:User" />
</provider>
</config>
</srv:container> |
1 2 3 4 5 6 7 8 9 10 11 12 |
// app/config/security.php
$container->loadFromExtension('security', array(
// ...
'providers' => array(
'our_db_provider' => array(
'entity' => array(
'class' => 'AppBundle:User',
),
),
),
)); |
如此即告之Symfony 不要 自动查询用户。相反,当有人登录时,将会调用 UserRepository
的 loadUserByUsername()
方法。
理解序列化并理解“如何把用户存入session” ¶
如果你关心 User
类中 serialize()
方法的重要性,以及User对象是如何序列化或反序列化的,那么本小节属于你。如果你不关心,跳过此段。
一旦用户登录进来,整个User对象会序列化到session中。接下来的请求中,User对象被反序列化。然后,id
属性的值用于数据库中 “最新User对象” 的再查询。最后,新的User对象与反序列化的User对象进行比较,以确保它们呈现的是同一用户。例如,如果由于某种原因,两个User对象上的 username
不匹配,出于安全原因,该用户将被注销。
尽管这一切都是自动触发,但也有一些严重的副作用。
首先,Serializable
接口及其 serialize
、unserialize
方法都被添加,以允许User
类能够被序列化到session之中。这可能是,也可能不是根据你的设置来完成的,但却是一个好主意。理论上,只有 id
才需要序列化,因为 refreshUser()
方法在每一次请求中,正是通过 id
(如上所述) 来刷新用户。它会给我们一个 "fresh"(新鲜的) 用户对象。
但是在Symfony中,还要使用 username
, salt
, 和 password
来验证用户在(两次)请求之间未有发生改变 (如果你这样做,系统会调用 AdvancedUserInterface
中的方法)。若(对)这些(属性的)序列化失败,可能会导致你在每次请求时被注销。如果你的User实现的是 EquatableInterface
,不同于检查这些属性,你的 isEqualTo()
方法将被调用,你可以随需检查任何属性。除非你理解此点,否则没有必要实现这个接口,也不用关心它。