Создание кастомного guard аутентификации в Laravel

Создание кастомного guard аутентификации в Laravel

От автора: в этой статье мы рассмотрим систему аутентификации в фреймворке Laravel. Цель статьи – создать кастомный Guard аутентификации, расширив базовую систему аутентификации. Laravel предоставляет очень надежную систему аутентификации в ядре, что упрощает реализацию стандартной аутентификации. На деле, для установки системы аутентификации необходимо запустить пару выученных команд.

Сама система спроектирована таким образом, что вы можете расширить ее и вставить в нее свои кастомные адаптеры аутентификации. Это мы подробно обсудим в этой статье. Прежде чем мы с головой погрузимся в реализацию кастомного Guard аутентификации, давайте обсудим базовые элементы, благодаря которым работает аутентификация Laravel – Guard и провайдеры.

Элементы ядра: Guard и провайдеры

Система аутентификации Laravel состоит из двух элементов – Guard и провайдеры.

Guard

Представьте Guard, как способ доставки логики, используемой для идентификации прошедших проверку пользователей. В ядре Laravel есть разные Guard – сессия и токен. Guard сессии обслуживает состояние пользователя во всех запросах через куки, а Guard токен проверяет подлинность пользователя через токен в каждом запросе.

Guard определяет логику аутентификации, и необязательно, чтобы он постоянно получал валидные данные авторизации с back end. Можно сделать Guard, который просто проверяет наличие определенных данных в заголовках запроса и на их основе проводит аутентификацию.

Фреймворк Laravel. Быстрая разработка с фреймворком №1

Узнай тонкости современной веб-разработки с помощью фреймворка Laravel

Узнать подробнее

Ниже в статье мы создадим Guard проверки определенных JSON параметров в заголовках запроса, который будет получать валидного пользователя с MongoDB back end.

Провайдеры

Если Guard определяет логику аутентификации, то провайдер аутентификации получает пользователя с хранилища back end. Если Guard требует, чтобы пользователь проверялся через back end хранилище, то реализация получения пользователя переходит в провайдер аутентификации.

Laravel идет с двумя стандартными провайдерами аутентификации — Database и Eloquent. Провайдер аутентификации Database работает с простым вытягиванием данных авторизации пользователя из хранилища back end, а Eloquent дает абстрактный слой.

В нашем примере мы реализуем провайдер аутентификации MongoDB, который будет получать данные авторизации пользователя через MongoDB back end.

Это было базовое представление Guard и провайдеров в системе аутентификации Laravel. Начиная со следующего раздела, мы будем разрабатывать кастомные Guard аутентификации и провайдер!

Быстрая настройка файлов

Давайте быстро пробежимся по файлам, которые нам необходимо создать в этой статье.

config/auth.php: файл настроек аутентификации, в который мы добавим точку входа в кастомный Guard.

config/mongo.php: файл для настроек MongoDB.

app/Services/Contracts/NosqlServiceInterface.php: интерфейс, который реализует наш кастомный класс Mongo database.

app/Database/MongoDatabase.php: главный класс базы данных, взаимодействующий с MongoDB.

app/Models/Auth/User.php: класс модели User, реализующий договор Authenticable.

app/Extensions/MongoUserProvider.php: реализация провайдера аутентификации.

app/Services/Auth/JsonGuard.php: реализация Guard драйвера аутентификации.

app/Providers/AuthServiceProvider.php: файл, с помощью которого мы будем добавлять привязки к сервис контейнерам.

app/Http/Controllers/MongoController.php: файл демо контроллера, с помощью которого мы будем тестировать наш кастомный Guard.

Не пугайтесь, что список файлов пока что вам ничего не говорит, мы постепенно все разберем.

Реализация

В этом разделе мы создадим необходимые файлы. Первым делом нам необходимо информировать Laravel о кастомном Guard. Скопируйте код ниже в файл config/auth.php.

