Symfony Security 配合数据库进行验证登录

在以往的工厂方法中,大量的工厂类增加了类之间的调用依赖关系的复杂度,开发者需要经常去处理这种依赖,随着工厂类的不断增多,这种依赖关系处理中就难免有些疏忽,导致一些错误的产生。此外,在单元测试中,由于具体的功能单元被封装在了每一个工厂类的内部,我们需要深入到类的内部进行测试,如果这些功能单元中有依赖其他类的部分,我们还需要进行修改,待测试完毕后还需要进行还原。

既然工厂类是作为基础类库角色,那么对于客户定制化的业务功能就需要从具体的工厂类中剥离出来,但仍旧保留通用的处理流程。也就说,基础类之间的依赖关系被剥离了出来。我们以往开发过程中那种应用层类依赖底层基础类的模式被颠倒了过来,变成基础类对应用层类依赖,也就是说依赖发生了反转,称为“控制反转”( IOC ,Inversion ofControl)。

我们经常使用的回调机制就是一种IOC思想的体现。比如视窗系统为控件提供的消息机制,消息的接收、判断、触发等工作由系统本身提供,对于消息处理的具体流程由用户来决定,我们可以通过事件订阅器来为每一个控件设置具体的处理方法。

框架抽象了通用流程,对成熟的、稳定的流程进行了封装。对于开发者来说通过框架提供的规范(比如子类化抽象类或者实现相关接口,实现相关的交互协议和接口)就可以将客户化的代码植入具体的流程中,实现具体场景下客户定制需求。

依赖注入(DI,Dependency Injection)是控制反转原则的一种具体实现方式,也就是通过注入的方式实现依赖关系的说明,框架在运行处理到对应位置时,根据注入的参数去调用具体的处理方法。Symfony对依赖注入做了很好的实现,整个框架的代码组织就是以这种方式来实现的。下面我们主要讲解Symfony中Security的实现过程。

Symfony的Security系统功能强大超出你的想象,但是它的配置也容易让人产生混乱。下面我们一步步讲解如何在你的应用中进行设置,从配置防火墙到如何加载用户以拒绝访问和获取用户对象,依赖你需要做什么,有时候仅仅需要做一些简单的初始化设置,一旦完成这些,Symfony的Security系统就会非常灵活和高效的工作起来。主要步骤如下:

1.安全配置文件

#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.通过认证的,也就是说你只是一个匿名用户。如下图所示:

Symfony Security 配合数据库进行验证登录_第1张图片
匿名用户访问

后面会详细介绍如何过滤确定的URL或者controller。

security是可配的,在“Security配置参考”中详细地介绍了可选项及其解释。

2.角色机制

认证的过程主要包括两个方面:

  • ①登录的时候用户接收到明确的用户角色集合;
  • ②添加代码(URL,控制器)指明可用于访问的属性(通常为ROLE_ADMIN)。

当用户登录时,系统会自动获取一个用户角色信息的集合,在上面的例子中,是以硬编码的方式存储在security.yml文件中。如果你是从数据库中加载的用户,那么这些信息存储在表中的一个列中。

如下所示,以/admin开始URL访问权限授予给了角色ROLE_ADMIN,也就是说只有具备此角色的用户才能访问这些URL

access_control:
        - { path: ^/admin, role: ROLE_ADMIN }
        - { path: /.*, role: IS_AUTHENTICATED_ANONYMOUSLY }

注意:所有指定的用户角色必须以ROLE_为前缀,否则,Symfony的安全系统不能够正确处理。

角色很简单,就是基本的字符串。假设你需要对你的网站中博客管理功能限制访问,你可以使用ROLE_BLOG_ADMIN角色。

注意:确保每一个用户至少拥有一个角色,否则你的用户不能够通过验证,通常的惯例是给每一个用户分配ROLE_USER角色。

角色层次

如下所示,ROLE_ADMIN涵盖了ROLE_TEACHERROLE_BACKEND两个角色,ROLE_ADMIN同时具备了这两个角色的权限,但是ROLE_TEACHERROLE_BACKEND任何一个都不具备ROLE_ADMIN的权限

    role_hierarchy:
        ROLE_TEACHER:     ROLE_USER
        ROLE_BACKEND:     ROLE_USER
        ROLE_ADMIN:       [ROLE_TEACHER, ROLE_BACKEND]

