Одно из золотых правил Symfony2 — никогда не хардкодить внутри кода или шаблонов какие-либо ссылки и пути. Соблюдение этого правила и генерация ссылок через роутер значительно облегчат вашу жизнь. Однако есть одна вещь, которую я часто наблюдаю: люди продолжают хардкодить ссылки на выход из системы, например, как "/logout", только вот сам процесс логаута немного сложнее, чем может казаться и использование такой ссылки может работать в большинстве случаев, но это не будет лучшим решением проблемы.
Большинство разработчиков знают, что можно сделать несколько защищенных разделов внутри одного проекта. Например, это может быть панель для обычных пользователей (зарегистрированные пользователи) с путем /secure. И, возможно, у вас в проекте может быть отдельная панель администрирования по адресу /admin и есть отдельная зона для пользователей API, которая находится в разделе с адресом /api. Также, можно сделать «защищенную зону», которой и вовсе не нужна защита — такой подход используется в тулбаре Symfony2 для разработчиков. В конце концов, можно вообще все это перенести в одну большую зону, в которой будет реализовано несколько вариантов определения кто же имеет доступ к защищенной части проекта. Вообще, хоть разделение проекта на отдельные зоны и делает ваш проект сложнее, это дает некоторые преимущества.
Каждая из защищенных зон вызывает свой файрвол, который и определяет, аутентифицировать пользователя или нет. Каждый файрвол отделен от других: если вы аутентифицировались в одном из них, это не значит, что вы автоматически аутентифицированы в других и есть лишь один активный файрвол (тот самый, который совпал с шаблоном URL). Это имеет значение, так как разные файрволы могут использовать разные базы данных или просто использовать разные способы аутентификации (например, в API можно использовать OAuth Token, в то время как остальные разделы могут использовать форму для входа).
Это также означает, что каждый файрволл имеет разные пути логаута, а для некоторых из них логаут как таковой и не существует. Пример security.yml
Это блок «firewall» в security.yml и в нем определено 3 файрвола — dev, superadminstuff и main. dev вообще не использует аутентификацию (security=false), что означает, что доступ разрешен всем и пути "/js", "/css" и другие не управляются файрволом main.
Следующий набор правил защищает зону администрирования. В ней используется http_basic в качестве входа, то есть браузер покажет диалоговое окно, в котором попросит вас указать логин и пароль (на самом деле это не очень безопасно, так как они будут передаваться как plain text). Более того — браузер будет отправлять логин и пароль при каждом запросе к проекту. Symfony2 может проверить эти данные используя провайдер «memory_user_provider», блок которого я не привел, но в нем, обычно, указывается несколько стандартных пользователей и их логин/пароль (прямо в файле конфигурации, а не в базе данных).
В http-basic в действительности нет логаута потому, что единственный способ выхода в таком случае — прекратить отправлять запросы. Очистка кеша или или перезапуск браузера обычно помогает сделать логаут в таком случае.
Последний файрвол — main. Вместо http-basic он использует форму для входа. Здесь используется FOSUserBundle, в котором есть своя форма входа и методы для ее обработки, поэтому единственное что требуется от разработчика — немного кастомизировать их, а не писать свои.
В случае, если вы открываете страницу в этом файрволе, и не вошли ранее — Symfony2 автоматически перенаправит пользователя на страницу входа, которая указана в в параметре login_path в блоке form_login. Обычно (по-умолчанию), это путь с адресом /login (его можно изменить как вам нравится или даже, если хотите, можете указать роут). Как только пользователь вошел, Symfony2 сохранит пользователя и роль внутри его сессии и при следующем запросе пользователю не придется входить снова.
Выход из такого файрвола довольно прост — нужно перейти на его страницу выхода. Но что это за страница?
В примере выше, используется параметр «logout: true». Стоит обратить внимание, что этот параметр находится в блоке файрвола, а не в блоке form_login. Указывая logout: true, мы говорим Symfony2 использовать стандартные настройки логаута, а именно:
Как можно заметить, указывается путь, по которому будет происходить выход. Но есть одна странность: по-умолчанию, логаут листенер запускается перед вызовом какого-нибудь контроллера или экшена, а затем делает редирект на страницу, указанную в параметре «target». Если у вас свой обработчик logout-события, который указывается в параметре «handlers», и он НЕ возвращает объект HTTP Response, то вызывается текущий роут. То есть по-умолчанию, ваши контроллер/экшн не будут вызваны, НО они должны быть указаны (то есть, роутер Symfony2 обязать знать о нем). По этой причине можно найти странный экшн logout в FOSUserBundle, бросающий исключение, так как он никогда не будет вызван.
Итак, что же делать с выходом? В первую очередь, не стоит хардкодить URL. Даже если вы используете маршрут вместо url, вы можете поменять его внутри конфигурации и выход перестанет работать. Что действительно стоит делать — указывать через twig ссылку или маршрут, указанный в конфигурации. К счастью, SecurityBundle имеет расширение для twig, который поможет это сделать. Речь идет о функциях logout_url и logout_path. Эти функции получают на вход id файрвола (например, «main», «dev» и т.д.) и генерирует правильный адрес выхода для него:
В этом случае произойдет выборка правильного адреса и в качестве бонуса добавится csrf-token, если это было указано в конфигурации. Таким образом, вместо того, чтобы указать в шаблоне адрес страницы, нужно указывать тот файрвол, который используется в данный момент.
Правда, теперь ваши шаблоны знают больше чем надо и необходимо указать имя файрвола вручную. В большинстве случаев это нормально, но иногда это может вызвать проблемы (например, если вы используете меню, где используется twig). Чтобы избежать проблем с этим существует возможность получить имя текущего файрвола, пусть немного и неправильная:
Внутри токена контекста безопасности находится необходимое нам название текущего файрвола. «Неправильность» решения в том, что глобальная переменная app.security в Symfony версии 2.6 будет в статусе deprecated и удалена в версии 3.0. Со временем, уверен, будут и другие пути генерации пути для выхода.
Немного информации о компоненте (и бандле) Symfony2 Security
Большинство разработчиков знают, что можно сделать несколько защищенных разделов внутри одного проекта. Например, это может быть панель для обычных пользователей (зарегистрированные пользователи) с путем /secure. И, возможно, у вас в проекте может быть отдельная панель администрирования по адресу /admin и есть отдельная зона для пользователей API, которая находится в разделе с адресом /api. Также, можно сделать «защищенную зону», которой и вовсе не нужна защита — такой подход используется в тулбаре Symfony2 для разработчиков. В конце концов, можно вообще все это перенести в одну большую зону, в которой будет реализовано несколько вариантов определения кто же имеет доступ к защищенной части проекта. Вообще, хоть разделение проекта на отдельные зоны и делает ваш проект сложнее, это дает некоторые преимущества.
Каждая из защищенных зон вызывает свой файрвол, который и определяет, аутентифицировать пользователя или нет. Каждый файрвол отделен от других: если вы аутентифицировались в одном из них, это не значит, что вы автоматически аутентифицированы в других и есть лишь один активный файрвол (тот самый, который совпал с шаблоном URL). Это имеет значение, так как разные файрволы могут использовать разные базы данных или просто использовать разные способы аутентификации (например, в API можно использовать OAuth Token, в то время как остальные разделы могут использовать форму для входа).
Это также означает, что каждый файрволл имеет разные пути логаута, а для некоторых из них логаут как таковой и не существует. Пример security.yml
# Раздел разработчика
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# Раздел админа
superadminstuff:
pattern: ^/admin
http_basic:
provider: memory_user_provider
realm: "Super Admin section!"
# все остальное с обычной формой для входа
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: /login
logout: true
Это блок «firewall» в security.yml и в нем определено 3 файрвола — dev, superadminstuff и main. dev вообще не использует аутентификацию (security=false), что означает, что доступ разрешен всем и пути "/js", "/css" и другие не управляются файрволом main.
Следующий набор правил защищает зону администрирования. В ней используется http_basic в качестве входа, то есть браузер покажет диалоговое окно, в котором попросит вас указать логин и пароль (на самом деле это не очень безопасно, так как они будут передаваться как plain text). Более того — браузер будет отправлять логин и пароль при каждом запросе к проекту. Symfony2 может проверить эти данные используя провайдер «memory_user_provider», блок которого я не привел, но в нем, обычно, указывается несколько стандартных пользователей и их логин/пароль (прямо в файле конфигурации, а не в базе данных).
В http-basic в действительности нет логаута потому, что единственный способ выхода в таком случае — прекратить отправлять запросы. Очистка кеша или или перезапуск браузера обычно помогает сделать логаут в таком случае.
Последний файрвол — main. Вместо http-basic он использует форму для входа. Здесь используется FOSUserBundle, в котором есть своя форма входа и методы для ее обработки, поэтому единственное что требуется от разработчика — немного кастомизировать их, а не писать свои.
В случае, если вы открываете страницу в этом файрволе, и не вошли ранее — Symfony2 автоматически перенаправит пользователя на страницу входа, которая указана в в параметре login_path в блоке form_login. Обычно (по-умолчанию), это путь с адресом /login (его можно изменить как вам нравится или даже, если хотите, можете указать роут). Как только пользователь вошел, Symfony2 сохранит пользователя и роль внутри его сессии и при следующем запросе пользователю не придется входить снова.
Выход из такого файрвола довольно прост — нужно перейти на его страницу выхода. Но что это за страница?
В примере выше, используется параметр «logout: true». Стоит обратить внимание, что этот параметр находится в блоке файрвола, а не в блоке form_login. Указывая logout: true, мы говорим Symfony2 использовать стандартные настройки логаута, а именно:
logout:
csrf_parameter: _csrf_token
csrf_token_generator: ~
csrf_token_id: logout
path: /logout
target: /
success_handler: ~
invalidate_session: true
delete_cookies:
name:
path: null
domain: null
handlers: []
Как можно заметить, указывается путь, по которому будет происходить выход. Но есть одна странность: по-умолчанию, логаут листенер запускается перед вызовом какого-нибудь контроллера или экшена, а затем делает редирект на страницу, указанную в параметре «target». Если у вас свой обработчик logout-события, который указывается в параметре «handlers», и он НЕ возвращает объект HTTP Response, то вызывается текущий роут. То есть по-умолчанию, ваши контроллер/экшн не будут вызваны, НО они должны быть указаны (то есть, роутер Symfony2 обязать знать о нем). По этой причине можно найти странный экшн logout в FOSUserBundle, бросающий исключение, так как он никогда не будет вызван.
Логаут
Итак, что же делать с выходом? В первую очередь, не стоит хардкодить URL. Даже если вы используете маршрут вместо url, вы можете поменять его внутри конфигурации и выход перестанет работать. Что действительно стоит делать — указывать через twig ссылку или маршрут, указанный в конфигурации. К счастью, SecurityBundle имеет расширение для twig, который поможет это сделать. Речь идет о функциях logout_url и logout_path. Эти функции получают на вход id файрвола (например, «main», «dev» и т.д.) и генерирует правильный адрес выхода для него:
<a href="{{ logout_path('main') }}">Logout</a>
В этом случае произойдет выборка правильного адреса и в качестве бонуса добавится csrf-token, если это было указано в конфигурации. Таким образом, вместо того, чтобы указать в шаблоне адрес страницы, нужно указывать тот файрвол, который используется в данный момент.
Правда, теперь ваши шаблоны знают больше чем надо и необходимо указать имя файрвола вручную. В большинстве случаев это нормально, но иногда это может вызвать проблемы (например, если вы используете меню, где используется twig). Чтобы избежать проблем с этим существует возможность получить имя текущего файрвола, пусть немного и неправильная:
<a href="{{ logout_path(app.security.token.providerKey) }}">Logout</a>
Внутри токена контекста безопасности находится необходимое нам название текущего файрвола. «Неправильность» решения в том, что глобальная переменная app.security в Symfony версии 2.6 будет в статусе deprecated и удалена в версии 3.0. Со временем, уверен, будут и другие пути генерации пути для выхода.
Only registered users can participate in poll. Log in, please.
Firewall
41.7% Файрвол98
58.3% Файерволл137
235 users voted. 105 users abstained.