...
...
'guards' => [
 'web' => [
 'driver' => 'session',
 'provider' => 'users',
 ],
 
 'api' => [
 'driver' => 'token',
 'provider' => 'users',
 ],
 
 'custom' => [
 'driver' => 'json',
 'provider' => 'mongo',
 ],
],
...
...

Как видите, мы добавили кастомный Guard под ключом custom. Далее необходимо добавить соответствующий провайдер в раздел providers.

...
...
'providers' => [
 'users' => [
 'driver' => 'eloquent',
 'model' => App\User::class,
 ],
 'mongo' => [
 'driver' => 'mongo'
 ],
 
 // 'users' => [
 // 'driver' => 'database',
 // 'table' => 'users',
 // ],
],
...
...

Провайдер добавлен под ключом mongo. Изменим стандартный Guard аутентификации с web на custom.

...
...
'defaults' => [
 'guard' => 'custom',
 'passwords' => 'users',
],
...
...

Конечно, пока что ничего не работает, так как мы не создали необходимые файлы. Этим мы и займемся в следующих разделах.

Создание MongoDB Driver

В этом разделе мы создадим необходимые файлы, которые будут общаться с объектом MongoDB. Сначала создадим файл настроек config/mongo.php, в котором будут храниться стандартные настройки подключения MongoDB.

<?php
return [
  'defaults' => [
 'host' => '{HOST_IP}',
 'port' => '{HOST_PORT}',
 'database' => '{DB_NAME}'
  ]
];

Данные необходимо заменить на свои. Вместо класса, взаимодействующего с MongoDB мы создадим интерфейс.

Плюс интерфейса в том, что он дает абстрактность, которой разработчик должен придерживаться при его реализации. Также нашу реализацию MongoDB можно легко заменить на другую NoSQL реализацию без необходимости.

Создайте файл интерфейса app/Services/Contracts/NosqlServiceInterface.php и добавьте в него код ниже.

<?php
// app/Services/Contracts/NosqlServiceInterface.php
namespace App\Services\Contracts;
 
Interface NosqlServiceInterface
{
  /**
 * Create a Document
 *
 * @param string $collection Collection/Table Name
 * @param array  $document Document
 * @return boolean
 */
  public function create($collection, Array $document);
 
  /**
 * Update a Document
 *
 * @param string $collection Collection/Table Name
 * @param mix $id Primary Id
 * @param array  $document Document
 * @return boolean
 */
  public function update($collection, $id, Array $document);
 
  /**
 * Delete a Document
 *
 * @param string $collection Collection/Table Name
 * @param mix $id Primary Id
 * @return boolean
 */
  public function delete($collection, $id);
 
  /**
 * Search Document(s)
 *
 * @param string $collection Collection/Table Name
 * @param array  $criteria Key-value criteria
 * @return array
 */
  public function find($collection, Array $criteria);
}

Простой интерфейс, определяющий CRUD методы, которые должен определить класс, реализующий этот интерфейс. Теперь создадим класс app/Database/MongoDatabase.php.

<?php
// app/Database/MongoDatabase.php
namespace App\Database;
 
use App\Services\Contracts\NosqlServiceInterface;
 
class MongoDatabase implements NosqlServiceInterface
{
  private $connection;
  private $database;
 
  public function __construct($host, $port, $database)
  {
 $this->connection = new MongoClient( "mongodb://{$host}:{$port}" );
 $this->database = $this->connection->{$database};
  }
 
  /**
 * @see \App\Services\Contracts\NosqlServiceInterface::find()
 */
  public function find($collection, Array $criteria)
  {
 return $this->database->{$collection}->findOne($criteria);
  }
 
  public function create($collection, Array $document) {}
  public function update($collection, $id, Array $document) {}
  public function delete($collection, $id) {}
}

Предполагаю, что вы уже установили MongoDB и соответствующее расширение MongoDB PHP.

Метод __construct создает объект класса MongoClient с необходимыми параметрами. Другой важный метод, нужный нам – find. Он получает запись на основе переданных аргументов. Мы реализовали драйвер MongoDB и постарались сохранить его простоту.