3.配置防火墙,进行安全过滤

工程目录\app\config\security.yml

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
    encoders:
        Scourgen\Services\User\User: sha256

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        # in_memory:
        #     memory: ~
        custom_provider:
            id: scourgen.user_provider

    role_hierarchy:
        ROLE_TEACHER:     ROLE_USER
        ROLE_BACKEND:     ROLE_USER
        ROLE_ADMIN:       [ROLE_TEACHER, ROLE_BACKEND]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            pattern: /.*
            form_login:
                login_path: login
                check_path: login_check
                use_referer: true
                success_handler: scourgen.authentication.success_handler
                failure_handler: scourgen.authentication.failure_handler
            logout:
                path: logout
                success_handler: scourgen.logout.success_handler
                
            anonymous: true
            # activate different ways to authenticate

            # http_basic: ~
            # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # http://symfony.com/doc/current/cookbook/security/form_login_setup.html
    access_control:
        - { path: ^/admin, role: ROLE_ADMIN }
        - { path: /.*, role: IS_AUTHENTICATED_ANONYMOUSLY }

工程目录\app\config\services.yml服务配置文件,框架根据这个参数进行注入:

# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
#    parameter_name: value

services:
#    service_name:
#        class: AppBundle\Directory\ClassName
#        arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
    scourgen.user_provider:
        class:  Scourgen\Services\User\UserProvider
        arguments:  ["@service_container"]

    scourgen.authentication.success_handler:
        class: Scourgen\WebBundle\Handler\AuthenticationSuccessHandler
        parent: security.authentication.success_handler

    scourgen.authentication.failure_handler:
        class: Scourgen\WebBundle\Handler\AuthenticationFailureHandler
        parent: security.authentication.failure_handler

    scourgen.logout.success_handler:
        class: Scourgen\WebBundle\Handler\LogoutSuccessHandler
        parent: security.logout.success_handler

4.创建用户实体

用户实体主要用于Symfony Security进行验证,我们通过从数据库获取用户信息,并实例化为用户实体,然后交给框架安全模块进行验证。需要注意的客制化User必须实现AdvancedUserInterface接口。

工程目录\Scourgen\Services\User\User.php


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Scourgen\Services\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

/**
 * User is the user implementation used by the in-memory user provider.
 *
 * This should not be used for anything else.
 *
 * @author Fabien Potencier 
 */
final class User implements AdvancedUserInterface, EquatableInterface, \ArrayAccess
{
    protected $data;

    // public function __construct($username, $password, array $roles = array(), $enabled = true, $userNonExpired = true, $credentialsNonExpired = true, $userNonLocked = true)
    // {
    //     if ('' === $username || null === $username) {
    //         throw new \InvalidArgumentException('The username cannot be empty.');
    //     }

    //     $this->username = $username;
    //     $this->password = $password;
    //     $this->enabled = $enabled;
    //     $this->accountNonExpired = $userNonExpired;
    //     $this->credentialsNonExpired = $credentialsNonExpired;
    //     $this->accountNonLocked = $userNonLocked;
    //     $this->roles = $roles;
    // }

    public function __toString()
    {
        return $this->getUsername();
    }

    public function __set($name, $value)
    {
        if (array_key_exists($name, $this->data)) {
            $this->data[$name] = $value;
        }
        throw new \RuntimeException("{$name} is not exist in current user.");    
    }

    public function __get($name)
    {
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
        throw new \RuntimeException("{$name} is not exist in current user.");        
    }

    public function __isset($name) {
        return isset($this->data[$name]);
    }

    public function __unset($name) {
        unset($this->data[$name]);
    }

    public function offsetExists ($offset) {
        return $this->__isset($offset);

    }
    public function offsetGet ($offset) {
        return $this->__get($offset);
    }

    public function offsetSet ($offset, $value) {
        return $this->__set($offset, $value);
    }

    public function offsetUnset ($offset) {
        return $this->__unset($offset);
    }

    /**
     * {@inheritdoc}
     */
    public function getRoles()
    {
        return $this->roles;
    }

    /**
     * {@inheritdoc}
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * {@inheritdoc}
     */
    public function getSalt()
    {
        return $this->salt;
    }

    public function getId()
    {
        return $this->id;
    }

    /**
     * {@inheritdoc}
     */
    public function getUsername()
    {
        return $this->nickname;
        //return $this->email;
    }

