phpixie/auth

PHPixie 的认证库

3.1.3 2018-02-16 00:55 UTC

This package is auto-updated.

Last update: 2024-09-13 01:43:06 UTC


README

PHPixie 认证库

Build Status Test Coverage Code Climate HHVM Status

Author Source Code Software License Total Downloads

这是 PHPixie 认证子系统的基本包,它被分割成几个组件。本手册涵盖了所有这些组件。

认证是任何应用程序最关键的部分,正确实现它是困难的,任何错误都可能损害大量用户,尤其是在开源项目中。使用旧的散列函数、密码学上不安全的随机生成器以及 cookie 的误用是我们仍然经常遇到的事情。这就是为什么我在 PHPixie 中花费了大量时间来仔细实现认证。

它的安全性体现在哪些方面

  • 使用 PHP 5.5 的安全密码_hash() 以及适用于旧 PHP 版本的兼容性包
  • 同样适用于 PHP 7 的密码学安全随机_bytes()
  • 遵循持久登录的最佳实践

最后一个点是其中最有趣的部分,目前没有其他框架默认支持。其背后的想法在于使用一个特殊的表来存储认证令牌。

  1. 当用户首次登录时,会生成一个随机的 序列 标识符和一个 密语。然后,这些信息作为 cookie 发送给用户。
  2. 序列密语 进行散列,然后将 序列、生成的散列、用户 ID 和过期日期保存在数据库中
  3. 当用户进入网站(如果会话尚未存在)时,他的 cookie 会被重新散列,并与数据库中的散列进行比较。如果匹配,则用户登录,启动会话,并为用户生成一个新的令牌。
  4. 如果散列不匹配,则假定发生了盗窃,并将具有相同序列标识符的任何令牌从表中删除

