Introduction
Windwalker user system is extendable, before we start use user auth, we must do some prepare to initial user system.
Prepare User System
Create UserHandler
UserHandlerInterface
help us handler user CRUD and login/logout actions, create a Model implements UserHandlerInterface
in your package.
namespace Flower\Model;
use Windwalker\Core\Model\DatabaseModelRepository;
use Windwalker\Core\User\UserData;
use Windwalker\Core\User\UserDataInterface;
use Windwalker\Core\User\UserHandlerInterface;
use Windwalker\Ioc;
class UserModel extends DatabaseModelRepository implements UserHandlerInterface
{
protected $table = 'users';
public function load($conditions)
{
$data = $this->getDataMapper()->findOne($conditions);
return new UserData($data->dump());
}
public function save(UserDataInterface $user)
{
$result = $this->getDataMapper()->saveOne($user->dump());
$user->id = $result['id'];
return $user;
}
public function delete($conditions)
{
return $this->getDataMapper()->delete($conditions);
}
public function login(UserDataInterface $user)
{
unset($user->password);
Ioc::getSession()->set('user', $user->dump());
return true;
}
public function logout(UserDataInterface $user = null)
{
Ioc::getSession()->remove('user');
return true;
}
}
Migration and Seeding
Use this simple schema to create user table, see Migration:
$this->createTable('users', function (Schema $sc)
{
$sc->primary('id')->comment('Primary Key');
$sc->varchar('name')->comment('Full Name');
$sc->varchar('username')->comment('Login name');
$sc->varchar('email')->comment('Email');
$sc->varchar('password')->comment('Password');
$sc->addIndex('id');
$sc->addIndex('username');
$sc->addIndex('email');
});
And you can add seeder to generate fake user data:
$faker = \Faker\Factory::create();
// Please do not use `pass123456` in production site
$pass = (new Password)->create('pass123456');
$model = UserModel::getInstance();
foreach (range(1, 50) as $i)
{
$data = [
'name' => $faker->name,
'username' => $faker->userName,
'email' => $faker->email,
'password' => $pass
];
$model->save(new UserData($data));
$this->outCounting();
}
Add this handler to etc/app/web.php
:
// ...
'user' => [
'handler' => \Flower\Model\UserModel::class,
'methods' => [
],
'policies' => [
]
]
Load and Save Users
Now we can do CRUD for user table:
use Windwalker\Core\User\User;
$user = User::get(1); // get User ID=1
$user = User::get(['username' => 'christina']); // get Username=christina
$user->name = 'Christina';
User::delete(1); // Delete User ID=1
Get User
use Windwalker\Core\User\User;
// Get a user by id
$user = User::get(12);
// Get user by conditions
$user = User::get(array('username' => 'christina'));
Save User
use Windwalker\Crypt\Password;
$user->username = 'riaok3784';
$user->password = 'abc1234';
// Hash password
$user->password = (new Password)->create($user->password);
User::save($user);
Delete User
// Delete by id
User::delete(12);
// Delete by conditions
User::delete(array('username' => 'richael3784'));
Create Authentication Method
Currently our UserHandler
class can only load/save user from database, but we need it to support login/logout from databse or other sources.
Windwalker Authentication allow developers attach multiple methods to match user(See Authentication Package).
This image described how authentication methods working.
DatabaseMethod
We create a method class to find user from database.
namespace Flower\User;
use Flower\Model\UserModel;
use Windwalker\Authentication\Authentication;
use Windwalker\Authentication\Credential;
use Windwalker\Authentication\Method\AbstractMethod;
use Windwalker\Crypt\Password;
class DatabaseMethod extends AbstractMethod
{
public function authenticate(Credential $credential)
{
// Do not allow empty username or password
if (!$credential->username || !$credential->password)
{
$this->status = Authentication::EMPTY_CREDENTIAL;
return false;
}
// Do not allow email as username
if (strpos($credential->username, '@') !== false)
{
$this->status = Authentication::INVALID_USERNAME;
return false;
}
// Get User from database
// You can also use User::get(['username' => $credential->username])
$user = UserModel::getInstance()->load(['username' => $credential->username]);
if ($user->isNull())
{
$this->status = Authentication::USER_NOT_FOUND;
return false;
}
// User found, then we check password
if (!(new Password)->verify($credential->password, $user->password))
{
$this->status = Authentication::INVALID_PASSWORD;
return false;
}
// Confirm user exists, bind user data into Credential since it is referenced
$credential->bind($user);
// Return TRUE and success status
$this->status = Authentication::SUCCESS;
return true;
}
}
And register to etc/app/web.php
:
// ...
'user' => [
'handler' => ...,
'methods' => [
\Flower\User\DatabaseMethod::class
],
'policies' => [
]
]
OK now we can login User by credential.
Login and Logout
Login User
// Prepare user data
$credential = new Credential;
$credential->username = 'richael2123';
$credential->password = '12345678';
try
{
$bool = User::login($credential);
}
catch (AuthenticateFailException $e)
{
$messages = $e->getMessages();
}
Remember me:
$bool = User::login($credential, true);
Our UserHandler set user login data to session user
key, see $_SESSION['user']
:
show($_SESSION);
Array
(
[_default] => Array
(
...
[user] => Array
(
[username] => richael2123
[id] => 2
[name] => Richael
[email] => richael@block.net
[_authenticated_method] => 0
)
)
[_flash] => Array
(
)
)
Logout User
$bool = User::logout();
Force Login User
Use makeUserLoggedIn()
to force login a user data to session without authentication.
$user = [
'username' => 'richael2123',
'name' => 'Richael'
];
User::makeUserLoggedIn($user);
Get Current User
$user = User::get();
// Check user is logged-in
$user->isMember();
// Check user is not logged-in
$user->isGuest();
Authenticate From Remote
This is an example to auth user from remote server.
use Windwalker\Http\HttpClient;
// ...
class RemoteMethod extends AbstractMethod
{
public function authenticate(Credential $credential)
{
// Use HttpClient to get remote json data
$response = (new HttpClient)->get('http://myapiserver.com/user/auth', [
'username' => $credential->username,
'password' => $credential->password
]);
if ($response->getStatusCode() != 200)
{
$this->status = Authentication::USER_NOT_FOUND;
return false;
}
$user = json_decode($response->getBody()->__toString());
// Confirm user exists, bind user data into Credential since it is referenced
$credential->bind($user);
// Return TRUE and success status
$this->status = Authentication::SUCCESS;
return true;
}
}
Then add this class to etc/app/web.php
so Windwalker will check two methods to find user.
// ...
'user' => [
'handler' => ...
'methods' => [
\Flower\User\DatabaseMethod::class,
\Flower\User\RemoteMethod::class
],
'policies' => [
]
]
Dependency Injection
If your method class dependent on other classes, just add it on constructor, The Ioc container will auto inject it:
use Windwalker\Core\Application\WebApplication;
class DatabaseMethod extends AbstractMethod
{
protected $app;
public function __contruct(WebApplication $app)
{
$this->app = $app;
}
// ...
Check User Is Allowed To Login
You can check a user has access to login or not, use listener to listen onUserAuthorisation
event:
namespace Flower\Listener;
use Windwalker\Core\User\Exception\AuthenticateFailException;
use Windwalker\Core\User\UserDataInterface;
use Windwalker\Event\Event;
class UserAuthListener
{
public function onUserAuthorisation(Event $event)
{
/** @var UserDataInterface $user */
$user = $event['user'];
if (!$user->activated)
{
// Return false to result
$event['result'] = false;
// OR throw exception
throw new AuthenticateFailException('User not activated');
}
if ($user->is_blocked)
{
throw new AuthenticateFailException('User is disbaled');
}
}
}
Then register it to etc/app/web.php
:
// ...
'listeners' => [
400 => \Flower\Listener\UserAuthListener::class
],
Now Windwalker will auto block users with no access to login.
If you found a typo or error, please help us improve this document.