    /**
     * {@inheritdoc}
     */
    public function isAccountNonExpired()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isAccountNonLocked()
    {
        return !$this->locked;
    }

    /**
     * {@inheritdoc}
     */
    public function isCredentialsNonExpired()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isEnabled()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function eraseCredentials()
    {
    }

    public function isEqualTo(UserInterface $user) {
        if ($this->email !== $user->getUsername()) {
            return false;
        }

        if (array_diff($this->roles, $user->getRoles())) {
            return false;
        }

        if (array_diff($user->getRoles(), $this->roles)) {
            return false;
        }

        return true;
    }

    public function fromArray(array $user)
    {
        $this->data = $user;
        return $this;
    }

    public function toArray()
    {
        return $this->data;
    }

    public function isLogin()
    {
        return empty($this->id) ? false : true;
    }
}

5.编写客制化的用户驱动

客制化的驱动主要是从数据库中获取用户的相关参数,如:用户名、密码、盐值、角色等等,并实例化一个User对象,返回给框架安全模块进行校验。

工程目录\Scourgen\Services\User\UserProvider.php

container = $container;
    }
    /**
     * Loads the user for the given username.
     *
     * This method must throw UsernameNotFoundException if the user is not
     * found.
     *
     * @param string $username The username
     *
     * @return UserInterface
     *
     * @see UsernameNotFoundException
     *
     * @throws UsernameNotFoundException if the user is not found
     */
    public function loadUserByUsername($username)
    {
        // 从数据库中查询用户
        $user = $this->getUserByLoginField($username);

        if (empty($user)) {
            throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
        }
        $user['currentIp'] = $this->container->get('request')->getClientIp();

        $currentUser = new User();
        $currentUser->fromArray($user);
        ServiceKernel::getInstance()->setCurrentUser($currentUser);

        return $currentUser;
    }

    /**
     * Refreshes the user for the account interface.
     *
     * It is up to the implementation to decide if the user data should be
     * totally reloaded (e.g. from the database), or if the UserInterface
     * object can just be merged into some internal array of users / identity
     * map.
     *
     * @param UserInterface $user
     *
     * @return UserInterface
     *
     * @throws UnsupportedUserException if the account is not supported
     */
    public function refreshUser(UserInterface $user)
    {
        if (! $user instanceof User) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
        return $this->loadUserByUsername($user->getUsername());
    }

    /**
     * Whether this provider supports the given user class.
     *
     * @param string $class
     *
     * @return bool
     */
    public function supportsClass($class)
    {
        return $class === 'Scourgen\Services\User\User';
    }

    public function getUserByLoginField($key)
    {
        // 判断$key为邮箱、用户名、手机号,这里略去,直接为用户名
        $nickname = $key;
        $sql = 'SELECT * FROM user WHERE nickname = ? LIMIT 1';
        $connection = ServiceKernel::getInstance()->getConnection();
        $user = $connection->fetchAssoc($sql, array($nickname));

        return !$user ? null : UserSerialize::unserialize($user);
    }
}

class UserSerialize
{
    public static function serialize(array $user)
    {
        $user['roles'] = empty($user['roles']) ? '' :  '|' . implode('|', $user['roles']) . '|';
        return $user;
    }

    public static function unserialize(array $user = null)
    {
        if (empty($user)) {
            return null;
        }
        $user['roles'] = empty($user['roles']) ? array() : explode('|', trim($user['roles'], '|')) ;
        return $user;
    }

    public static function unserializes(array $users)
    {
        return array_map(function($user) {
            return UserSerialize::unserialize($user);
        }, $users);
    }
}

6.实现一个全局服务ServiceKernel

Symfony框架本身实现了依赖注入机制,我们可以通过在控制器中获取任意一个服务。但是为了实现业务模块与框架的独立,因此我们自己实现了一个专用的服务定位器,便于以后调用我们自己的全局服务。

下面展示的代码中只给出了部分功能实现:

工程目录\Scourgen\Services\Common\ServiceKernel.php