与通常将单个令牌存储在用户表中的方法相比,这种方法具有巨大的优势

  • 用户可以在多个设备上拥有多个持久会话(每个设备将获得自己的 序列
  • 令牌是一次性使用的,如果通过中间人攻击被窃取,则不能重复使用
  • 由于第一次不成功的尝试会删除序列,因此令牌不能被暴力破解
  • 如果数据库遭到破坏,那么暴露的只有令牌散列,因此攻击者仍然无法登录

基本上,如果你的框架将持久令牌原封不动地保存在数据库中而不进行散列,那么这相当于在那里保存未散列的密码。而且仍然有很多流行的框架这样做,只需看看。

初始化

初始化可能看起来有些令人不知所措,但这是因为架构高度模块化,并试图最小化不必要的依赖。如果你不需要特定的扩展,可以自由地不构建它。当然,如果你使用 PHPixie 框架,所有这些都会自动处理。

$slice = new \PHPixie\Slice();

// The database component is only required if you need
// the token storage functionality
$database = new \PHPixie\Database($slice->arrayData(array(
    'default' => array(
        'driver' => 'pdo',
        'connection' => 'sqlite::memory:'
    )
)));

// the security component handles hashing, random numbers and tokens
$security = new \PHPixie\Security($database);

// This plugin allows using the login/password auth
$authLogin = new \PHPixie\AuthLogin($security);

// To use HTTP authorization we must first
// build an HTTP context
$http = new \PHPixie\HTTP();
$request = $http->request();
$context = $http->context($request);
$contextContainer = $http->contextContainer($context);

$authHttp = new \PHPixie\AuthHTTP($security, $contextContainer);


$authConfig = $slice->arrayData(array(
    // config options
));

// This is your class that must impplement the
// \PHPixie\Auth\Repositories\Registry interface
$authRepositories = new AuthRepositories();

// Initialize the Auth system with both extensions
$auth = new \PHPixie\Auth($authConfig, $authRepositories, array(
    $authLogin->providers(),
    $authHttp->providers()
));

仓库

你需要的第一件事是用户仓库。最基本的一个是 PHPixie\Auth\Repositories\Repository,它只提供根据 ID 获取用户的功能。但对于任何实际用途,你可能需要 \PHPixie\AuthLogin\Repository 接口,它允许基于密码的登录。你需要一个仓库构建器来传递给 Auth 组件

class AuthRepositories extends \PHPixie\Auth\Repositories\Registry\Builder
{
    protected function buildUserRepository()
    {
        return new YourRepository();
    }
}

// that is the second parameter we passed to Auth
$authRepositories = new AuthRepositories();

框架支持

如果你使用 PHPixie ORM,只需要扩展预制的包装器

namespace Project\App\ORMWrappers\User;

// Repository wrapper
class Repository extends \PHPixie\AuthORM\Repositories\Type\Login
{
    // You can supply multiple login fields,
    // in this case its both usernam and email
    protected function loginFields()
    {
         return array('username', 'email');
    }
}
namespace Project\App\ORMWrappers\User;

// Entity wrapper
class Entity extends \PHPixie\AuthORM\Repositories\Type\Login\User
{
    // get hashed password value
    // from the field in the database
    public function passwordHash()
    {
         return $this->passwordHash;
    }
}

别忘了将这些包装器注册到 ORM 中

namespace Project\App;

class ORMWrappers extends \PHPixie\ORM\Wrappers\Implementation
{
    protected $databaseEntities = array('user');
    protected $databaseRepositories = array('user');

    public function userEntity($entity)
    {
        return new ORMWrappers\User\Entity($entity);
    }
    
    public function userRepository($repository)
    {
        return new ORMWrappers\User\Repository($repository);
    }
}

并在你的包中注册一个 AuthRepositories 类

namespace Project\App;

class AuthRepositories extends \PHPixie\Auth\Repositories\Registry\Builder
{
    protected $builder;

    public function __construct($builder)
    {
        $this->builder = $builder;
    }

    protected function buildUserRepository()
    {
        $orm = $this->builder->components()->orm();
        return $orm->repository('user');
    }
}
namespace Project\App;

class Builder extends \PHPixie\DefaultBundle\Builder
{
    protected function buildAuthRepositories()
    {
        return new AuthRepositories($this);
    }
}

配置选项

配置被划分为域。域是一个由存储库和认证提供者组成的上下文。通常您的应用程序只有一个域,但有时可能需要更多。例如,想象您为网站用户提供某种社交登录,但网站管理员使用其数据库账户在单独的页面上登录。

// /assets/auth.php

return array(
    'domains' => array(
        'default' => array(

            // using the 'user' repository from the 'app' bundle
            'repository' => 'app.user',
            'providers'  => array(

                // include session support
                'session' => array(
                    'type' => 'http.session'
                ),

                // include persistent cookies (remember me)
                'cookie' => array(
                    'type' => 'http.cookie',
                    
                    // when a cookie is used to login
                    // persist login using session too
                    'persistProviders' => array('session'),
                    
                    // token storage
                    'tokens' => array(
                        'storage' => array(
                            'type'            => 'database',
                            'table'           => 'tokens',
                            'defaultLifetime' => 3600*24*14 // two weeks
                        )
                    )
                ),
                
                // password login suport
                'password' => array(
                    'type' => 'login.password',
                    
                    // remember the user in session
                    // note that we did not add 'cookies' to this array
                    // because we don't want every login to be persistent
                    'persistProviders' => array('session')
                )
            )
        )
);

如您所见,所有提供者都是相互独立的,这意味着我们可以轻松地更改行为。例如,让我们假设我们根本不想使用会话,只想使用基于cookie的登录,并在每个请求上关闭令牌再生。

// /assets/auth.php

return array(
    'domains' => array(
        'default' => array(
               'cookie' => array(
                    'type' => 'http.cookie',

                    // token storage
                    'tokens' => array(
                        'storage' => array(
                            'type'            => 'database',
                            'table'           => 'tokens',
                            'defaultLifetime' => 3600*24*14,
                            
                            // don't refresh tokens
                            'refresh'         => false
                        )
                    )
                ),
                
                'password' => array(
                    'type' => 'login.password',
                    
                    // persist lgoin with cookie
                    'persistProviders' => array('cookie')
                )
            )
        )
);

令牌存储

在这两个例子中,我们都引用了一个用于存储令牌的数据库表。实际上,这也可以是一个MongoDB集合。创建表的SQL语句如下

CREATE TABLE `tokens` (
  `series` varchar(50) NOT NULL,
  `userId` int(11) DEFAULT NULL,
  `challenge` varchar(50) DEFAULT NULL,
  `expires` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`series`)
);

使用示例

现在我们已经配置了一切,让我们测试一下它们是如何协同工作的。这里有一个简单的处理器

namespace Project\App\HTTPProcessors;

class Auth extends \PHPixie\DefaultBundle\Processor\HTTP\Actions
{
    protected $builder;

    public function __construct($builder)
    {
        $this->builder = $builder;
    }

    // Check if the user is logged in
    public function defaultAction($request)
    {
        $user = $this->domain()->user();

        return $user ? $user->username : 'not logged';
    }
    
    // Action for adding user to the database
    public function addAction($request)
    {
        $query = $request->query();
        $username = $query->get('username');
        $password = $query->get('password');

        $orm = $this->builder->components()->orm();
        $provider = $this->domain()->provider('password');

        $user = $orm->createEntity('user');

        $user->username     = $username;

        // Hash password using the password provider
        $user->passwordHash = $provider->hash($password);

        $user->save();

        return 'added';
    }
    
    // Attempt to login user using his password
    public function loginAction($request)
    {
        $query = $request->query();
        $username = $query->get('username');
        $password = $query->get('password');

        $provider = $this->domain()->provider('password');

        $user = $provider->login($username, $password);
        
        if($user) {
        
              // Generate persistent login cookie
              $provider = $this->domain()->provider('cookie');
              $provider->persist();
        }
        return $user ? 'success' : 'wrong password';
    }
    
    // logout action
    public function logoutAction($request)
    {
        $this->domain()->forgetUser();
        return 'logged out';
    }
     
    protected function domain()
    {
        $auth = $this->builder->components()->auth();
        return $auth->domain();
    }
}

要测试它,请尝试访问以下URL

  1. /auth - 用户未登录
  2. /auth/add?username=dracony&password=5 - 将用户添加到数据库
  3. /auth/login?username=dracony&password=5 - 登录
  4. /auth - 检查登录
  5. /auth/logout - 登出

添加自己的提供者

在某个时候,您可能需要添加自己的登录提供者(例如,用于社交网络),为此您需要满足PHPixie\Auth\Providers\Builder接口,并将其与其他扩展一起传递。请参考AuthLogin组件以获取示例。如果您使用PHPixie框架,您可以通过覆盖此方法将自定义扩展传递给Auth组件。