> Symfony中文手册 > 怎样从数据库加载安全用户

怎样从数据库加载安全用户

Symfony的安全系统( security system )可以从任何地方加载用户 – 比如从数据库、从 Active Directory 或是 OAuth服务 等。本文向你展示如何通过Doctrine entity从数据库中加载用户信息。

概述 ¶

如果你需要一个登录表单,并且把用户存到某种类型的数据库中,那么你应该考虑使用 FOSUserBundle,它帮助你建立 User 对象,还提供了常见任务所需的的路由和控制器,包括登录、注册、找回密码等。

通过Doctrine entity加载用户需要2个基本步骤:

  1. 创建你的User Entity

  2. 配置 security.yml 加载你的Entity

之后,你可以通过 禁用不活跃用户,使用自定义查询 和 把用户序列化到session 了解更多。

1)创建你的User Entity ¶

此刻,假设你在 AppBundle 中已经有了一个 User entity,包含如下字段:idusernamepasswordemailisActive

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 |
+----+----------+--------------------------------------------------------------+--------------------+-----------+

你是否需要Salt属性?

如果你使用 bcrypt,那么不需要。虽然所有的密码必须用一个 salt 进行哈希处理,但是 bcrypt 在内部做好了。由于本教程确实 使用了 bcryptUser 中的 getSalt() 方法可以直接返回null 值(它未被使用)。如果你使用了其他算法,就需要在 User 对象中取消对 salt 行的注释,并添加一个需要持久化(译注:即,入库)的 salt 属性。

禁用不活跃用户(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 不要 自动查询用户。相反,当有人登录时,将会调用 UserRepositoryloadUserByUsername()方法。

理解序列化并理解“如何把用户存入session” ¶

如果你关心 User 类中 serialize() 方法的重要性,以及User对象是如何序列化或反序列化的,那么本小节属于你。如果你不关心,跳过此段。

一旦用户登录进来,整个User对象会序列化到session中。接下来的请求中,User对象被反序列化。然后,id 属性的值用于数据库中 “最新User对象” 的再查询。最后,新的User对象与反序列化的User对象进行比较,以确保它们呈现的是同一用户。例如,如果由于某种原因,两个User对象上的 username 不匹配,出于安全原因,该用户将被注销。

尽管这一切都是自动触发,但也有一些严重的副作用。

首先,Serializable 接口及其 serializeunserialize方法都被添加,以允许User类能够被序列化到session之中。这可能是,也可能不是根据你的设置来完成的,但却是一个好主意。理论上,只有 id 才需要序列化,因为 refreshUser() 方法在每一次请求中,正是通过 id (如上所述) 来刷新用户。它会给我们一个 "fresh"(新鲜的) 用户对象。

但是在Symfony中,还要使用 username, salt, 和 password 来验证用户在(两次)请求之间未有发生改变 (如果你这样做,系统会调用 AdvancedUserInterface 中的方法)。若(对)这些(属性的)序列化失败,可能会导致你在每次请求时被注销。如果你的User实现的是 EquatableInterface,不同于检查这些属性,你的 isEqualTo() 方法将被调用,你可以随需检查任何属性。除非你理解此点,否则没有必要实现这个接口,也不用关心它。