environment = $environment;
        $instance->debug = (Boolean)$debug;
        self::$_instance = $instance;

        return $instance;
    }
    /**
     * 获取服务核心实例
     */
    public static function getInstance()
    {
        if (empty(self::$_instance)) {
            throw new \RuntimeException('ServiceKernel未实例化');
        }

        self::$_instance->boot();

        return self::$_instance;
    }

    public function boot()
    {

    }

    public function setCurrentUser($user)
    {
        if (null == $user) {
            throw new \RuntimeException('Arguments 1 $user is null');
        }

        $this->currentUser = $user;
        return $this;
    }

    public function getCurrentUser()
    {
        if ((null == $this->currentUser)) {
            throw new \RuntimeException('尚未初始化CurrentUser');
        }
        return $this->currentUser;
    }

    public function setConnection($connection)
    {
        if (null == $connection) {
            throw new \RuntimeException('Arguments 1 $connection is null');
        }

        $this->connection = $connection;
        return $this;
    }

    public function getConnection()
    {
        if (null == $this->connection) {
            throw new \RuntimeException('尚未初始化数据库连接');
        }

        return $this->connection;
    }
}

7.实例化ServiceKernel

在网站入口文件中对ServiceKernel进行实例化

工程目录\web\app_dev.php

...

// 初始化ServiceKernel
$serviceKernel = ServiceKernel::create($kernel->getEnvironment(), $kernel->isDebug());
$serviceKernel->setConnection($kernel->getContainer()->get('database_connection'));
$serviceKernel->getConnection()->exec('SET NAMES UTF8');

// var_dump($kernel->getContainer()->getParameterBag());
// var_dump($request);

$currentUser = new User();
$currentUser->fromArray(array(
    'id' => 0,
    'nickname' => '游客',
    'currentIp' =>  $request->getClientIp(),
    'roles' => array()
));
$serviceKernel->setCurrentUser($currentUser);

...

8.成功登录处理钩子

工程目录\Scourgen\WebBundle\Handler\AuthenticationSuccessHandler.php

getUser()->id;

        // $forbidden = AuthenticationHelper::checkLoginForbidden($userId);

        // if ($forbidden['status'] == 'error') {
        //     $exception = new AuthenticationException($forbidden['message']);
        //     throw $exception;
        // } else {
        //     $this->getUserService()->markLoginSuccess($userId, $request->getClientIp());
        // }

        $sessionId = $request->getSession()->getId();

        if ($request->isXmlHttpRequest()) {
            $response = array(
                'success' => true
            );

            return new JsonResponse($response, 200);
        }

        // if ($this->getAuthService()->hasPartnerAuth()) {
        //     $url = $this->httpUtils->generateUri($request, 'partner_login');
        //     $queries = array('goto' => $this->determineTargetUrl($request));
        //     $url = $url . '?' . http_build_query($queries);
        //     return $this->httpUtils->createRedirectResponse($request, $url);
        // }
        var_dump($token);
        return parent::onAuthenticationSuccess($request, $token);
    }

    public function createToken(Request $request)
    {
        $userLoginToken = $request->cookies->get('U_LOGIN_TOKEN');

        if (empty($userLoginToken)) {
            $userLoginToken = md5($request->getSession()->getId);
            etcookie('U_LOGIN_TOKEN', $userLoginToken, time() + 3600 * 24 * 265);
        }
        return $userLoginToken;
    }
}

9.登录失败处理钩子

工程目录\Scourgen\WebBundle\Handler\AuthenticationFailureHandler.php

getSession()->set('_target_path', $request->request->get('_target_path'));

        if ($exception->getMessage() === 'Bad credentials.') {
            $message = '用户名或密码错误';
        } else {
            goto end;
        }

        // 连续登陆时间间隔 登陆次数上限检测

        $default = array(
            'temporary_lock_enabled' => 0,
            'temporary_lock_allowed_times' => 5,
            'ip_temporary_lock_allowed_times' => 20,
            'temporary_lock_minutes' => 20,
        );

        $exception = new AuthenticationException($message);


        end:

        if ($request->isXmlHttpRequest()) {
            $content = array(
                'success' => false,
                'message' => $message
            );

            return new JsonResponse($content ,400);
        }

        return parent::onAuthenticationFailure($request, $exception);
    }
}

10.成功退出钩子

工程目录\Scourgen\WebBundle\Handler\LogoutSuccessHandler.php

request->get('goto');

        if (!$goto) {
            $goto = 'login';
        }

        $this->targetUrl = $this->httpUtils->generateUri($request, $goto);
        setcookie('REMEMBERME');

        return parent::onLogoutSuccess($request);
    }
}

你可能感兴趣的:(Symfony Security 配合数据库进行验证登录)