phpixie / auth
PHPixie 的认证库
Requires
- phpixie/slice: ~3.0
Requires (Dev)
- phpixie/test: ~3.0
README
PHPixie 认证库
这是 PHPixie 认证子系统的基本包,它被分割成几个组件。本手册涵盖了所有这些组件。
认证是任何应用程序最关键的部分,正确实现它是困难的,任何错误都可能损害大量用户,尤其是在开源项目中。使用旧的散列函数、密码学上不安全的随机生成器以及 cookie 的误用是我们仍然经常遇到的事情。这就是为什么我在 PHPixie 中花费了大量时间来仔细实现认证。
它的安全性体现在哪些方面
- 使用 PHP 5.5 的安全密码_hash() 以及适用于旧 PHP 版本的兼容性包
- 同样适用于 PHP 7 的密码学安全随机_bytes()
- 遵循持久登录的最佳实践
最后一个点是其中最有趣的部分,目前没有其他框架默认支持。其背后的想法在于使用一个特殊的表来存储认证令牌。
- 当用户首次登录时,会生成一个随机的 序列 标识符和一个 密语。然后,这些信息作为 cookie 发送给用户。
- 对 序列 和 密语 进行散列,然后将 序列、生成的散列、用户 ID 和过期日期保存在数据库中
- 当用户进入网站(如果会话尚未存在)时,他的 cookie 会被重新散列,并与数据库中的散列进行比较。如果匹配,则用户登录,启动会话,并为用户生成一个新的令牌。
- 如果散列不匹配,则假定发生了盗窃,并将具有相同序列标识符的任何令牌从表中删除
与通常将单个令牌存储在用户表中的方法相比,这种方法具有巨大的优势
- 用户可以在多个设备上拥有多个持久会话(每个设备将获得自己的 序列)
- 令牌是一次性使用的,如果通过中间人攻击被窃取,则不能重复使用
- 由于第一次不成功的尝试会删除序列,因此令牌不能被暴力破解
- 如果数据库遭到破坏,那么暴露的只有令牌散列,因此攻击者仍然无法登录
基本上,如果你的框架将持久令牌原封不动地保存在数据库中而不进行散列,那么这相当于在那里保存未散列的密码。而且仍然有很多流行的框架这样做,只需看看。
初始化
初始化可能看起来有些令人不知所措,但这是因为架构高度模块化,并试图最小化不必要的依赖。如果你不需要特定的扩展,可以自由地不构建它。当然,如果你使用 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
- /auth - 用户未登录
- /auth/add?username=dracony&password=5 - 将用户添加到数据库
- /auth/login?username=dracony&password=5 - 登录
- /auth - 检查登录
- /auth/logout - 登出
添加自己的提供者
在某个时候,您可能需要添加自己的登录提供者(例如,用于社交网络),为此您需要满足PHPixie\Auth\Providers\Builder
接口,并将其与其他扩展一起传递。请参考AuthLogin组件以获取示例。如果您使用PHPixie框架,您可以通过覆盖此方法将自定义扩展传递给Auth组件。