Статьи

Пример Java EE 6 – Galleria – Часть 3

В предыдущих статьях ( часть 1 | часть 2 | часть 3 | часть 4 ) о примере Galleria вы познакомились с основами и начальным развертыванием как GlassFish, так и WebLogic. С сегодняшнего дня я пытаюсь добавить в него некоторые функции корпоративного уровня, поскольку я часто их просил в своих собственных проектах. Я знаю, что Vineet также со временем добавит больше возможностей, и я надеюсь, что это не будет слишком запутанным для читателей. Но давайте посмотрим, как это работает, и какие из моих функций приняты Vineet, а какие нет :). Дайте мне знать, если есть что-то особенное, что вы хотели бы видеть добавленным!

Фиксация сессии

Самая горячая тема для корпоративных приложений Java – безопасность. И поскольку в нем много разных аспектов, я решил начать с очень простой, но часто необходимой функции: предотвращение фиксации сессии. Это не очень специфично для Java или JSF, но является общей проблемой для веб-приложений. Фиксация сеанса возникает, когда идентификаторы сеанса легко обнаружить или угадать. Основным методом атаки является наличие идентификатора сеанса в URL-адресе или любой другой части ответа. Злоумышленник может зафиксировать сеанс, а затем встроить ссылку на свою страницу, обманом заставив пользователя посетить ее и стать частью сеанса. Затем, когда пользователь аутентифицирует сеанс, аутентифицируется Использование Cookies только дает определенную безопасность здесь, потому что чаще всего они также устанавливаются с помощью метода, который подразумевает потерю конфиденциальности. Большинство серверов приложений генерируют новый идентификатор сеанса с первым запросом. После того, как это аутентифицировано, оно снова используется в дальнейшем. Единственный способ предотвратить это – создать новый случайный сеанс после успешного запроса аутентификации.
Это легко сделать в общем. Перейдите в проект galleria-jsf и найдите объект info.galleria.view.user.Authenticator. Добавьте следующие строки в начало метода authenticate ():

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
String result = null;
ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
 
// Session Fixation Prevention
HttpSession session = (HttpSession) externalContext.getSession(false);
 
if (logger.isDebugEnabled()) {
       logger.debug("Session before authentication request: " + session.getId());
   }
 
session.invalidate();
session = (HttpSession) externalContext.getSession(true);
 
  if (logger.isDebugEnabled()) {
       logger.debug("Session after authentication request: " + session.getId());
   }

Вот и все. Очень легкая смена в первый раз касаясь кодовой базы. Переход на уровень отладки FINE для пакета info.galleria должен раскрыть магию в лог-файле:

1
2
3
4
5
6
7
8
[#|2012-03-27T17:17:25.298+0200|FINE|glassfish3.1.2|info.galleria.view.user.Authenticator|_ThreadID=27;
_ThreadName=Thread-4;ClassName=info.galleria.view.user.Authenticator;MethodName=
authenticate;|Session before authentication request: 33b1205d7ad740631978ed211bce|#]
 
[#|2012-03-27T17:17:25.301+0200|FINE|glassfish3.1.2|info.galleria.view.user.Authenticator
|_ThreadID=27
;_ThreadName=Thread-4;ClassName=info.galleria.view.user.Authenticator;MethodName
=authenticate;|Session after authentication request: 33b1f344ad1730c69bccc35e752e|#]

Как и ожидалось, мы изменили сеанс http во время запроса аутентификации. Вы также можете проверить это с помощью надстройки браузера по вашему выбору (в данном случае «Редактировать этот файл cookie»):

И приложение Galleria сделало это немного безопаснее. Если вы хотите узнать больше о фиксации сессии, прочитайте страницу OWASP .

Предотвратить несколько входов

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

В этом есть некоторые хитрости. Во-первых, вам нужен способ хранить всю информацию о пользователе и HttpSession для приложения. И второе: вам нужен кто-то, кто позаботится об этом. Начнем с самого последнего.

Вам нужен знаменитый синглтон здесь. Единственное место для хранения соответствующей информации HttpSession. Первой мыслью было бы использовать .getExternalContext (). GetApplicationMap (). Это может сработать. Ограничение входа в систему, которое мы размещаем здесь, имеет некоторые побочные эффекты. Представьте, что пользователь вошел в систему и рухнул в браузере, не выходя из системы раньше. В результате он / она не сможет снова войти в систему, пока не произойдет очистка или перезапуск приложения. Поэтому важно также иметь доступ к нему в HttpSessionListener. Учитывая тот факт, что JSF ExternalContext является ServletContext, мы здесь в безопасности.

Прежде чем продолжить, еще одно слово о кластеризации. Мы собираемся построить некластерируемую конструкцию здесь. Согласно спецификации сервлета атрибуты контекста являются локальными для JVM, в которой они были созданы. Таким образом, вы потеряете защиту, если запустите ее в кластерной среде, поскольку вы можете иметь сеанс на каждом узле кластера. Обеспечение безопасности этого кластера означает использование базы данных, компонента ejb или распределенного кэша.

Перейдите на info.galleria.view.util и создайте новый конечный класс с именем SessionConcierge. Нужны методы для добавления и удаления сеанса. И нам, очевидно, нужно что-то для обработки карты приложения. Начиная с метода addSession, который будет вызываться из управляемого компонента info.galleria.view.user.Authenticator, позже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public static boolean addSession(HttpSession session) {
       String account = FacesContext.getCurrentInstance().getExternalContext().getRemoteUser();
       String sessionId = session.getId();
       if (account != null && !getApplicationMap(session).containsKey(account)) {
           getApplicationMap(session).put(account, sessionId);
           if (logger.isDebugEnabled()) {
               logger.debug("Added Session with ID {} for user {}", sessionId, account);
           }
           return true;
       } else {
           logger.error("Cannot add sessionId, because current logged in account is NULL or session already assigned!");
           return false;
       }
   }

Это в основном проверяет, есть ли у нас вошедший в систему пользователь и назначен ли ему сеанс. Если есть пользователь, и он уже не использует сеанс, мы собираемся добавить текущий сеанс к карте приложения под учетной записью в качестве ключа. Далее немного удалим логику:

01
02
03
04
05
06
07
08
09
10
public static void removeSession(HttpSession session) {
       String sessionId = session.getId();
       String account = getKeyByValue(getApplicationMap(session), sessionId);
       if (account != null) {
           getApplicationMap(session).remove(account);
           if (logger.isDebugEnabled()) {
               logger.debug("Removed Session with ID {} for user {}", sessionId, account);
           }
       }
   }

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

1
2
3
4
5
6
7
8
private static <T, E> T getKeyByValue(Map<T, E> map, E value) {
    for (Entry<T, E> entry : map.entrySet()) {
        if (value.equals(entry.getValue())) {
            return entry.getKey();
        }
    }
    return null;
}

Готово. Одна вещь отсутствует. Метод getApplicationMap (сеанс HttpSession). Это не очень волшебно. Он просто пытается выяснить, нужно ли нам получить его через FacesContext или ServletContext. Посмотрите на источник SessionConcierge, если вам интересно. Последнее, что нужно сделать, это добавить SessionConcierge в Authenticator . Добавьте этот код в try {request.login ()} (я добавил первые две строки для вашей ориентации:

01
02
03
04
05
06
07
08
09
10
11
request.login(userId, new String(password));
           result = "/private/HomePage.xhtml?faces-redirect=true";
 
           // save sessionId to disable multiple sessions per user
           if (!SessionConcierge.addSession(session)) {
               request.logout();
               logger.error("User {} allready logged in with another session", userId);
               FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, Messages.getString(
                       "Login.AllreadyLoggedIn", locale), null);
               FacesContext.getCurrentInstance().addMessage(null, facesMessage);
           }

Если добавление HttpSession через SessionConcierge не удается, пользователь немедленно выходит из системы и добавляется FacesMessage. Не забудьте добавить это в galleria-jsf \ src \ main \ resources \ resources messages.properties и его переводы. И не забудьте добавить

1
SessionConcierge.removeSession(session);

для публичного выхода из системы (). Хорошо. Вот и все, не так ли? По крайней мере, это работает на данный момент. Но мы все еще должны решить проблему сбоя браузера. Если кто-то не выполняет выход из приложения, время сеанса истекло или происходит сбой браузера, вы не сможете войти в систему, пока приложение не будет перезапущено. Это странно и непреднамеренно. Необходим какой-то механизм для очистки. Как насчет HttpSessionListener? Это звучит здорово! Добавьте его в info.galleria.listeners и назовите его SessionExpirationListener .

01
02
03
04
05
06
07
08
09
10
@Override
public void sessionDestroyed(HttpSessionEvent se) {
    HttpSession session = se.getSession();
 
    SessionConcierge.removeSession(session);
 
    if (logger.isDebugEnabled()) {
        logger.debug("Session with ID {} destroyed", session.getId());
    }
}

Хорошо. Это должно работать сейчас. Давай и попробуй. Откройте два разных браузера и попробуйте войти с обоими. Только один позволит вам получить доступ к приложению. Второй должен ответить сообщением об ошибке, которое вы поместили в messages.properties. Обратите внимание, что это не предотвращение нескольких окон. Вы по-прежнему можете открыть столько окон на HttpSession, сколько захотите.

Небольшое добавление: если вы сильно полагаетесь на очистку HttpSessionListener, вы должны убедиться, что у нее есть правильное время жизни. Он настраивается с помощью дескриптора развертывания веб-приложения для конкретного продукта (например, weblogic.xml или glassfish-web.xml). Я рекомендую установить его на разумно низкое значение (например, 30 минут или меньше), чтобы пользователи не ждали слишком долго. Вот как это будет выглядеть для Glassfish (glassfish-web.xml):

1
2
3
4
5
<session-config>
      <session-properties>
          <property name="timeoutSeconds" value="1800" />
      </session-properties>
  </session-config>

и для WebLogic (weblogic.xml)

1
2
3
<session-descriptor>
      <timeout-secs>180</timeout-secs>
</session-descriptor>

Пример приложения Galleria Java EE 6 растет. Сегодня я собираюсь написать о том, как изящно бороться с ошибками. Много уже сделано для проверки пользовательского ввода, но все еще есть много ситуаций сбоя, которые не обрабатываются, но должны быть. Если вам интересно, что произошло в прошлом, ознакомьтесь с первыми частями серии: Основы , запуск на GlassFish , запуск на WebLogic , тестирование и повышение безопасности .

Общий механизм исключения

Приложение использует проверенные исключения для передачи ошибок между слоями. ApplicationException является корнем всех возможных бизнес-исключений.

Эти бизнес-исключения сообщают о нарушениях проверки и всех известных ошибках между доменом и уровнем представления. Классы <domain> Manager (например, AlbumManger) в проекте представления galleria-jsf ловят их и используют ExceptionPrecessor для заполнения сообщений об ошибках в представлении. Другой вид исключений, которые могут возникнуть между этими двумя уровнями, – RuntimeExceptions. Они помещаются в EJBException контейнером, а также перехватываются классами диспетчера <domain>. Они генерируют более общее сообщение об ошибке, которое показывается пользователю.

Я не собираюсь вдаваться в исключения, отмеченные как проверенные, так и не отмеченные здесь (Google немного об этом, если вам интересно). Я склонен использовать проверенные исключения, когда у приложения есть шанс восстановиться после ошибки. Непроверенные выбрасываются, когда происходит что-то, что не подлежит восстановлению. Вот почему я не доволен встроенным механизмом обработки исключений. Я собираюсь войти в это немного позже.

Чего не хватает? ViewExpired и многое другое.

Кажется, как будто все решается прямо сейчас. Но только на первое впечатление. Откройте экран входа в систему и немного подождите, и время ожидания http сессии истечет. Теперь вы встретили не очень хороший экран исключений ViewExpired.

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

1
2
3
4
<error-page>
    <exception-type>javax.faces.application.ViewExpiredException</exception-type>
    <location>/viewExpired.xhtml</location>
</error-page>

Теперь вы перенаправляете своих пользователей на выделенную страницу, которая может рассказать ему / ей что-то приятное о безопасности на рабочем месте и не оставлять приложение без присмотра в течение столь длительного времени. Это работает для большинства приложений там. Если вы хотите иметь некоторую дополнительную информацию на странице или просто хотите перехватить несколько исключений и обработать их по отдельности, не настраивая их статически, вам нужно нечто, называемое ExceptionHandler. Это новое в JSF 2, и все, что вам нужно сделать, это реализовать ExceptionHandler и его фабрику. Сама фабрика настроена в facex-config.xml, потому что для нее нет аннотаций.

Откройте файл face-config.xml и добавьте следующие строки внизу:

1
2
3
<factory>
     <exception-handler-factory>info.galleria.handlers.GalleriaExceptionHandlerFactory</exception-handler-factory>
 </factory>

Теперь мы собираемся реализовать GalleriaExceptionHandlerFactory в выделенном пакете. Интересный метод здесь:

1
2
3
4
5
6
@Override
   public ExceptionHandler getExceptionHandler() {
       ExceptionHandler result = parent.getExceptionHandler();
       result = new GalleriaExceptionHandler(result);
       return result;
   }

Он вызывается один раз за запрос, и каждый раз он должен возвращать новый экземпляр ExceptionHandler. Здесь вызывается реальный ExceptionHandlerFactory, и ему предлагается создать экземпляр, который затем оборачивается в пользовательский класс GalleriaExceptionHandler . Здесь происходят действительно интересные вещи.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public void handle() throws FacesException {
    for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) {
        ExceptionQueuedEvent event = i.next();
        ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
        Throwable t = context.getException();
        if (t instanceof ViewExpiredException) {
            ViewExpiredException vee = (ViewExpiredException) t;
            FacesContext fc = FacesContext.getCurrentInstance();
            Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
            NavigationHandler nav =
                    fc.getApplication().getNavigationHandler();
            try {
                // Push some stuff to the request scope for later use in the page
                requestMap.put("currentViewId", vee.getViewId());
                nav.handleNavigation(fc, null, "viewExpired");
                fc.renderResponse();
 
            } finally {
                i.remove();
            }
        }
    }
    // Let the parent handle all the remaining queued exception events.
    getWrapped().handle();
}

Выполните итерацию по необработанным исключениям, используя итератор, возвращенный из getUnhandledExceptionQueuedEvents (). Iterator (). ExeceptionQueuedEvent – это SystemEvent, из которого вы можете получить фактическое исключение ViewExpiredException. Наконец, вы извлекаете некоторую дополнительную информацию из исключения и помещаете ее в область запроса для доступа к ней через EL на следующей странице. Последнее, что нужно сделать для исключения ViewExpiredException, – это использовать систему неявной навигации JSF («viewExpired» разрешается в «viewExpired.xhtml») и перейти на страницу «viewExpired» через NavigationHandler. Не забудьте удалить обработанное исключение в блоке finally. Вы не хотите, чтобы это снова обрабатывалось родительским обработчиком исключений. Теперь нам нужно создать страницу viewExpired.xhtml. Сделайте это в папке galleria-jsf \ src \ main \ webapp.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns:ui="http://java.sun.com/jsf/facelets"
                template="./templates/defaultLayout.xhtml"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:h="http://java.sun.com/jsf/html"
                >
    <ui:define name="title">
        <h:outputText value="#{msg['Exception.page.title']}" />
    </ui:define>
 
    <ui:define name="content">
        <h:form>
            <h:outputText value="#{msg['Exception.page.message']}" />
            <p>You were on page #{currentViewId}.  Maybe that's useful.</p>
            <p>Please re-login via the <h:outputLink styleClass="homepagelink" value="#{request.contextPath}/Index.xhtml" ><h:outputText value="Homepage" /></h:outputLink>.</p>
        </h:form>
    </ui:define>
</ui:composition>

Обратите внимание, что я добавил новые свойства сообщений здесь, поэтому вам необходимо убедиться, что они помещены в galleria-jsf \ src \ main \ resources \ resources \ messages.properties и translations.

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

Рефакторинг обработки RuntimeException

Как я уже сказал, меня не устраивает то, как приложение обрабатывает RuntimeExceptions. Теперь, когда у нас есть хорошая централизованная обработка исключений, мы можем немного переместить эти вещи и реорганизовать классы * Manager. Удалите все эти catch (EJBException ejbEx) {блоки из всех них. Мы позаботимся о них в GalleriaExceptionHandler через минуту. Просто добавьте еще одну проверку в GalleriaExceptionHandler и перенаправьте пользователя на другую страницу, если выбрасывается любое другое исключение, кроме ViewExpiredException.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// check for known Exceptions
           if (t instanceof ViewExpiredException) {
               ViewExpiredException vee = (ViewExpiredException) t;
               // Push some stuff to the request scope for later use in the page
               requestMap.put("currentViewId", vee.getViewId());
 
           } else {
               forwardView = "generalError";
 
               Locale locale = fc.getViewRoot().getLocale();
               String key = "Excepetion.GeneralError";
               logger.error(Messages.getLoggerString(key), t);
               String message = Messages.getString(key, locale);
               FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
               fc.addMessage(null, facesMessage);
           }

Этот подход имеет некоторые преимущества. Это уменьшает необходимый код в классах * Manager, и у нас наконец-то есть центральное место, чтобы позаботиться об этих неисправимых исключениях. Это все еще не очень похоже на предприятие. Представьте, что ваша группа поддержки первого уровня должна присматривать за клиентами, и они начинают жаловаться, что единственное сообщение, которое они получают, – это «GeneralError». Это не очень полезно. Ваша группа поддержки должна была бы расширить ее, а второй или третий уровень должен будет проверить журналы и и, и … Все это из-за ошибки, о которой мы могли знать. Первое, что нужно сделать, это выяснить причину ошибки. Разбор следов стека не очень весело. Особенно это касается RuntimeExceptions, которые заключены в EJBExceptions и далее в FacesExceptions. Слава богу за Apache Commons ExceptionUtils . Откройте ваш galleria-jsf pom.xml и добавьте их как зависимости:

1
2
3
4
5
<dependency>
          <groupId>commons-lang</groupId>
          <artifactId>commons-lang</artifactId>
          <version>2.6</version>
      </dependency>

Теперь вы можете начать исследовать основную причину:

1
2
3
4
5
6
7
8
} else {
               forwardView = "generalError";
               // no known instance try to specify
 
               Throwable causingEx = ExceptionUtils.getRootCause(t);
               if (causingEx == null) {
                   causingEx = t;
               }
1
2
3
//...
 logger.error(Messages.getLoggerString(key), t);
requestMap.put("errorCode", errorCode);

Не забудьте также зарегистрировать полную трассировку стека (t, а не только, вызывая Ex) здесь. В общем, плохо сообщать пользователям об исключениях. Никто на самом деле не хочет, чтобы ошибки происходили (потому что мы ненавидим делать ошибки), и после всех следов стека исключений может раскрыться конфиденциальная информация, которую вы не хотели бы видеть где-нибудь на экране. Таким образом, вам нужно найти способ показать что-то значимое для пользователя, не раскрывая слишком много. Вот где известные коды ошибок вступают в игру. Используйте исключение первопричины в качестве ключа сообщения или примите свои собственные решения относительно того, какие усилия вы собираетесь приложить здесь. Это может быть система категорий ошибок (БД, интерфейсные системы и т. Д.), Которые дают первому уровню поддержки хороший совет о том, что является причиной ошибки. Я бы с самого начала придерживался более простого решения. Просто сгенерируйте UUID для каждого перехваченного исключения и отследите его до журнала и пользовательского интерфейса. Очень простым может быть следующее.

1
String errorCode = String.valueOf(Math.abs(new Date().hashCode()));

Это также следует добавить к свойствам сообщения и не забывать, что вам нужен еще один для шаблона generalError. Если slf4j будет использовать тот же формат сообщения, что и в журнале jdk, вам понадобится только одно свойство … в любом случае:

  Exception.generalError.log = Общая ошибка в журнале: {}. 
  Exception.generalError.message = Произошла общая ошибка с идентификатором {0}.  Пожалуйста, позвоните на нашу горячую линию. 

Добавьте это в файл generalError.xhtml и посмотрите, как код ошибки передается в шаблон сообщения.

1
2
3
<h:outputFormat  value="#{msg['Exception.generalError.message']}" >
               <f:param value="#{errorCode}"/>
           </h:outputFormat>

Здесь еще многое можно улучшить. Вы можете использовать javax.faces.application.ProjectStage для поиска текущего режима, в котором работает приложение. Если вы работаете в ProjectStage.Development, вы также можете поместить полную трассировку стека в пользовательский интерфейс и немного упростить отладку в реальном времени. Следующий фрагмент пытается получить ProjectStage от JNDI.

01
02
03
04
05
06
07
08
09
10
11
12
public static boolean isProduction() {
        ProjectStage stage = ProjectStage.Development;
        String stageValue = null;
        try {
            InitialContext ctx = new InitialContext();
            stageValue = (String) ctx.lookup(ProjectStage.PROJECT_STAGE_JNDI_NAME);
            stage = ProjectStage.valueOf(stageValue);
        } catch (NamingException | IllegalArgumentException | NullPointerException e) {
            logger.error("Could not lookup JNDI object with name 'javax.faces.PROJECT_STAGE'. Using default 'production'");
        }
        return ProjectStage.Production == stage;
    }

А как насчет 3-значных страниц ошибок Http?

Это еще одна вещь, чтобы заботиться о. Все эти оставшиеся 3-значные коды ошибок http, которые возвращают одну из тех не красиво выглядящих страниц ошибок. Единственное, что нужно сделать, это отобразить их в файле web.xml, как показано ниже:

1
2
3
4
<error-page>
        <error-code>404</error-code>
        <location>/404.xhtml</location>
    </error-page>

Вы должны убедиться, что эти сопоставления на месте и представить значительную ошибку для ваших пользователей. Лучше всего всегда предлагать способ двигаться дальше оттуда.

Ссылка: пример Java EE 6 – повышение безопасности с помощью Galleria – часть 5 , пример Java EE 6 – изящная работа с ошибками в Galleria – часть 6 от нашего партнера по JCG Маркуса Эйзела (Markus Eisele) из блога Enterprise Software Development с Java .