Создание модели User

Придерживаясь стандартов системы аутентификации, нам необходимо реализовать модель User, которая должна реализовывать контракт Illuminate\Contracts\Auth\Authenticatable. Создайте файл app/Models/Auth/User.php со следующим кодом.

<?php
// app/Models/Auth/User.php
namespace App\Models\Auth;
 
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use App\Services\Contracts\NosqlServiceInterface;
 
class User implements AuthenticatableContract
{
  private $conn;
 
  private $username;
  private $password;
  protected $rememberTokenName = 'remember_token';
 
  public function __construct(NosqlServiceInterface $conn)
  {
 $this->conn = $conn;
  }
 
  /**
 * Fetch user by Credentials
 *
 * @param array $credentials
 * @return Illuminate\Contracts\Auth\Authenticatable
 */
  public function fetchUserByCredentials(Array $credentials)
  {
 $arr_user = $this->conn->find('users', ['username' => $credentials['username']]);
 
 if (! is_null($arr_user)) {
 $this->username = $arr_user['username'];
 $this->password = $arr_user['password'];
 }
 
 return $this;
  }
 
  /**
 * {@inheritDoc}
 * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName()
 */
  public function getAuthIdentifierName()
  {
 return "username";
  }
 
  /**
 * {@inheritDoc}
 * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier()
 */
  public function getAuthIdentifier()
  {
 return $this->{$this->getAuthIdentifierName()};
  }
 
  /**
 * {@inheritDoc}
 * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthPassword()
 */
  public function getAuthPassword()
  {
 return $this->password;
  }
 
  /**
 * {@inheritDoc}
 * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberToken()
 */
  public function getRememberToken()
  {
 if (! empty($this->getRememberTokenName())) {
 return $this->{$this->getRememberTokenName()};
 }
  }
 
  /**
 * {@inheritDoc}
 * @see \Illuminate\Contracts\Auth\Authenticatable::setRememberToken()
 */
  public function setRememberToken($value)
  {
 if (! empty($this->getRememberTokenName())) {
 $this->{$this->getRememberTokenName()} = $value;
 }
  }
 
  /**
 * {@inheritDoc}
 * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberTokenName()
 */
  public function getRememberTokenName()
  {
 return $this->rememberTokenName;
  }
}

Возможно, вы уже заметили, что App\Models\Auth\User реализует контракт Illuminate\Contracts\Auth\Authenticatable.

Большинство методов, реализованных в нашем классе, говорят сами за себя. Мы определили метод fetchUserByCredentials, который вытягивает пользователя с back end. В нашем случае это будет класс MongoDatabase, именно к нему мы будем обращаться за необходимой информацией. Мы создали реализацию модели User.

Создание провайдера аутентификации

Мы уже говорили ранее, что система аутентификации Laravel состоит из двух элементов – Guard и провайдеры. В этом разделе мы создадим провайдер аутентификации, который будет вытягивать пользователя с back end. Создайте файл app/Extensions/MongoUserProvider.php, как показано ниже.

<?php
// app/Extensions/MongoUserProvider.php
namespace App\Extensions;
 
use Illuminate\Support\Str;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
 
class MongoUserProvider implements UserProvider
{
  /**
 * The Mongo User Model
 */
  private $model;
 
  /**
 * Create a new mongo user provider.
 *
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 * @return void
 */
  public function __construct(\App\Models\Auth\User $userModel)
  {
 $this->model = $userModel;
  }
 
  /**
 * Retrieve a user by the given credentials.
 *
 * @param  array  $credentials
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
  public function retrieveByCredentials(array $credentials)
  {
 if (empty($credentials)) {
 return;
 }
 
 $user = $this->model->fetchUserByCredentials(['username' => $credentials['username']]);
 
 return $user;
  }
 
  /**
 * Validate a user against the given credentials.
 *
 * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
 * @param  array  $credentials  Request credentials
 * @return bool
 */
  public function validateCredentials(Authenticatable $user, Array $credentials)
  {
 return ($credentials['username'] == $user->getAuthIdentifier() &&
 md5($credentials['password']) == $user->getAuthPassword());
  }
 
