...

воскресенье, 22 декабря 2013 г.

[Из песочницы] Элегантная форма входа в админку на Laravel и Sentry

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

Статья содержит описание некоторых базовых приемов использования Laravel при разработки сайтов и будет полезна тем, кто начинает осваивать данный фреймворк. Для примера использую Ubuntu 12.04, PostgreSQL 9.3, Nginx 1.1.19, PHP 5.5.7, Composer и свежий проект, созданный с использованием Laravel 4.1. Под управление PostgreSQL крутится база данных examples, к которой имеет доступ пользователь examples c одноименным паролем. Nginx же настроен таким образом, что при обращении по адресу http://examples.loc в браузере открывается главная страницу-заглушка, которая идет с Laravel в комплекте, с надписью «You have arrived.»



Все пути к редактируемым файлам указаны относительно директории проекта.


Окружение




Сначала настраиваю локальное окружение в Laravel. Для этого создаю директорию app/config/local и добавляю в нее файл database.php:

<?php
/**
* app/config/local/database.php
*/
return array(
'default' => 'pgsql',

'connections' => array(
'pgsql' => array(
'driver' => 'pgsql',
'host' => 'localhost',
'database' => 'examples',
'username' => 'examples',
'password' => 'examples',
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
),
),
);




Прошу Laravel использовать окружение local по умолчанию. Для этого редактирую файл bootstrap/start.php и заменяю строку 'your-machine-name' на '*':

// Фрагмент из bootstrap/start.php
$env = $app->detectEnvironment(array(

'local' => array('*'),

));


Подключение Sentry




Ссылку на инструкцию по подключению Sentry можно найти в конце статьи. Кратко, делаю следующее.

Добавляю в composer.json строку "cartalyst/sentry": "2.0.*" в блок require.

// Фрагмент composer.json
{
"name": "laravel/laravel",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"license": "MIT",
"require": {
"laravel/framework": "4.1.*",
"cartalyst/sentry": "2.0.*"
},
...




Выполняю команду:

$ composer update

Символ $ набирать не надо

Символ $ означает что, надо набрать в командной строке composer update





Добавляю в список сервис-провайдеров в файле app/config/app.php:

'Cartalyst\Sentry\SentryServiceProvider',

Добавляю в список псевдонимов в файле app/config/app.php:

'Sentry' => 'Cartalyst\Sentry\Facades\Laravel\Sentry',


Выполняю миграции Sentry:

$ php artisan migrate --package=cartalyst/sentry


Публикую конфигурационный файл Sentry:

$ php artisan config:publish cartalyst/sentry


Открываю на редактирование app/config/packages/cartalyst/sentry/config.php. Нахожу в нем строку 'login_attribute' => 'email' и заменяю на 'login_attribute' => 'username', для того чтобы Sentry выполнял аутентификацию пользователя по его логину, а не e-mail.


При выполнении миграций Sentry была создана таблица users, в которой есть поле email, но нет username. Поэтому, чтобы Sentry работал, надо добавить недостающее поле. Для этого создаю миграцию:

$ php artisan migrate:make alter_users_add_username


В директории app/database/migration появится файл имя, которого будет состоять из текущей даты и времени и заканчивается на alter_users_add_username.php. Редактирую его следующим образом:



<?php
/**
* app/database/migration/0000_00_00_000000_alter_users_add_username.php
*/
use Illuminate\Database\Migrations\Migration;

class AlterUsersAddUsername extends Migration {

/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function($table)
{
$table->string('username');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function($table)
{
$table->dropColumn('username');
});
}

}




Для проверки миграции выполняю:

$ php artisan migrate

Для проверки, что миграция успешно откатывается, выполняю:

$ php artisan migrate:rollback


Создаю еще одну миграцию, которая добавляет в таблицу users запись о суперпользователе:

$ php artisan migrate:make add_user_admin


Нахожу в директории app/database/migrate файл, который заканчивается на add_user_admin.php и редактирую его:



<?php
/**
* app/database/migration/0000_00_00_000001_add_user_admin.php
*/
use Illuminate\Database\Migrations\Migration;

class AddUserAdmin extends Migration {

/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$user = Sentry::createUser(array(
'username' => 'admin',
'email' => 'admin@examples.loc',
'password' => 'password',
'activated' => 1,
'permissions' => array(
'superuser' => 1,
),
));
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
User::where('username', '=', 'admin')->firstOrFail()->delete();
}

}




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

