在以往的工厂方法中,大量的工厂类增加了类之间的调用依赖关系的复杂度,开发者需要经常去处理这种依赖,随着工厂类的不断增多,这种依赖关系处理中就难免有些疏忽,导致一些错误的产生。此外,在单元测试中,由于具体的功能单元被封装在了每一个工厂类的内部,我们需要深入到类的内部进行测试,如果这些功能单元中有依赖其他类的部分,我们还需要进行修改,待测试完毕后还需要进行还原。
既然工厂类是作为基础类库角色,那么对于客户定制化的业务功能就需要从具体的工厂类中剥离出来,但仍旧保留通用的处理流程。也就说,基础类之间的依赖关系被剥离了出来。我们以往开发过程中那种应用层类依赖底层基础类的模式被颠倒了过来,变成基础类对应用层类依赖,也就是说依赖发生了反转,称为“控制反转”( IOC ,Inversion ofControl)。
我们经常使用的回调机制就是一种IOC思想的体现。比如视窗系统为控件提供的消息机制,消息的接收、判断、触发等工作由系统本身提供,对于消息处理的具体流程由用户来决定,我们可以通过事件订阅器来为每一个控件设置具体的处理方法。
框架抽象了通用流程,对成熟的、稳定的流程进行了封装。对于开发者来说通过框架提供的规范(比如子类化抽象类或者实现相关接口,实现相关的交互协议和接口)就可以将客户化的代码植入具体的流程中,实现具体场景下客户定制需求。
依赖注入(DI,Dependency Injection)是控制反转原则的一种具体实现方式,也就是通过注入的方式实现依赖关系的说明,框架在运行处理到对应位置时,根据注入的参数去调用具体的处理方法。Symfony对依赖注入做了很好的实现,整个框架的代码组织就是以这种方式来实现的。下面我们主要讲解Symfony中Security的实现过程。
Symfony的Security系统功能强大超出你的想象,但是它的配置也容易让人产生混乱。下面我们一步步讲解如何在你的应用中进行设置,从配置防火墙到如何加载用户以拒绝访问和获取用户对象,依赖你需要做什么,有时候仅仅需要做一些简单的初始化设置,一旦完成这些,Symfony的Security系统就会非常灵活和高效的工作起来。主要步骤如下:
对security系统的进行配置的文件是:app/config/security.yml。一般默认的配置如下:
#app/config/security.yml
security:
providers:
custom_provider:
in_memory:
memory: ~
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)
security: false
default:
aynonymous: ~
关键字“firewalls”是security配置核心,firewalls配置中关键字“dev”不是必要的,主要是为了确保Symfony的开发者工具不会被security系统所阻塞。
firewalls中default配置项对所有的URL请求进行了过滤(如果没有设置pattern意味着将匹配所有的URL字符串)关键字aynonymous关心是URL的认证问题。现在以开发者模式打开主页,你就可以看到当前的你是以anon.通过认证的,也就是说你只是一个匿名用户。如下图所示:
后面会详细介绍如何过滤确定的URL或者controller。
security是高可配的,在“Security配置参考”中详细地介绍了可选项及其解释。
防火墙firewalls的作用就是配置如何使用你的User进行认证,是使用登录表单(login form)吗?还是使用HTTP协议提供的基本认证?还是使用一个API令牌?或者以上所有的方式?
首先,我们使用HTTP协议提供的基本认证。通过为firewalls配置项增加http_basic来进行激活:
#app/config/security.yml
security:
#...
firewalls:
#...
default:
aynonymous: ~
http_basic: ~
就是如此简单,尝试着做一下。你会看到一个需要输入用户名和密码进行登录的页面。为了更加有趣,我们可以创建一个URL为/admin的页面。下面的例子中,如果你是使用annotations的方式,可以这样写:
// src/AppBundle/Controller/DefaultController.php
// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class DefaultController extends Controller
{
/**
* @Route("/admin")
*/
public function adminAction()
{
return new Response('Admin page!');
}
}
接着,在“security.yml”配置文件中增加access_control入口,要求用户在访问/admin页面时进行登录。
# app/config/security.yml
security:
# ...
firewalls:
# ...
default:
# ...
access_control:
# require ROLE_ADMIN for /admin*
- { path: ^/admin, roles: ROLE_ADMIN }
现在,你可以访问/admin页面,就会看到HTTP的基本认证对话框:
但问题是,你要使用什么进行登录?这个用户来自哪里?
当你在对话框中输入用户名之后,Symfony需要从一个叫做“user provider”中加载你的用户信息,同样地需要进行配置。Symfony内建了一种方式可以从数据库加载用户吗,或者你也可以创建你自己的“userprovider”。
最容易的方式(通常被限制)就是配置Symfony从它自身的“security.yml”中获取硬编码的用户。这被称为一种“in memory”驱动provider,但是最好的方式是创建一个“in configuration”驱动provider:
# app/config/security.yml
security:
providers:
in_memory:
memory:
users:
ryan:
password: ryanpass
roles: 'ROLE_USER'
admin:
password: kitten
roles: 'ROLE_ADMIN'
#...
就像firewalls一样,你可以拥有多个驱动providers,但是你只能使用其中一种。如果你有多个,你可以通过在防火墙firewall中provider关键字中指定一个要使用的驱动provider(比如,provider: in_memory)。
尝试使用用户名“admin”密码“kitten”进行登录,你将会看到一个错误!
为了修正这个错误,增加一个encoders关键字:
# app/config/security.yml
security:
# ...
encoders:
Symfony\Component\Security\Core\User\User: plaintext
# ...
用户驱动User providers用于加载用户信息,并将其放入一个用户对象中Userobject。如果你是通过数据库或者其他数据源加载用户,你需要创建你自己的用户类来完成这个过程。但是,当你使用“inmemory”驱动时,系统内置了一个用户类用于实现这个过程:“Symfony\Component\Security\Core\User\Userobject”。
无论你的用户类是什么样的,你都需要告诉Symfony用于加密用户密码所使用的加密算法。在这个例子中,密码采用的是明文的方式。但是接下来,你可以采用bcrypt的加密算法。
现在刷新一下,你就可以完成的登录了。浏览器页面中的调试工具栏甚至会告诉你你是谁以及你当前所用的角色。
因为这个URL地址要求的角色是ROLE_ADMIN,所以如果你是用ryan用户进行登录,系统将拒绝你的访问请求。
从数据库加载用户
主要有两步:一是创建你的User实体;二是配置“security.yml”是系统从你的User实体中进行加载。
①创建User实体
假设你的AppBundle中已经有了一个User实体,并且包含这些字段:id,username, password, email和isActive。
// 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的方法省略掉了。你可以通过下面的方式进行创建:
$ php bin/console doctrine:generate:entities AppBundle/Entity/User
接下来,确保与之对应的数据表也创建成功:
$ php bin/console doctrine:schema:update –force
什么是用户接口UserInterface呢?
到目前为止,我们仅仅只是创建了一个正常的数据实体,但是要在Security系统中使用这个类,还需要实现UserInterface,必须强制实现下面的5个方法:
•getRoles()
•getPassword()
•getSalt()
•getUsername()
•eraseCredentials()
序列化和反序列化方法做了什么?
在一个请求结束后,用户对象经过序列化后保存到会话中,当下一个请求到来时,就会进行反序列化操作。为了使PHP正确地完成这些,你需要自己实现Serialize方法,但是也不是序列化全部,仅仅对你需要的那些字段进行操作即可。在每一个请求中,通过id字段从数据库中查询用户信息并刷新User对象
②配置Security从你的User实体进行加载
现在,你已经拥有了一个实现了UserInterface 的User实体,你仅需要在“security.yml”文件中告诉Symfony的安全系统。在这个例子中,用户通过HTTP基本认证窗口中输入他们的用户名和密码,Symfony会查询User实体,对用户名进行匹配,并对密码进行校验:
# 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:
main:
pattern: ^/
http_basic: ~
provider: our_db_provider
# ...
首先,encoders配置项告诉Symfony用于数据库中用户密码采用bcrypt加密算法。其次,providers配置项创建了一个叫做our_db_provider 的用户驱动,它告诉了系统通过username从AppBundle:User实体中进行查询。our_db_provider命名不是很重要,只需要和firewall中的provider的键值保持一致。活着你也不用设置provider的键值,系统会自动使用第一个用户驱动。
在数据库中创建一个用户记录。
什么情况下设置Salt属性?所有的密码都会使用一个salt进行哈希计算。如果你使用了bcrypt算法进行加密,其内部已经实现了这种计算,User中的getSalt()方法返回为null(也就是说不使用)。如果你使用了不同的加密算法,那么你就需要增加一个长久的salt属性。
禁止非激活用户。如果一个用户isActive属性为false(数据库中字段值为0),这个用户仍然可以正常地进行登录。为了排除inactive用户,你需要对User类进行修改使其实现AdvancedUerInterface接口,这个接口扩展自UserInterface。
添加一个登录表单页面,替换掉HTTP协议的验证机制。
无论你的用户是存储在“security.yml”文件中,还是存储在数据库或者其他什么地方,最好的加密算法是bcrypt:
# app/config/security.yml
security:
# ...
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
cost: 12
现在,你的用户密码就需要使用实际的加密算法进行编码,对于硬编码的用户来说,可以使用在建的命令来实现:
$ php bin/console security:encode-password
它将会产生下面的东西:
# app/config/security.yml
security:
providers:
in_memory:
memory:
users:
ryan:
password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli
roles: 'ROLE_USER'
admin:
password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G
roles: 'ROLE_ADMIN'
#...
接下来工作和之前一样。如果你是使用动态的用户(比如来自数据库中用户)。如何在用户信息插入到数据库之前,通过编程的方式加密用户密码?看一看动态加密密码的细节。
祝贺你,你现在已经拥有了一个使用HTTP的基本验证,从“security.yml”文件中加载用户的验证系统,接下来的工作依赖于你的具体设置:
为用户登录配置一种不同方式,比如一个登录表单或者其他完全客制化的方式。
从不同的数据源加载用户,比如从数据库或者其他数据源。
学习如何拒绝访问,加载用户对象,处理认证的角色。
现在,用户可以使用HTTP基本的验证机制或其他方法进行登录。很好,你现在需要学习如何拒绝访问,与用户对象进行交互,这个称为认证。它的工作决定了一个用户是否可以访问一些资源(URL、模型或者方法等等)。
认证的过程主要包括两个方面:
①登录的时候用户接收到明确的用户角色集合;
②添加代码(URL,控制器)指明可用于访问的属性(通常为ROLE_ADMIN)。
角色
当用户登录时,系统会自动获取一个用户角色信息的集合,在上面的例子中,是以硬编码的方式存储在“security.yml”文件中。如果你是从数据库中加载的用户,那么这些信息存储在表中的一个列中。
注意:所有指定的用户角色必须以“ROLE_”为前缀,否则,Symfony的安全系统不能够正确处理。
角色很简单,就是基本的字符串。假设你需要对你的网站中博客管理功能限制访问,你可以使用“ROLE_BLOG_ADMIN”角色。
注意:确保每一个用户至少拥有一个角色,否则你的用户不能够通过验证,通常的惯例是给每一个用户分配ROLE_USER角色。
添加拒绝访问的代码
这里主要有两种方式:
①通过配置security.yml文件中access_control,这种方式比较简单,但缺乏灵活性。
②通过在security.authorization_checker服务加入代码进行控制。
Symfony认证机制处理依赖于“User Provider”,当用户提交了用户名和密码是,认证层向配置的用户驱动(User provider)请求一个带有username的用户对象。然后,Symfony检查该用户的密码是否正确,并生成一个token用于在当前会话中保持认证状态。除此之外,Symfony还拥有“in_memory”和 “entity”的用户驱动。如果你的用户是通过数据库、文件等当时提供的,那么你就有必要创建自己的User Provider。
无论你的用户数据来自哪里,你都要先创建一个User类。无论你想要的User类是什么样的或者包含哪些数据,唯一的要求就是实现了UserInterface。这些需要在User类定义中方法包括:getRoles()、getPassword()、getSalt()、getUsername()、eraseCredentials()。实现EqualtableInterface也是比较有用的,它定义了一个用户检查用户是否为当前已登录用户的方法,就是isEqualTo()。
class CurrentUser implements AdvancedInterface, EqualtableInterface{
private $username;
private $password;
private $salt;
private $roles;
public function setData(array $user){}
public function getRoles(){}
public function getPassword(){}
public function getSalt(){}
public function getUsername(){}
public function eraseCredentials(){}
public function isEqualTo(UserInterface $user){}
}
有了User类(前面示例代码中CurrentUser)之后,你可以创建一个User Provider,用于获取从web服务(如UserService)中获取用户信息。User Provider类必须实现UserProviderInterface接口,主要包括三个方法:loadUserByUsername($username),refreshUser(UserInterface $user)和supportClass($class)。更多的细节,详见UserProviderInterface。下面给出例子:
class UserProvider implements UserProviderInterface{
public function loadUserByUserName($username){
// 调用你定制关于用户的服务,获取用户数据
$user = $this->getUserService()->getUserByLoginField($username);
// pretend it returns an array on success
if(empty($user)){
throw new UsernameNotFoundException(...);
}
$currentUser = new CurrentUser();
$currentUser->setData($user);
return $currentUser;
}
public function refreshUser(UserInterface){}
}
}