  public function retrieveById($identifier) {}
 
  public function retrieveByToken($identifier, $token) {}
 
  public function updateRememberToken(Authenticatable $user, $token) {}
}

Необходимо, чтобы кастомный провайдер реализовывал контракт Illuminate\Contracts\Auth\UserProvider. Файл определяет два важных метода retrieveByCredentials и validateCredentials.

Метод retrieveByCredentials используется для получения данных авторизации пользователя через модель User, о которой мы говорили в предыдущем разделе. Метод validateCredentials проверяет пользователя по заданному набору данных.

Фреймворк Laravel. Быстрая разработка с фреймворком №1

Узнай тонкости современной веб-разработки с помощью фреймворка Laravel

Узнать подробнее

Мы создали кастомный провайдер аутентификации. В следующем разделе мы перейдем к созданию Guard, который будет взаимодействовать с провайдером аутентификации MongoUserProvider.

Создание Guard аутентификации

Как мы уже говорили ранее, Guard в системе аутентификации Laravel определяет логику аутентификации пользователя. Мы будем проверять наличие параметра запроса jsondata. В нем должна быть JSON строка с данными авторизации.

В этом разделе мы создадим Guard, который будет общаться с провайдером аутентификации, созданном в предыдущем разделе. Создайте файл app/Services/Auth/JsonGuard.php со следующим кодом.

<?php
// app/Services/Auth/JsonGuard.php
namespace App\Services\Auth;
 
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use GuzzleHttp\json_decode;
use phpDocumentor\Reflection\Types\Array_;
use Illuminate\Contracts\Auth\Authenticatable;
 
class JsonGuard implements Guard
{
  protected $request;
  protected $provider;
  protected $user;
 
  /**
 * Create a new authentication guard.
 *
 * @param  \Illuminate\Contracts\Auth\UserProvider  $provider
 * @param  \Illuminate\Http\Request  $request
 * @return void
 */
  public function __construct(UserProvider $provider, Request $request)
  {
 $this->request = $request;
 $this->provider = $provider;
 $this->user = NULL;
  }
 
  /**
 * Determine if the current user is authenticated.
 *
 * @return bool
 */
  public function check()
  {
 return ! is_null($this->user());
  }
 
  /**
 * Determine if the current user is a guest.
 *
 * @return bool
 */
  public function guest()
  {
 return ! $this->check();
  }
 
  /**
 * Get the currently authenticated user.
 *
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
  public function user()
  {
 if (! is_null($this->user)) {
 return $this->user;
 }
  }
 
  /**
 * Get the JSON params from the current request
 *
 * @return string
 */
  public function getJsonParams()
  {
 $jsondata = $this->request->query('jsondata');
 
 return (!empty($jsondata) ? json_decode($jsondata, TRUE) : NULL);
  }
 
  /**
 * Get the ID for the currently authenticated user.
 *
 * @return string|null
  */
  public function id()
  {
 if ($user = $this->user()) {
 return $this->user()->getAuthIdentifier();
 }
  }
 
  /**
 * Validate a user's credentials.
 *
 * @return bool
 */
  public function validate(Array $credentials=[])
  {
 if (empty($credentials['username']) || empty($credentials['password'])) {
 if (!$credentials=$this->getJsonParams()) {
 return false;
 }
 }
 
 $user = $this->provider->retrieveByCredentials($credentials);
 
 if (! is_null($user) && $this->provider->validateCredentials($user, $credentials)) {
 $this->setUser($user);
 
 return true;
 } else {
 return false;
 }
  }
 
  /**
 * Set the current user.
 *
 * @param  Array $user User info
 * @return void
 */
  public function setUser(Authenticatable $user)
  {
 $this->user = $user;
 return $this;
  }
}

Прежде всего, нашему классу необходимо реализовывать интерфейс Illuminate\Contracts\Auth\Guard. Поэтому необходимо определить все методы, объявленные в интерфейсе.

