Статьи

Повышение безопасности с помощью Galleria — часть 5

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

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

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 должен раскрыть магию в лог-файле:

[#|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, позже:

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;
       }
   }

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

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);
           }
       }
   }

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

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 ()} (я добавил первые две строки для вашей ориентации:

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 и его переводы. И не забудьте добавить 

SessionConcierge.removeSession(session);

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

@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): 

<session-config>
      <session-properties>
          <property name="timeoutSeconds" value="1800" />
      </session-properties>
  </session-config>

и для WebLogic (weblogic.xml) 

<session-descriptor>
      <timeout-secs>180</timeout-secs>
</session-descriptor>

Интересует обработка ошибок в примере Galleria? Читайте дальше