symfony的安全系统可以从任何地方加载用户 – 一个数据库和一个OAuth服务等。这一章告诉我们如何使用Doctrine entity从数据库加载用户信息。
概述:再开始之前,你应该看看FOSUserBundle。这个外部的bundle允许你从数据库加载用户(就像本文要讲的),并为您提供了内置的路由和控制器等,来完成登陆,注册,忘记密码。但是,如果你的用户系统需要大量功能或者你想学学他是如何工作的,本教程正好讲解这些。
通过Doctrine entity 加载用户需要2个步骤:
1.创建你自己的用户实体(User Entity)
2. 配置 security.yml 加载你的实体(Entity)
之后,您还可以了解更多例如:禁止不活跃的用户,使用自定义查询和把用户序列化到session
1)创建你自己的用户实体(User Entity)
假设你在AppBundle中
已经有了一个User实体,它含有一下字段: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 |
// 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 // $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 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, // see section on salt below // $this->salt, )); } /** @see \Serializable::unserialize() */ public function unserialize($serialized) { list ( $this->id, $this->username, $this->password, // see section on salt below // $this->salt ) = unserialize($serialized); } } |
为了让事情变得精简,一些getter和setter方法就没有显示。但是你可以用下面的命令生成:
1 |
$ php app/console doctrine:generate:entities AppBundle/Entity/User |
下面,创建数据库表
1 |
$ php app/console doctrine:schema:update --force |
什么是UserInterface?
到目前为止,他只是一个普通的实体。但是为了他能够在安全系统中使用就必须要实现UserInterface。这迫使我们要有以下五种方法:
想了解更多,就要查看 UserInterface
.
什么是序列化和反序列化方法呢?
在每一个请求结束,用户对象会被序列化到session。在下一个请求,他会反序列化。要帮助 PHP 正确做到这一点,您需要实现Serializable。但你不必序列化所有东西:你只需要几个字段(如果你想在这个基础上加一些额外的东西,你就要实现 AdvancedUserInterface)。对于每个请求,这个id用来从数据库中查询User对象。
想要去了解更多吗?请查看下面的 《了解序列化和用户是怎样保存在session中的》
2)配置 security.yml 加载你的实体
现在,你有一个User实体实现了UserInterface,你需要用security.yml文件告诉symfony安全系统。在这个例子中,用户将输入用户名和密码通过HTTP基本验证。symfony会查询和用户名匹配的User实体,然后检查密码(通常检查密码的用时较短):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 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 # manager_name: customer firewalls: default: pattern: ^/ http_basic: ~ provider: our_db_provider # ... |
首先,encoders部分告诉symfony数据库中的密码使用bcrypt方式编码。第二,providers部分创建了一个叫our_db_provider的”user provider“,他知道username属性是AppBundle:User实体的。这个our_db_provider名称并不重要:她仅仅是防火墙要匹配provider键的一个值。或者,如果您没有在防火墙下设置 provider 键,第一个 “user provider” 会被自动使用。
如果你使用的是php5.4或者更低,想要使用bcrypt方式编码,
你需要通过composer安装ircmaxell/password-compat库:
1 2 3 4 5 6 |
{ "require": { ... "ircmaxell/password-compat": "~1.0.3" } } |
创建第一个用户
要添加用户,你需要实现一个注册表单或者用fixtures添加一些。这是一个正常的实体,所以没有什么猫腻,只是你需要对每个用户的密码进行加密。不过不用担心,symfony会给你一个能做此事的服务(service ),查看 Dynamically Encoding a Password。
下面是从 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 内部做了这件事。由于本教程使用 bcrypt ,User 中的 getSalt() 方法只能返回空值(它没有被使用)。如果你使用了一个不同的算法,您需要在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 |
// 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中正确反序列化。
恭喜,你已经完成了从数据库中加载数据到安全系统的所有配置!接下来,如果你像添加一个真正的 login form 来代替http basic,就需要阅读别的文章了。
使用自定义查询加载用户
如果一个用户能够使用用户名或者邮箱登录那就太好了,因为二者在数据库中都是唯一的。不幸的是,原生的entity provider仅仅只能通过单个用户属性来处理查询。
想要二者都可以登陆,就需要你的UserRepository去实现一个特殊的 UserProviderInterface
.这个接口需要三个方法:loadUserByUsername($username)
, refreshUser(UserInterface $user)
, 和supportsClass($class)
:
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 |
// src/AppBundle/Entity/UserRepository.php namespace AppBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Doctrine\ORM\EntityRepository; class UserRepository extends EntityRepository implements UserProviderInterface { public function loadUserByUsername($username) { $user = $this->createQueryBuilder('u') ->where('u.username = :username OR u.email = :email') ->setParameter('username', $username) ->setParameter('email', $username) ->getQuery() ->getOneOrNullResult(); if (null === $user) { $message = sprintf( 'Unable to find an active admin AppBundle:User object identified by "%s".', $username ); throw new UsernameNotFoundException($message); } return $user; } public function refreshUser(UserInterface $user) { $class = get_class($user); if (!$this->supportsClass($class)) { throw new UnsupportedUserException( sprintf( 'Instances of "%s" are not supported.', $class ) ); } return $this->find($user->getId()); } public function supportsClass($class) { return $this->getEntityName() === $class || is_subclass_of($class, $this->getEntityName()); } } |
有关这些方法的详细信息,请参阅 UserProviderInterface
.
别忘了将 repository 类添加到实体并映射 mapping definition of your entity.
只需在 security.yml 中移除用户提供者的 property 键值。
1 2 3 4 5 6 7 8 |
# app/config/security.yml security: # ... providers: our_db_provider: entity: class: AppBundle:User |
告诉symfony不要自动查询User。相反,当有人登录时,UserRepository的loadUserByUsername()方法将会被调用。
了解序列化和如何保持用户到session
如果你关心在User类中 serialize() 方法的重要性和如何将用户对象序列化或反序列化,那么这一节适合于你。如果你不关心,那么你就可以跳过这一章。
一旦用户登录,整个用户对象会序列化到session。在接下来的请求,用户对象反序列化。然后,id 属性的值是用来从数据库中查询一个新的用户对象。最后,新的用户对象与反序列化的用户对象进行比较,以确保它们表示相同的用户。例如,如果由于某种原因,两个用户对象上的username不匹配,则出于安全原因,该用户将被注销。
尽管这一切都自动触发,但也有一些副作用。
首先, Serializable接口和他自己的serialize、
unserialize方法都被添加到允许的User类,并完成序列化到session。这可能是,也可能不是根据您的设置来完成的,但是他是一个好主意。从理论上讲,只有id才需要序列化,因为refreshUser()方法通过id刷新每个请求
(如上所述)。这给我们一个 "fresh" 用户对象。
但是在symfony中,他还使用 username
, salt
, 和password验证用户请求之间没有改变(如果这样实现,它也会调用你的AdvancedUserInterface 方法)。未能序列化,这些可能会导致你被注销。如果您的User实现了EquatableInterface,你使用isEqualTo方法很容易的替代之前的属性检查,并且你能检查所有你想要的属性。你一定要明白这一点,要不你没有必要实现这个接口或者你也不用关心他。