Важно отметить тот факт, что функция __construct требует реализации Illuminate\Contracts\Auth\UserProvider. Мы передадим объект App\Extensions\MongoUserProvider.

Далее есть функция getJsonParams, получающая данные авторизации пользователя из параметра jsondata запроса. Ожидается строка JSON с данными авторизации пользователя, поэтому мы декодируем JSON данные с помощью функции json_decode.

В функции валидации первое, что мы проверяем – существование аргумента $credentials. Если его нет, вызывается метод getJsonParams для получения данных авторизации пользователя из параметров запроса.

Далее мы вызываем метод retrieveByCredentials провайдера MongoUserProvider, который получает пользователя из базы данных MongoDB с back end. И наконец, метод validateCredentials провайдера MongoUserProvider валидирует пользователя.

Это была реализация кастомного Guard. В следующем разделе говорится, как связать все эти части вместе и сформировать систему аутентификации.

Собираем все вместе

Мы разработали все элементы кастомного Guard аутентификации, которые должны дать нам новую систему аутентификации. Однако прямо вот так она не заработает. Ее необходимо зарегистрировать в привязках сервис контейнера Laravel.

Откройте файл app/Providers/AuthServiceProvider.php. С его помощью можно добавить привязки сервис контейнера аутентификации. Если в нем нет кастомных изменений, можете просто заменить код на код ниже.

<?php
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
 
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Services\Auth\JsonGuard;
use App\Extensions\MongoUserProvider;
use App\Database\MongoDatabase;
use App\Models\Auth\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
 
class AuthServiceProvider extends ServiceProvider
{
  /**
 * The policy mappings for the application.
 *
 * @var array
 */
  protected $policies = [
 'App\Model' => 'App\Policies\ModelPolicy',
  ];
 
  /**
 * Register any authentication / authorization services.
 *
 * @return void
 */
  public function boot()
  {
 $this->registerPolicies();
 
 $this->app->bind('App\Database\MongoDatabase', function ($app) {
 return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database'));
 });
 
 $this->app->bind('App\Models\Auth\User', function ($app) {
 return new User($app->make('App\Database\MongoDatabase'));
 });
 
 // add custom guard provider
 Auth::provider('mongo', function ($app, array $config) {
 return new MongoUserProvider($app->make('App\Models\Auth\User'));
 });
 
 // add custom guard
 Auth::extend('json', function ($app, $name, array $config) {
 return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
 });
  }
 
  public function register()
  {
 $this->app->bind(
 'App\Services\Contracts\NosqlServiceInterface',
 'App\Database\MongoDatabase'
 );
  }
}

Рассмотрим метод boot, в котором хранится большая часть привязок провайдера. Для начала создадим привязки для App\Database\MongoDatabase и App\Models\Auth\User.

$this->app->bind('App\Database\MongoDatabase', function ($app) {
  return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database'));
});
 
$this->app->bind('App\Models\Auth\User', function ($app) {
  return new User($app->make('App\Database\MongoDatabase'));
});

Пришло время вставить наш кастомный Guard в систему аутентификации Laravel.

С помощью провайдер метода Auth Facade мы добавили наш кастомный провайдер аутентификации под ключ mongo. Вспоминайте, этот ключ отражает настройки, которые мы ранее вносили в файл auth.php.

Auth::provider('mongo', function ($app, array $config) {
  return new MongoUserProvider($app->make('App\Models\Auth\User'));
});

Точно так же мы вставим наша реализацию кастомного Guard с помощью метода extend Auth façade.

Auth::extend('json', function ($app, $name, array $config) {
  return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
});

Далее идет метод register, с помощью которого мы привязали интерфейс App\Services\Contracts\NosqlServiceInterface к реализации App\Database\MongoDatabase.

$this->app->bind(
  'App\Services\Contracts\NosqlServiceInterface',
  'App\Database\MongoDatabase'
);

