Каждый хочет все виды интеграции для Tapestry с другими фреймворками, но иногда накатывать свои собственные на самом деле проще. Давайте начнем с обеспечения доступа к страницам, тема, которая все еще продолжает появляться в списке рассылки. Я думал, что покажу немного о том, как я решаю эту проблему в целом.
Люди просят одно единственное окончательное решение для обеспечения безопасности … но я не вижу ни одного единственного решения, удовлетворяющего даже большинство проектов. Зачем? Потому что просто слишком много переменных. Например, вы используете LDAP, OpenAuth или какой-то специальный реестр пользователей (в вашей базе данных)? Страницы доступны по умолчанию или недоступны по умолчанию? Вы используете безопасность на основе ролей? Как вы представляете роли тогда? Создание единого решения, которое достаточно подключаемо для всех этих возможностей, кажется непреодолимой задачей … но, возможно, мы можем придумать инструментарий, чтобы вы могли собрать собственное решение (подробнее об этом позже).
Одним из подходов к безопасности может быть определение базового класса ProtectedPage, который обеспечивает соблюдение основных правил (вы должны войти в систему, чтобы использовать эту страницу). Вы можете сделать это, используя обработчик событий активации … но я считаю такой подход неуклюжим. В любое время вы можете избежать наследования, вы обнаружите, что ваш код легче понять, легче управлять, легче тестировать и легче развивать.
Вместо этого давайте продолжим декларативный подход, в котором мы используем аннотацию для пометки страниц, требующих входа пользователя. Мы начнем с этих основных правил:
- Страницы доступны любому, если у них нет аннотации @RequiresLogin
- Любой статический ресурс (в каталоге веб-контекста) доступен любому
- Уже есть какая-то служба UserAuthentication, которая знает, вошел ли пользователь в данный момент в систему или нет, и (если вошел в систему), кто он, как объект User
Итак, нам нужно определить аннотацию requireLogin, и нам нужно обеспечить ее выполнение, запретив любой доступ к странице, если пользователь не вошел в систему.
Это создает проблему: как вы получаете «внутри» Гобелен, чтобы обеспечить выполнение этой аннотации? То, что вы действительно хотите сделать, это «вставить» немного вашего кода в существующий код Tapestry … код, который анализирует входящий запрос, определяет, какой это тип запроса (запрос на визуализацию страницы или запрос события компонента). ), и в конечном итоге начинает вызывать код страницы для выполнения работы.
Это отличный пример центрального дизайна Tapestry и его контейнера IoC: встроенная поддержка такого рода расширяемости. Благодаря использованию сервисных конфигураций можно сделать именно это: вставить кусок кода в середину этого кода Tapestry по умолчанию. Хитрость заключается в том, чтобы определить, где. Это изображение дает приблизительную карту того, как Tapestry обрабатывает входящие запросы:
Фактически, для такого рода расширения есть определенное место: служба конвейера ComponentRequestHandler 1 . Как служба конвейера, ComponentRequestHandler имеет конфигурацию фильтров, и добавление фильтра в этот конвейер — это как раз то, что нам нужно.
Определение аннотации
Во-первых, давайте определим нашу аннотацию:
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresLogin {
}
Эта аннотация предназначена для размещения в классе страницы, чтобы указать, что пользователь должен войти в систему для доступа к странице. Здесь важна политика хранения: она должна быть видимой во время выполнения, чтобы наш код времени выполнения мог видеть ее и действовать в соответствии с ее присутствием.
Сама аннотация ничего не делает … нам нужен код, который проверяет аннотацию.
Создание ComponentRequestFilter
Фильтры для конвейера ComponentRequestHandler являются экземплярами интерфейса ComponentRequestFilter :
/**
* Filter interface for {@link org.apache.tapestry5.services.ComponentRequestHandler}.
*/
public interface ComponentRequestFilter
{
/**
* Handler for a component action request which will trigger an event on a component and use the return value to
* send a response to the client (typically, a redirect to a page render URL).
*
* @param parameters defining the request
* @param handler next handler in the pipeline
*/
void handleComponentEvent(ComponentEventRequestParameters parameters, ComponentRequestHandler handler)
throws IOException;
/**
* Invoked to activate and render a page. In certain cases, based on values returned when activating the page, a
* {@link org.apache.tapestry5.services.ComponentEventResultProcessor} may be used to send an alternate response
* (typically, a redirect).
*
* @param parameters defines the page name and activation context
* @param handler next handler in the pipeline
*/
void handlePageRender(PageRenderRequestParameters parameters, ComponentRequestHandler handler) throws IOException;
}
Наша реализация этого фильтра проверит страницу, на которую ссылается запрос, чтобы узнать, есть ли в ней аннотация. Если аннотация присутствует, а пользователь еще не вошел в систему, мы перенаправим на страницу входа. Когда перенаправление не требуется, мы делегируем следующему обработчику в конвейере 2 :
public class RequiresLoginFilter implements ComponentRequestFilter {
private final PageRenderLinkSource renderLinkSource;
private final ComponentSource componentSource;
private final Response response;
private final AuthenticationService authService;
public PageAccessFilter(PageRenderLinkSource renderLinkSource,
ComponentSource componentSource, Response response,
AuthenticationService authService) {
this.renderLinkSource = renderLinkSource;
this.componentSource = componentSource;
this.response = response;
this.authService = authService;
}
public void handleComponentEvent(
ComponentEventRequestParameters parameters,
ComponentRequestHandler handler) throws IOException {
if (dispatchedToLoginPage(parameters.getActivePageName())) {
return;
}
handler.handleComponentEvent(parameters);
}
public void handlePageRender(PageRenderRequestParameters parameters,
ComponentRequestHandler handler) throws IOException {
if (dispatchedToLoginPage(parameters.getLogicalPageName())) {
return;
}
handler.handlePageRender(parameters);
}
private boolean dispatchedToLoginPage(String pageName) throws IOException {
if (authService.isLoggedIn()) {
return false;
}
Component page = componentSource.getPage(pageName);
if (! page.getClass().isAnnotationPresent(RequiresLogin.class)) {
return false;
}
Link link = renderLinkSource.createPageRenderLink("Login");
response.sendRedirect(link);
return true;
}
}
Приведенный выше код делает кучу предположений и упрощений. Во-первых, предполагается, что имя страницы для перенаправления — «Логин». Он также не пытается перехватить какую-либо часть входящего запроса, чтобы позволить приложению продолжить работу после входа пользователя. Наконец, AuthenticationService не является частью Tapestry … это что-то специфическое для приложения.
Вы заметите , что зависимость (PageRenderLinkSource и т.д.) вводится через параметры конструктора , а затем сохраняется в конечных полях. Это предпочтительный, если более подробный подход. Мы также могли бы использовать не конструктор, а неконечные поля с аннотацией @Inject (это в основном выбор стиля, хотя внедрение конструктора с финальными полями более гарантированно является полностью безопасным для потоков).
Однако одного класса недостаточно: нам нужно получить Tapestry, чтобы фактически использовать этот класс.
Вклад фильтра
Последняя часть этого заключается в подключении вышеуказанного кода в поток. Это делается путем внесения вклада в конфигурацию службы ComponentEventHandler .
Вклады сервиса реализуются как методы класса модуля Tapestry, такого как AppModule:
public static void contributeComponentRequestHandler(
OrderedConfiguration configuration) {
configuration.addInstance("RequiresLogin", RequiresLoginFilter.class);
}
Вкладывающие модули вносят вклад в OrderedConfiguration: после того, как все модули имеют возможность внести свой вклад, конфигурация преобразуется в список, который передается реализации сервиса.
Метод addInstance () облегчает добавление фильтра: Tapestry будет смотреть на класс, видеть конструктор и вставлять зависимости в фильтр через параметры конструктора. Все это очень декларативно: код нуждается в PageRenderLinkSource, поэтому он просто определяет конечное поле и параметр конструктора … Гобелен позаботится обо всем остальном.
Вы можете задаться вопросом, почему мы должны указать имя («Требуется регистрация») для вклада? Ответ касается несколько редкого, но все же важного случая: множественные вклады в одну и ту же конфигурацию, которые имеют некоторую форму взаимодействия. Присвоив каждому вкладу уникальный идентификатор, можно установить правила упорядочения (например, «вклад« Foo »идет после вклада« Бар »»). Здесь нет необходимости в заказе, потому что других фильтров нет (Tapestry предоставляет эту услугу и конфигурацию, но не вносит в нее свой вклад).
Улучшения и выводы
Это всего лишь первый проход в безопасности. Для моих клиентов я разработал более сложные решения, которые включают захват имени страницы и контекста активации, чтобы приложение могло «возобновить» после завершения входа в систему, а также подходы для автоматического входа пользователя по мере необходимости (через cookie или другой механизм).
Другие улучшения будут заключаться в ограничении доступа к страницам на основе некоторого набора пользовательских ролей; Опять же, то, как это представлено как в коде и аннотациях, так и в модели данных, вполне подойдет.
Мой опыт работы с разными клиентами действительно подчеркивает, насколько нечеткой может быть безопасность в мире: существует очень много вариантов того, как вы представляете, идентифицируете и аутентифицируете пользователя. Даже базовые решения, лежащие в основе, подлежат интерпретации; например, один из моих клиентов хочет, чтобы все страницы требовали входа в систему, если не найдена конкретная аннотация. Возможно, со временем достаточно этих вариантов использования, чтобы создать инструментарий, о котором я упоминал ранее.
Тем не менее , количество коды для создания прочной, реализации пользовательских безопасности все еще довольно мало … хотя трюк, как всегда, писать только код записи и закреплять его в гобелен в только правильном пути.
Я ожидаю продолжения этой статьи в части 2, которая немного расширит возможности решения, а также устранит некоторые из реальных ограничений, с которыми сталкиваются мои клиенты. Оставайтесь в курсе!
1 Фактически, этот сервис и конвейер были созданы в Tapestry 5.1 специально для решения этого варианта использования. В Tapestry 5.0 этот подход требовал двух очень похожих вкладов фильтра в два одинаковых конвейера.
2 Если имеется несколько фильтров, можно подумать, что вы делегируете следующий фильтр. На самом деле вы делаете, но Tapestry обеспечивает мост : обертка вокруг фильтра, который использует основной интерфейс для службы. Таким образом, каждый фильтр одинаково делегирует либо следующему фильтру, либо терминатору (реализация службы после всех фильтров). Более подробно об этом в документации по конвейеру .