Форма входа




В директории app/controllers создаю файл AuthController.php:

<?php
/**
* app/controllers/AuthController.php
*/
class AuthController extends BaseController {

/**
* Отображает страницу входа
*
* @return Illuminate\View\View
*/
public function getLogin()
{
$title = 'Вход';
return View::make('auth.login', compact('title'));
}
}




Метод getLogin() передает заголовок страницы через переменную $title в представление auth.login, которое служит для отображения страницы входа в админку.

Создаю представление auth.login. Для этого в директорию app/views добавляю директорию auth и создаю в ней файл login.blade.php:



/**
* app/views/auth/login.blade.php
*/
@extends('layout')

@section('main')
<div class="container">
{{ Form::open(array('class' => 'form-signin')) }}

@if (!$errors->isEmpty())
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif

<h2 class="form-signin-heading">{{ $title }}</h2>

{{ Form::text('username', null, array('class' => 'form-control', 'placeholder' => 'Логин')) }}
{{ Form::password('password', array('class' => 'form-control', 'placeholde' => 'Пароль')) }}

<label class="checkbox">
{{ Form::checkbox('remember-me', 1) }} Запомни меня
</label>

{{ Form::submit('Войти', array('class' => 'btn btn-lg btn-primary btn-block')) }}

{{ Form::close() }}
</div>
@stop




Шаблон login.blade.php расширяет шаблон layout.blade.php. Поэтому создаю его в директории app/views:

/**
* app/views/layout.blade.php
*/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ $title }}</title>

@section('styles')
{{ HTML::style('//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css') }}
{{ HTML::style(URL::asset('styles/base.css')) }}
@show
</head>
<body>
@yield('main')

@section('scripts')
{{ HTML::script('//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js') }}
@show
<body>
</html>




После того, как есть метод контроллера и возвращаемое им представление, необходимо настроить роутинг, чтобы GET запросы, отправляемые по адресу http://examples.loc/login, обрабатывались методом getLogin(). Для этого добавляю в файл app/routes.php следующий код:

// Фрагмент app/routes.php
...
Route::group(array('before' => 'guest'), function ()
{
Route::get('login', array(
'as' => 'auth.login',
'uses' => 'AuthController@getLogin'
));
});




С помощью Route::get() определяю роут с именем auth.login, который направляет GET запросы по адресу /login методу getLogin() контроллера AuthController.

Также поместил роут auth.login в группы роутов. Перед любым роутом, входящим в данную группу будет выполнятся фильтр guest. Данный фильтр означает, что обработка GET запросов по адресу /login будет происходить только в том случает, если пользователя является гостем, т.е. не авторизован.

Перепишу фильтр guest так, чтобы он использовал Sentry. Для этого в файле app/filters.php изменю код фильтра guest:



// Фрагмент app/filters.php
...
Route::filter('guest', function()
{
if (Sentry::check()) return Redirect::to('/');
});
...




Теперь можно посмотреть, как выглядит форма входа в браузере. Для этого перехожу по адресу http://examples.loc/login и…

Вижу сообщение об ошибке Call to undefined method Illuminate\Cookie\CookieJar::get().


Легким движением Google выясняю, что Sentry 2.0 совместим с Laravel 4.0, но не совместим 4.1. Хорошо, что уже зарелизился Sentry 2.1. Чтобы избавиться от ошибки, изменяю версию Sentry в composer.json на 2.1:



// Фрагмент composer.json
{
"name": "laravel/laravel",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"license": "MIT",
"require": {
"laravel/framework": "4.1.*",
"cartalyst/sentry": "2.1.*"
},
...




Выполняю команду:

$ composer update

Еще раз пытаюсь открыть http://examples.loc/login и вижу форму ввода логина и пароля. Для того чтобы форма выглядела элегантно, надо добавить немного CSS. Для этого в директории public создаю директорию styles и добавляю туда файл base.css:



/**
* public/style/base.css
*/
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #eee;
}

.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}

.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}

.form-signin .form-signin-heading {
text-align: center;
}

.form-signin .checkbox {
font-weight: normal;
}

.form-signin .form-control {
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}

.form-signin .form-control:focus {
z-index: 2;
}

.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}

.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}


Контролеры




Теперь форма отображается красиво, но при нажатии на кнопку «Войти», возвращается ошибка 404. Значит надо добавить в AuthController обработчик POST запросов, отправляемых на адрес /login.

<?php
/**
* app/controllers/AuthController.php
*/
class AuthController extends BaseController {

/**
* Отображает страницу входа
*
* @return Illuminate\View\View
*/
public function getLogin()
{
$title = 'Вход';
return View::make('auth.login', compact('title'));
}

/**
* Аутентифицирует и редиректит в админку
*
* @return Illuminate\Http\RedirectResponse
*/
public function postLogin()
{
Input::flash();

try {
$credentials = array(
'username' => Input::get('username'),
'password' => Input::get('password')
);
$user = Sentry::authenticate($credentials, Input::get('remember-me'));
} catch (Exception $e) {
return Redirect::to(route('auth.login'))
->withErrors(array($e->getMessage()));
}

return Redirect::intended(route('admin'));
}

/**
* Обрабатывает выход
*
* @return Illuminate\Http\RedirectResponse
*/
public function getLogout()
{
Sentry::logout();
return Redirect::route('auth.login');
}
}




В методе postLogin() данные, переданные через форму сохраняются в сессии с помощью Input::flash(). В блоке try Sentry пытается аутентифицировать пользователя. Если в форме ввода логина и пароля пользователь установит галочку «Запомни меня», то вторым параметром в Sentry::authenticate() будет передано значение true и Sentry запомнит пользователя в случае успешной аутентификации. Если аутентификация по какой-либо причине не прошла успешно, то в блоке catch метод Redirect::to() отправит нас на страницу ввода логина и пароля в месте с сообщением об ошибке. В случае успешной аутентификации метод Redirect::intended() отправит пользователя на ту страницу, на которую он намеревался зайти, когда был перенаправлен на форму ввода логина и пароля. Если такая страница не задана, то откроется страница по адресу /admin, с которой связан роут с именем admin.

Метод getLogin() самая проста реализация как можно разлогинить пользователя. После выхода пользователь будет перенаправление на страницу ввода логина и пароля.


Прежде чем переходить к настройке роутинга, надо добавить метод, который будет обрабатывать запросы, отправляемые по адресу /admin. Для этого отредактирую файл app/controllers/HomeController.php следующим образом:



<?php
/**
* app/controllers/HomeController.php
*/
class HomeController extends BaseController {

public function showWelcome()
{
return View::make('hello');
}

public function getAdmin()
{
return link_to(route('auth.logout'), 'Выход');
}

}




Метод getAdmin() просто отображает на страницу ссылку «Выход», при нажатии на которою пользователь будет разлогиниваться.

Роутинг




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

/**
* app/routes.php
*/
Route::get('/', function()
{
return View::make('hello');
});

Route::group(array('before' => 'guest'), function ()
{
Route::get('login', array(
'as' => 'auth.login',
'uses' => 'AuthController@getLogin'
));

Route::post('login', array(
'before' => 'csrf',
'uses' => 'AuthController@postLogin'
));
});

Route::group(array('before' => 'auth'), function ()
{
Route::get('admin', array(
'as' => 'admin',
'uses' => 'HomeController@getAdmin',
));

Route::get('logout', array(
'as' => 'auth.logout',
'uses' => 'AuthController@getLogout'
));
});




Первая группа роутов, перед которой выполняется фильтр guest, содержи роутинг для адреса /login для GET и POST запросов. Фильтр guest означает, что по адресу /login будут обрабатываться запросы только от гостей, т.е. неавторизованных пользователей.

Вторая группа роутов, перед которой выполняется фильтр auth, содержит роутинг для адресов /admin и /logout. Фильтр auth означает, что по данным адресам будут обрабатываться запросы только от авторизованных пользователей.


Также необходимо отредактировать фильтр auth, так чтобы он использовал Sentry. Для этого открываю файл app/filters.php изменяю фильтр auth следующим образом:



// Фрагмент app/filters.php
...
Route::filter('auth', function()
{
if (!Sentry::check()) return Redirect::guest(route('auth.login'));
});
...


Теперь можно пробовать логиниться в админку использую логин admin и пароль password. Неавторизованному пользователю не удастся зайти на страницу /admin, она будет доступна, только после успешной аутентификации. Также после аутентификации будет невозможно зайти на страницу /login, так как будет происходить редирект на главную страницу.


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


Ссылки по теме




getcomposer.org/doc/00-intro.md#installation-nix

laravel.com/docs/installation#install-laravel

cartalyst.com/manual/sentry/installation/laravel-4

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


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

Отправить комментарий