Теперь когда понадобится разрешить зависимость App\Services\Contracts\NosqlServiceInterface, Laravel ответит реализацией адаптера App\Database\MongoDatabase.

Преимущество этого подхода в том, что реализацию можно легко заменить на другую. Например, кто-то захотел заменить реализацию App\Database\MongoDatabase на адаптер CouchDB. Для этого ему необходимо лишь добавить соответствующую привязку в метод register.

Сервис провайдер в вашем распоряжении. Сейчас у нас есть все необходимое для тестирования кастомной реализации Guard. Остался раздел, подводящий итоги.

Это работает?

Вы проделали сложную работу по созданию своего первого кастомного Guard аутентификации. Пора попробовать его в действии. Быстро создадим простой файл контроллера app/Http/Controllers/MongoController.php.

<?php
// app/Http/Controllers/MongoController.php
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Guard;
 
class MongoController extends Controller
{
  public function login(Guard $auth_guard)
  {
 if ($auth_guard->validate()) {
 // get the current authenticated user
 $user = $auth_guard->user();
 
 echo 'Success!';
 } else {
 echo 'Not authorized to access this page!';
 }
  }
}

Подробно разберем зависимость метода login, который требует реализацию Guard Illuminate\Contracts\Auth\Guard. В файле auth.php по умолчанию мы задали Guard custom, поэтому вставляться будем App\Services\Auth\JsonGuard!

Далее мы вызвали метод validate класса App\Services\Auth\JsonGuard, который запускает серию вызовов методов:

вызывается метод retrieveByCredentials класса App\Extensions\MongoUserProvider

метод retrieveByCredentials вызывает метод fetchUserByCredentials класса App\Models\Auth\User

метод fetchUserByCredentials вызывает метод find класса App\Database\MongoDatabase и получает данные авторизации пользователя

метод find класса App\Database\MongoDatabase возвращает ответ!

Если все отработает, как ожидается, мы должны получить аутентифицированного пользователя через вызов метода user нашего Guard.

Для доступа к контроллеру необходимо добавить соответствующий роут в файл routes/web.php.

Route::get('/custom/mongo/login', 'MongoController@login');

Попробуйте открыть http://your-laravel-site/custom/mongo/login, не передавая параметры. Должно отобразиться сообщение «not authorized».

Если открыть что-то типа http://your-laravel-site/custom/mongo/login?jsondata={«username»:»admin»,»password»:»admin»}, должно вернуться успешное сообщение, если пользователь есть в базе данных.

Заметьте, что это лишь демонстрация возможностей кастомного Guard. Такие вещи, как логин, необходимо делать с защитой от дурака. Я же показал лишь общий взгляд на процесс аутентификации. Вы сами должны создать надежную и безопасную систему для своего приложения.

Наше приключение заканчивается, вскоре вернусь с новой полезной статьей, надеюсь. Хотите спросить что-то по какой-либо теме, пишите!

Заключение

Фреймворк Laravel предоставляет надежную систему аутентификации в ядре, которую можно расширить кастомной системой. Темой сегодняшней статьи стала реализация кастомного Guard и его вставка в процесс аутентификации Laravel.

В процессе мы разработали систему, аутентифицирующую пользователя по JSON в запросе и проверяющую эти данные с базой данных MongoDB. Для этого мы создали кастомный Guard и провайдер.

Надеюсь, этот пример показал вам общий взгляд на аутентификацию в Laravel, и вы теперь лучше понимаете ее внутреннее устройство.

Автор: Sajal Soni

Источник: https://code.tutsplus.com/

Редакция: Команда webformyself.

Фреймворк Laravel. Быстрая разработка с фреймворком №1

Узнай тонкости современной веб-разработки с помощью фреймворка Laravel

Узнать подробнее
Самые свежие новости IT и веб-разработки на нашем Telegram-канале

Фреймворк Laravel: теория и первая практика

Овладейте азами фреймворка Laravel!

Получить

Метки:

Похожие статьи:

Комментарии Вконтакте:

Комментарии Facebook:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree