(Security)安全系统如何从数据库中读取用户-Entity Provider(2.7)

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:

为了让事情变得精简,一些getter和setter方法就没有显示。但是你可以用下面的命令生成:

下面,创建数据库表

 

什么是UserInterface?

到目前为止,他只是一个普通的实体。但是为了他能够在安全系统中使用就必须要实现UserInterface。这迫使我们要有以下五种方法:

想了解更多,就要查看 UserInterface.

 

什么是序列化和反序列化方法呢?

在每一个请求结束,用户对象会被序列化到session。在下一个请求,他会反序列化。要帮助 PHP 正确做到这一点,您需要实现Serializable。但你不必序列化所有东西:你只需要几个字段(如果你想在这个基础上加一些额外的东西,你就要实现 AdvancedUserInterface)。对于每个请求,这个id用来从数据库中查询User对象。

想要去了解更多吗?请查看下面的 《了解序列化和用户是怎样保存在session中的》

 

2)配置 security.yml 加载你的实体

现在,你有一个User实体实现了UserInterface,你需要用security.yml文件告诉symfony安全系统。在这个例子中,用户将输入用户名和密码通过HTTP基本验证。symfony会查询和用户名匹配的User实体,然后检查密码(通常检查密码的用时较短):

首先,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库:

 

创建第一个用户

要添加用户,你需要实现一个注册表单或者用fixtures添加一些。这是一个正常的实体,所以没有什么猫腻,只是你需要对每个用户的密码进行加密。不过不用担心,symfony会给你一个能做此事的服务(service ),查看 Dynamically Encoding a Password

下面是从 MySQL 中导出的 app_users 表,包含了用户 admin 和密码 admin (密码是加密过的)。

 注释:是不是还要添加一个Salt属性?不需要,因为你使用bcrypt。所有的密码必须用一个 salt 进行哈希处理,但是 bcrypt 内部做了这件事。由于本教程使用 bcrypt ,User 中的 getSalt() 方法只能返回空值(它没有被使用)。如果你使用了一个不同的算法,您需要在User对象中取消对 salt 行的注释,并且添加一个持久的salt 属性。

 

禁止不活跃的用户(AdvancedUserInterface)

如果用户的isActive属性被设置成false(即is_active在数据库中就是0),用户仍然可以正常登陆网站。这很容易解决。

禁止不活跃的用户,需要你的User类修改为实现AdvancedUserInterface。他继承了 UserInterface,所以你只需要新的接口方法。

这个AdvancedUserInterface接口需要添加四个额外的方法来验证账户状态:

如果他们都返回false,用户将不能允许登录进来。你可以选择持久化所有的这些属性或者挑选你需要的(在这个例子中,数据库中只有isActive)。
那么,他们方法之间的区别是什么?每个方法都会返回一个不同的错误信息(你在登录模板进一步定制这些信息时,这些信息都可以被翻译)。

如果你使用AdvancedUserInterface,你还需要把这些属性(例如isActive)添加到serialize()和unserialize()方法中去。如果你不这样做,您的用户可能无法从每个请求上的session中正确反序列化。

恭喜,你已经完成了从数据库中加载数据到安全系统的所有配置!接下来,如果你像添加一个真正的 login form 来代替http basic,就需要阅读别的文章了。

 

使用自定义查询加载用户

如果一个用户能够使用用户名或者邮箱登录那就太好了,因为二者在数据库中都是唯一的。不幸的是,原生的entity provider仅仅只能通过单个用户属性来处理查询。

想要二者都可以登陆,就需要你的UserRepository去实现一个特殊的 UserProviderInterface.这个接口需要三个方法:loadUserByUsername($username), refreshUser(UserInterface $user), 和supportsClass($class):

有关这些方法的详细信息,请参阅 UserProviderInterface.

别忘了将 repository 类添加到实体并映射 mapping definition of your entity.

只需在 security.yml 中移除用户提供者的 property 键值。

告诉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方法很容易的替代之前的属性检查,并且你能检查所有你想要的属性。你一定要明白这一点,要不你没有必要实现这个接口或者你也不用关心他。

发表评论