Обеспечение безопасности в многопользовательском приложении SaaS

Аутентификация и авторизация с использованием инфраструктуры Spring Security и Apache Directory Server

Приложения, поставляемые в виде сервисов (SaaS или Software as a Service) имеют «коммунальную» или многопользовательскую (англ. multitenant) природу – т.е. с одной версией приложения одновременно могут работать множество различных пользователей, что делает безопасность серьезным источником беспокойства. В этой статье предлагается жизнеспособный практический подход к обеспечению безопасности многопользовательских приложений Java™ с помощью сочетания открытой инфраструктуры Spring Security и сервера Apache Directory. Для демонстрации этого подхода авторы представляют пример многопользовательского Web-приложения.

Массимилиано Парлионе, архитектор ПО, IBM

Massimiliano ParlioneМассимилиано (Макс) Парлионе - работает архитектором решений в инновационном центре IBM в Дублине, Ирландия. Он занимается проектами в области микрофинансирования. Массимилиано получил степень бакалавра (Лауреа) компьютерных наук в университете Л'Акуила в 1995 году и докторскую степень по компьютерной инженерии в римском университете Ла Сапиенса в 2000 году. Он является соавтором книг IBM "Введение в Tivoli Monitoring for Web Infrastructure" и "Tivoli Monitoring версии 5.1.1 от IBM. Создание моделей ресурсов и провайдеров."



Чико Чарльзворт, ведущий Java разработчик, IBM

Chico CharlesworthЧико Чарльзворт - ведущий Java-разработчик с более чем 8-летним опытом. После окончания со степенью бакалавра компьютерных наук Стаффордширского университета в Англии он занимался в основном разработкой с использованием коммерческих технологий Java для таких отраслей, как телекоммуникации, электронный биллинг, экологические технологии и микрофинансирование. Круг его интересов включает Java EE, технологии с открытым кодом и проектирование приложений.



14.09.2011

Применение SaaS в бизнесе

Модель SaaS в последние годы получает всё большее распространение. Все больше компаний обращаются к подобным решениям доступным «по-требованию», чтобы решать задачи бизнеса быстрее и с меньшими затратами. Исследование, проведенное в 2007 году компанией Forrester Research, показало, что 16 процентов крупных предприятий и 15 процентов средних и малых предприятий используют SaaS. По сравнению с предыдущим годом рост составил 33 и 50 процентов соответственно (см. раздел Ресурсы). А согласно мнению аналитической компании Saugatuck Technology, опубликованному в мае 2008 года, к 2012 году не менее 70 процентов предприятий, имеющих более 100 сотрудников, будут использовать по крайней мере одно приложение, поставляемое в виде услуги (SaaS).

Используя решения SaaS, поставщики сервисов могут достигать экономии на масштабе, предоставляя свои приложения в аренду, и возможно, проникая в ранее не охваченные сегменты рынка. Главным преимуществом многопользовательских решений SaaS является то, что они дают поставщикам услуг возможность обслуживать множество клиентов (см. раздел Ресурсы). Лучшим способом обеспечения безопасности в приложениях SaaS, в которых множество пользователей совместно используют одни и те же ресурсы, является логическое разбиение на разделы данных и конфигурации (на основе идентификаторов арендаторов).

В этой статье показана реализация эффективной передовой линии защиты многопользовательского приложения SaaS, написанного на Java. В предлагаемом решении сочетается применение Spring Security – подтвердившей свою эффективность инфраструктуры безопасности с открытым кодом - и Apache Directory Server – популярного сервера с открытым кодом, основанного на Java и поддерживающего протокол LDAP (Lightweight Directory Access Protocol) 3-ей версии. Предлагаемое решение доступно в виде примера Web-приложения на Java, которое можно развернуть на сервере Apache Tomcat или на Apache Geronimo.

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

Аутентификация и авторизация в многопользовательском приложении SaaS

Аутентификация и авторизация – это две главные задачи обеспечения безопасности в реальных приложениях:

  • Аутентификация – это процесс, позволяющий приложению убедиться, что человек (или другое приложение, смарт-карта и т.д.) является именно тем, за кого он себя выдает при создании соединения.
  • Авторизация определяет права и полномочия пользователя в системе. После аутентификации пользователя процесс авторизации определяет, что этому пользователю разрешено делать в системе. Поэтому обычно предполагается, что авторизация следует за аутентификацией.

Аутентификация и авторизация обуславливают повышенную сложность приложений SaaS. Инфраструктуру аутентификации и авторизации безопасного решения SaaS можно спроектировать двумя способами - централизованным и федеративным. В решении, предлагаемом в этой статье, используется централизованная система аутентификации (сервер LDAP). Централизованная система аутентификации не исключает и возможности поддержки распределенного каталога, хранящего информацию, которая может быть разделена на разделы или реплицирована. В данной статье не рассматривается альтернативный децентрализованный подход, называемый федеративным управлением идентификацией. Федеративное управление идентификацией порождает множество новых требующих решения задач безопасности в приложениях SaaS. (Типичные примеры таких проблем - это междоменные скрипты, основанная на Web единая регистрация, обеспечение междоменной работы учетной записи пользователя, междоменное управление правами и междоменный обмен атрибутами пользователей. Эти темы освещаются в статье "Meeting the SaaS Security Challenge", ссылка на которую приведена в разделе Ресурсы.)


Введение в Spring Security

Spring Security: мощнее, чем Acegi

Spring Security появилась в 2003 и изначально называлась Acegi Security System for Spring (Система безопасности Acegi для Spring). В конце 2007 года она стала официальным проектом Spring и была переименована в Spring Security. Мы рекомендуем вам использовать Spring Security вместо Acegi по следующим причинам:

  • Она дает те же самые преимущества, что и Acegi, и даже больше.
  • Она использует особую конфигурацию пространства имен Spring 2.0.
  • Сложные детали механизма безопасности теперь скрыты за более простой XML-конфигурацией.
  • Она имеет возможности автоконфигурации.

По умолчанию ни в одной из схем аутентификации Java Enterprise Edition (Java EE) 5 (базовой, основанной на формах, сборной или основанной на клиентских сертификатах) не поддерживаются дополнительные атрибуты, такие как идентификатор пользователя. Для реализации поддержки многопользовательской модели приложения необходимо разработать дополнительное решение. В этой статье мы покажем, как построить подобное решение с помощью инфраструктуры Spring Security.

Инфраструктура Spring Security предоставляет полноценное решение, существенно упрощающее реализацию мер безопасности в приложениях Java EE. Она предоставляет высокий уровень абстракции, позволяющий применять различные модели аутентификации, а также поддерживает богатые возможности авторизации. Кроме того, она обеспечивает высокую переносимость между различными серверами приложений. Вот некоторые из возможностей инфраструктуры Spring Security:

  • Декларативная безопасность
  • Поддержка множества схем аутентификации и авторизации, таких как базовая, основанная на формах, основанная на дайджестах, JDBC и LDAP.
  • Поддержка безопасности на уровне методов и аннотаций безопасности JSR-250
  • Поддержка однократного предъявления пароля
  • Поддержка интеграции контейнеров
  • Поддержка анонимных сеансов, одновременных сеансов, режима «запомни меня», безопасности на уровне канала и многое другое

Данная статья сосредоточена на интеграции Spring Security напрямую с LDAP. Возможны и другие варианты развертывания с использованием таких альтернатив, как сервис Java для аутентификации и авторизации (Java Authentication and Authorization Service или JAAS) или аутентификация, управляемая контейнером через адаптеры, доступные через инфраструктуру Spring Security.


Обзор LDAP и Apache Directory

В коммерческих системах управление пользователями и ролями зачастую осуществляется с помощью сервера LDAP. Существует несколько как открытых, так и коммерческих решений LDAP (см. Ресурсы). Для читателей, незнакомых с LDAP и Apache Directory Server, далее приводится их краткое описание.

Основы LDAP

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

Как способ аутентификации и авторизации каталог LDAP имеет следующие преимущества над реляционными СУБД:

  • Имеется связанный с каталогом протокол; нет необходимости в драйверах
  • Гибкая схема
  • Сфокусированность на пользователях:
    • Аутентификация, авторизация и аудит
    • Группы
    • Безопасность (пароли, сертификаты)

Основы Apache Directory Server

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

Проект Apache Directory поддерживает сервер Apache Directory, сертифицированный на соответствие LDAP версии 3, а также набор основанных на Eclipse инструментов работы с каталогами - Apache Directory Studio (см. раздел Ресурсы).


Интеграция инфраструктуры Spring Security и сервера Apache Directory в многопользовательской среде

В нормальных условиях конфигурирование Spring Security для работы с сервером LDAP не представляет сложности. И хотя в многопользовательской среде интеграция этих двух технологий также относительно проста, все же требуется немного больше усилий. Сначала мы обсудим создание совмещенного реестра пользователей в сервере Apache Directory, а затем покажем, как решение для динамической маршрутизации LDAP упрощает эффективное осуществление многопользовательской безопасности.

Совмещенный реестр пользователей сервера Apache Directory

В этом разделе мы опишем пример совмещенного реестра пользователей, состоящего из двух областей безопасности, называемых tenant1 и tenant2 (т.е. арендаторы 1 и 2), каждая из которых имеет 2 группы пользователей – администраторов и гостей. Каждая группа имеет множество прикрепленных к ней пользователей, которые совместно используют заданную роль и принадлежат области безопасности, соответствующей данной группе.

Данные для аутентификации пользователей, находящихся в разных областях безопасности, хранятся в отдельных поддеревьях реестра пользователей сервера Apache Directory, как показано на рисунке 1. Различным областям безопасности назначаются различные суффиксы LDAP. Например, для группы tenant1 базовое уникальное имя выглядит как [dc=tenant1, dc=com], а для группы tenant2 - [dc=tenant2, dc=com].

Рисунок 1. Пример совмещенного LDAP-реестра пользователей
Example multitenant LDAP user registry

Затем каждой области безопасности назначаются различные группы, которые на практике соответствуют ролям пользователей. Например группы [cn=adm, ou=groups] и [cn=gst, ou=groups], соответствующие ролям администратора и гостя, принадлежат к области безопасности tenant1 с базовым уникальным именем DN [dc=tenant1, dc=com].

В свою очередь с каждой конкретной группой в данной области безопасности ассоциированы различные пользовательские записи. Например, пользователь [uid=tenant1admin, ou=people] имеет роль администратора [cn=adm, ou=groups] и принадлежит области безопасности [dc=tenant1, dc=com].

Для каждой области безопасности, показанной на рисунке 1, необходимо создать новый раздел и контекст безопасности в конфигурационном файле server.xml сервера Apache Directory (находится по адресу: установочный каталог сервера Apache Directory/instances/default/conf/server.xml), как показано в листинге 1:

Листинг 1. файл server.xml сервера Apache Directory
<?xml version="1.0" encoding="UTF-8"?>
<spring:beans xmlns:spring="http://xbean.apache.org/schemas/spring/1.0" 
  xmlns:s="http://www.springframework.org/schema/beans"
  xmlns="http://apacheds.org/config/1.0">
...
  <jdbmPartition id="tenant1" cacheSize="100" suffix="dc=tenant1,dc=com"
                          optimizerEnabled="true" syncOnWrite="true">
    <indexedAttributes>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.1" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.2" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.3" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.4" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.5" cacheSize="10"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.6" cacheSize="10"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.7" cacheSize="10"/>
      <jdbmIndex attributeId="dc" cacheSize="100"/>
      <jdbmIndex attributeId="ou" cacheSize="100"/>
      <jdbmIndex attributeId="krb5PrincipalName" cacheSize="100"/>
      <jdbmIndex attributeId="uid" cacheSize="100"/>
      <jdbmIndex attributeId="objectClass" cacheSize="100"/>
    </indexedAttributes>
    <contextEntry>#tenant1ContextEntry</contextEntry>
  </jdbmPartition>
...
  <spring:bean id="tenant1ContextEntry" 
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <spring:property name="targetObject">
      <spring:ref local='directoryService'/>
    </spring:property>
    <spring:property name="targetMethod">
      <spring:value>newEntry</spring:value>
    </spring:property>
    <spring:property name="arguments">
      <spring:list>
        <spring:value xmlns="http://www.springframework.org/schema/beans">
          objectClass: top
          objectClass: domain
          objectClass: extensibleObject
          dc: tenant1
        </spring:value>
        <spring:value>dc=tenant1,dc=com</spring:value>
      </spring:list>
    </spring:property>
  </spring:bean>
...
</spring:beans>

Аналогичным образом следует добавить новый раздел и контекст для области безопасности tenant2. Образец файла server.xml можно найти в примере Web-приложения.

После создания областей безопасности в них можно импортировать данные из файлов LDIF (LDAP Date Interchange Format – формата обмена данными LDAP) с помощью мастера импорта LDIF, имеющегося в модуле расширения Eclipse для Apache Directory Studio (см. раздел Ресурсы). Пример файла LDIF приведен в Листинге 2. Образцы файлов LDIF можно найти также в примере web-приложения.

Листинг2. tenant1_users.ldif
dn: ou=groups,dc=tenant1,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=tenant1,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=tenant1admin,ou=people,dc=tenant1,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: tenant1admin
sn: tenant1admin
uid: tenant1admin
userPassword: tenant1admin

dn: uid=tenant1guest,ou=people,dc=tenant1,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: tenant1guest
sn: tenant1guest
uid: tenant1guest
userPassword: tenant1guest

dn: cn=gst,ou=groups,dc=tenant1,dc=com
objectclass: top
objectclass: groupOfNames
cn: gst
member: uid=tenant1guest,ou=people,dc=tenant1,dc=com

dn: cn=adm,ou=groups,dc=tenant1,dc=com
objectclass: top
objectclass: groupOfNames
cn: adm
member: uid=tenant1admin,ou=people,dc=tenant1,dc=com

Реализация динамической маршрутизации LDAP с помощью Spring Security

В основе динамической маршрутизации LDAP лежит возможность динамического выбора контекста безопасности LDAP во время выполнения на основе ключа просмотра (см. рисунок 2). В многопользовательской среде этому процессу соответствует процедура аутентификации и авторизации на основе исходных данных LDAP, полученных «на лету» из идентификатора пользователя.

Рисунок 2. Многопользовательская динамическая маршрутизация LDAP
Multitenant dynamic LDAP routing

Динамическая маршрутизация LDAP не поставляется в готовом виде вместе со Spring, поэтому мы реализовали свой собственный механизм. Мы почерпнули наши идеи из похожего решения, имеющегося в Spring – AbstractRoutingDataSource, предназначенного для источников данных (см. Ресурсы).

Наша реализация динамической маршрутизации LDAP инкапсулирована в трех основных классах:

  • Класс AbstractRoutingSpringSecurityContextSource, показанный в листинге 3, представляет собой абстрактную реализацию, основанную на классе LdapContextSource из Spring. Он хранит ссылку на коллекцию «реальных» источников контекстов безопасности (см. класс targetSpringSecurityContextSources) и предназначен для маршрутизации вызовов к одному из нескольких целевых источников контекстов безопасности на основе определенного ключа просмотра (см. метод getResolvedContextSource()).
    Листинг 3. AbstractRoutingSpringSecurityContextSource.java
    public abstract class AbstractRoutingSpringSecurityContextSource<T 
        extends Serializable> extends LdapContextSource 
        implements SpringSecurityContextSource, InitializingBean {
    
      private Map<T, DefaultSpringSecurityContextSource>
        targetSpringSecurityContextSources;
    
      /** Определяем текущий ключ просмотра. Обычно это делается для проверки 
          ограниченного нитью контекста. */
      protected abstract T determineCurrentLookupKey();
    
      /** Динамически во время выполнения определяем "настоящий" контекст безопасности
      на основе ключа просмотра. */
      protected DefaultSpringSecurityContextSource getResolvedContextSource() {
        T lookupKey = determineCurrentLookupKey();
        DefaultSpringSecurityContextSource springSecurityContextSource =
          this.targetSpringSecurityContextSources.get(lookupKey);
        if (springSecurityContextSource == null) {
            throw new IllegalStateException(
              "Cannot determine target SpringSecurityContextSource for lookup key [" +
               lookupKey + "]");
        }
        return springSecurityContextSource;
      }
    
      public void setTargetSpringSecurityContextSources(
        Map<T, DefaultSpringSecurityContextSource> targetSpringSecurityContextSources) {
        this.targetSpringSecurityContextSources = targetSpringSecurityContextSources;
      }
    
      public void afterPropertiesSet() throws Exception {
        if (this.targetSpringSecurityContextSources == null) {
          throw new IllegalArgumentException(
            "targetSpringSecurityContextSources is required");
        }
      }
    
      public DirContext getReadWriteContext(String userDn, Object credentials) {
        return this.getResolvedContextSource().getReadWriteContext(userDn, credentials);
      }
    
      @Override
      public DirContext getReadOnlyContext() {
        return this.getResolvedContextSource().getReadOnlyContext();
      }
    
      @Override
      public DirContext getReadWriteContext() {
        return this.getResolvedContextSource().getReadWriteContext();
      }
    
      @Override
      public DistinguishedName getBaseLdapPath() {
        return this.getResolvedContextSource().getBaseLdapPath();
      }
    
      @Override
      public String getBaseLdapPathAsString() {
        return this.getResolvedContextSource().getBaseLdapPathAsString();
      }
    
      @Override
      public Class getContextFactory() {
       return this.getResolvedContextSource().getContextFactory();
      }
    
      @Override
      public Class getDirObjectFactory() {
        return this.getResolvedContextSource().getDirObjectFactory();
      }
    
      @Override
      public boolean isPooled() {
        return this.getResolvedContextSource().isPooled();
      }
    
      @Override
      public AuthenticationSource getAuthenticationSource() {
        return this.getResolvedContextSource().getAuthenticationSource();
      }
    
      @Override
      public boolean isAnonymousReadOnly() {
        return this.getResolvedContextSource().isAnonymousReadOnly();
      }
    
      @Override
      public String[] getUrls() {
        return this.getResolvedContextSource().getUrls();
      }
      
    }
  • Второй класс – это TenantRoutingSpringSecurityContextSource, показанный в листинге 4. Обратите внимание, что он реализует абстрактный метод determineCurrentLookupKey(), определяющий четкое логическое разделение контекстов.
    Листинг 4. TenantRoutingSpringSecurityContextSource.java
    public class TenantRoutingSpringSecurityContextSource<T extends Serializable>
        extends AbstractRoutingSpringSecurityContextSource<String> {
       
      @Override
      protected String determineCurrentLookupKey() {
        String lookupKey = TenantSecurityContextHolder.getTenantID();
        return lookupKey;
      }
    
    }
  • Последняя деталь мозаики – это класс TenantSecurityContextHolder, показанный в листинге 5. Он поддерживает ограниченный нитью контекст, который хранит ссылку на идентификатор пользователя, чтобы класс TenantRoutingSpringSecurityContextSource имел к нему доступ во время выполнения.
    Листинг 5. TenantSecurityContextHolder.java
    public class TenantSecurityContextHolder {
    
      private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
      public static void setTenantID(String tenantID) {
        contextHolder.set(tenantID);
      }
    
      public static String getTenantID() {
        return contextHolder.get();
      }
    
      public static void clearTenantID() {
        contextHolder.remove();
      }
    
    }

Web-интеграция динамической маршрутизации LDAP

Мы создали основу динамической маршрутизации LDAP, но нам еще нужно проделать некоторую работу по ее интеграции в Web-приложение. Когда у нас настроен ограниченный нитью контекст, который хранит ссылку на идентификатор клиента, интеграцию можно сделать огромным количеством способов. Один из подходов – возложить ответственность за эту задачу на специально разработанный сервлет, называемый фильтром.

Работа фильтра безопасности, показанного в листинге 6, основана на предположении, что идентификаторы клиентов передаются в качестве параметров запроса при входе пользователя в систему. Они хранятся в Web-сеансе и потом извлекаются при поступлении аутентифицированных запросов.

Листинг 6. TenantSecurityContextFilter.java
public class TenantSecurityContextFilter implements Filter {

  private static final String
    SPRING_SECURITY_CHECK_MAPPING = "/j_spring_security_check";

  private static final String
    SPRING_SECURITY_LOGOUT_MAPPING = "/j_spring_security_logout";

  private static final String TENANT_HTTP_KEY = "tenant";

  protected final Log logger = LogFactory.getLog(this.getClass());

  private FilterConfig filterConfig;

  public void init(FilterConfig filterConfig) throws ServletException {
    this.filterConfig = filterConfig;
  }

  public void destroy() {
    this.filterConfig = null;
  }

  public void doFilter(ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {
    if (null == filterConfig) {
      return;
    }
    HttpServletRequest httpRequest = (HttpServletRequest) request;

    // очищаем объект, хранящий контекст безопасности пользователя, а если это запрос 
    // выхода из системы, то удаляем атрибут пользователя из сессии
    TenantSecurityContextHolder.clearTenantID();
    if (httpRequest.getRequestURI().endsWith(SPRING_SECURITY_LOGOUT_MAPPING)) {
      httpRequest.getSession().removeAttribute(TENANT_HTTP_KEY);
    }

    // пытаемся получить идентификатор пользователя
    String tenantID = null;
    if (httpRequest.getRequestURI().endsWith(SPRING_SECURITY_CHECK_MAPPING)) {
      tenantID = request.getParameter(TENANT_HTTP_KEY);
      httpRequest.getSession().setAttribute(TENANT_HTTP_KEY, tenantID);
    } else {
      tenantID = (String) httpRequest.getSession().getAttribute(TENANT_HTTP_KEY);
    }

    // если идентификатор найден, задаем Tenant ID в контексте безопасности
    if (null != tenantID) {
      TenantSecurityContextHolder.setTenantID(tenantID);
      if (logger.isInfoEnabled()) logger.info(
        "Tenant context set with Tenant ID: " + tenantID);
    }

    chain.doFilter(request, response);
  }

}

XML конфигурация Spring Security

Одним из главных преимуществ Spring является реализация в нем принципа обращения управления (Inversion of Control или IoC), в которой конфигурация приложения и спецификация зависимостей четко отделена от кода самого приложения (см. Ресурсы). Одним из поводов для недовольства инфраструктурой Spring является то, что ее конфигурационные файлы, которые обычно представлены в формате XML, могут становиться слишком громоздкими. К счастью, XML-конфигурация Spring Security была существенно урезана после появления в Spring 2.0 возможности конфигурирования пространства имен.

Используя XML-конфигурацию Spring Security можно определить большую часть подробностей механизмов аутентификации и авторизации в файле контекста приложения Spring. Эти подробности могут включать в себя конфигурацию провайдера аутентификации LDAP, а также авторизацию уровня URL на основе роли пользователя. (Хотя это и не показано в статье, в Spring Security также возможно детализировать механизм авторизации посредством безопасности уровня методов.)

Конфигурационный XML-файл контекста безопасности приложения, показанный в листинге 7, является лишь примером, но все же дает хорошее представление об основных компонентах XML, необходимых для декларативной конфигурации безопасности в Spring Security. Также обратите внимание, как наш специфичный многопользовательский источник контекста безопасности маршрутизации LDAP встраивается в конфигурацию для обеспечения интеграции многопользовательских возможностей.

Листинг 7. application-context-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:s="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">
...
 <!-- HTTP security configuration -->
 <s:http>        
        <s:intercept-url pattern="/poc/login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <s:intercept-url pattern="/poc/admin/**" access="ROLE_ADM" />
        <s:intercept-url pattern="/poc/**" access="ROLE_GST, ROLE_ADM" />
        
        <s:form-login login-page="/poc/login" default-target-url="/poc/home"/>
        <s:anonymous />
        <s:logout />
 </s:http>
    
 <!-- LDAP Authentication Provider -->
 <s:ldap-authentication-provider server-ref="contextSource"
        group-search-filter="member={0}" group-search-base="ou=groups"
        user-search-base="ou=people" user-search-filter="uid={0}"/>
    
 <!-- Custom Multitenant Routing Spring Security Context Source -->
 <bean id="contextSource" class=
 "poc.saas.security.core.multitenancy.context.TenantRoutingSpringSecurityContextSource">
<property name="targetSpringSecurityContextSources">
      <map>
         <entry key="Tenant1" value-ref="tenant1ContextSource"/>
         <entry key="Tenant2" value-ref="tenant2ContextSource"/>
      </map>
        </property>
 </bean>
    
 <!-- This bean points at the at the Tenant1 LDAP Server  -->
 <bean id="tenant1ContextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
        <constructor-arg value="ldap://localhost:10389/dc=tenant1,dc=com"/>
        <property name="userDn"><value>uid=admin,ou=system</value></property>
      <property name="password"><value>secret</value></property>
 </bean>
    
 <!-- This bean points at the at the Tenant2 LDAP Server  -->
 <bean id="tenant2ContextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
        <constructor-arg value="ldap://localhost:10389/dc=tenant2,dc=com"/>
        <property name="userDn"><value>uid=admin,ou=system</value></property>
      <property name="password"><value>secret</value></property>
 </bean>
...
</beans>

Также для работы Spring Security в стандартном web-приложении на Java необходимо в файл web.xml, также называемый дескриптором развертывания, добавить контекст безопасности и фильтры безопасности, как показано в листинге 8:

Листинг 8. web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.4"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
...
 <!-- Spring context configuration files -->
 <context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
...
classpath*:application-context-security.xml
</param-value>
 </context-param>
...
 <!-- Tenant Security Context Filter -->
 <filter>
<filter-name>Tenant Security Context Filter</filter-name>
<filter-class>
  poc.saas.security.core.multitenancy.web.filter.TenantSecurityContextFilter
</filter-class>
 </filter>
...
 <!-- Spring Security Filter -->
 <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 </filter>
...
 <!-- Tenant Security Context Filter Mapping -->
 <filter-mapping>
<filter-name>Tenant Security Context Filter</filter-name>
<url-pattern>/*</url-pattern>
 </filter-mapping>
...
 <!-- Spring Security Filter Mapping -->
 <filter-mapping>
      <filter-name>springSecurityFilterChain</filter-name>
      <url-pattern>/*</url-pattern>
 </filter-mapping>
...
 <!-- Spring MVC web context listener -->
 <listener>
<listener-class>
  org.springframework.web.context.ContextLoaderListener
</listener-class>
 </listener>
...
</web-app>

Проверка многопользовательской аутентификации

В листинге 9 показан Junit-тест, реализация которого упрощается благодаря поддержке инфраструктурой Spring интеграционного тестирования. Этот интеграционный тест позволяет нам проверить правильность работы механизма многопользовательской аутентификации, не развертывая при этом наш код на сервере приложений. Тем не менее для теста необходим работающий сервер Apache Directory, предоставляющий сервис аутентификации. Во время работы тест пытается аутентифицировать различных пользователей для работы с двумя областями безопасности. Для аутентификации части пользователей предоставляются правильные данные, а для других – неверные. Детальный сценарий теста показан после листинга.

Листинг 9. MultiTenantAuthenticationTest.java
public class MultiTenantAuthenticationTest
    extends AbstractDependencyInjectionSpringContextTests {

  private static final String
    APPLICATION_CONTEXT_SECURITY = "classpath:application-context-security.xml";

  private FilterChain passThroughFilterChain;

  private Filter formLoginFilter;

  private Filter tenantSecurityContextFilter;

  @Override
  protected String[] getConfigLocations() {
    return new String[] {APPLICATION_CONTEXT_SECURITY};
  }

  @Override
  protected void onSetUp() throws Exception {
    // Настраиваем объекты-заглушки
    MockServletContext servletContext = new MockServletContext("");
    servletContext.addInitParameter(ContextLoader.CONFIG_LOCATION_PARAM,
                                APPLICATION_CONTEXT_SECURITY);
    ServletContextListener contextListener = new ContextLoaderListener();
    ServletContextEvent event = new ServletContextEvent(servletContext);
    contextListener.contextInitialized(event);
    MockFilterConfig mockConfig = new MockFilterConfig(servletContext);        
        
    // Настраиваем фильтр контекста безопасности
    tenantSecurityContextFilter = new TenantSecurityContextFilter();
    tenantSecurityContextFilter.init(mockConfig);

    // Настраиваем фильтр формы входа Spring Security
    formLoginFilter = 
      (Filter) this.getApplicationContext().getBean("_formLoginFilter");
    formLoginFilter.init(mockConfig);

    // Настраиваем цепочку фильтров
    passThroughFilterChain =
      new PassThroughFilterChain(formLoginFilter, new MockFilterChain());
  }

  public void testMultiTenantAuthenticationWorksAsExpected() throws Exception {
    this.assertGivenUserCredentialsAreValid("Tenant1", "tenant1admin", "tenant1admin");
    this.assertGivenUserCredentialsAreValid("Tenant2", "tenant2admin", "tenant2admin");
    this.assertGivenUserCredentialsAreInvalid("Tenant1", "invalidUser", "wrongPassword");
    this.assertGivenUserCredentialsAreInvalid("Tenant1", "tenant1admin", "wrongPassword");
    this.assertGivenUserCredentialsAreInvalid("Tenant2", "tenant1admin", "tenant1admin");
    this.assertGivenUserCredentialsAreValid("Tenant2", "tenant2guest", "tenant2guest");
  }

  private void assertGivenUserCredentialsAreValid
    (String tenantId, String username, String password) throws Exception {
    // Аутентифицируем пользователя с правильными данными
    Object[] result = this.performUserAuthentication(tenantId, username, password);

    // Убеждаемся, что пользователю было отказано в аутентификации и
    // он был перенаправлен на страницу регистрации

    assertNotNull(SecurityContextHolder.getContext().getAuthentication());
    assertEquals(username,
      SecurityContextHolder.getContext().getAuthentication().getName());                
    assertEquals("/poc/home", result[0]);
        
    System.out.println("Authentication success for user " + username + " [" +
      SecurityContextHolder.getContext().getAuthentication().getPrincipal().getClass()
      + "]");
  }

  private void assertGivenUserCredentialsAreInvalid(
    String tenantId, String username, String password) throws Exception {
    // Пытаемся аутентифицировать пользователя с неправильными данными
    Object[] result = this.performUserAuthentication(tenantId, username, password);

    // Убеждаемся, что прользователю было отказано в аутентификации и
    // он был перенаправлин на страницу регистрации
    assertNull(SecurityContextHolder.getContext().getAuthentication());
    assertEquals("/poc/login", result[0]);

    System.out.println("Authentication failed for user "
      + username + " [" + result[1].getClass() + "]");
  }

  private Object[] performUserAuthentication(
    String tenantId, String username, String password) throws Exception {
    // Создаем запрос-заглушку
    MockHttpServletRequest request =
      new MockHttpServletRequest("POST", "/poc/j_spring_security_check");
    request.setParameter("tenant", tenantId);
    request.setParameter("j_username", username);
    request.setParameter("j_password", password);
        
    // Запускаем фильтр безопасности и возвращаем URL ответа
    MockHttpServletResponse response = new MockHttpServletResponse();
    tenantSecurityContextFilter.doFilter(request, response, passThroughFilterChain);
        
    Object[] result = 
      {response.getRedirectedUrl(), 
       request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION")};
    return result;
  }

}

Тест имитирует следующую последовательность действий:

  1. В методе onSetUp() настраивается набор объектов-заглушек и наш фильтр TenantSecurityContextFilter.
  2. Метод testMultiTenantAuthenticationWorksAsExpected(), вызываемый при каждом прогоне теста, проверяет, что пользователи с корректными данными получают доступ в систему, а пользователям с некорректными данными доступ запрещается. Для этого используются методы assertGivenUserCredentialsAreValid() и assertGivenUserCredentialsAreInvalid() соответственно.
  3. Метод assertGivenUserCredentialsAreValid() пытается аутентифицировать заданного пользователя и убеждается, что пользователь, как и ожидалось, был аутентифицирован.
  4. Метод assertGivenUserCredentialsAreInvalid() пытается аутентифицировать пользователя, не имеющего полномочий на вход в систему, и убеждается, что пользователю запрещается доступ и что его перенаправляют на страницу входа в систему.
  5. Метод performUserAuthentication(), вызываемый из методов assertGivenUserCredentialsAreValid() и assertGivenUserCredentialsAreInvalid(), выполняет сам процесс аутентификации:
    1. Генерируется запрос-заглушка аутентификации с заданными идентификатором клиента, именем пользователя и паролем в качестве параметров. Этот запрос передается созданному ранее фильтру TenantSecurityContextFilter.
    2. Наш фильтр безопасности перехватывает запрос аутентификации, извлекает из него идентификатор клиента и запоминает его в виде ограниченного нитью контекста TenantSecurityContextHolder, после чего запрос передается в цепочку passThroughFilterChain. Можно заметить, что внутри метода onSetUp(), цепочка passThroughFilterChain реализована как обертка вокруг стандартного фильтра регистрации для формы из Spring Security. Этот фильтр создается провайдером аутентификации LDAP, определенным в файле application-context-security.xml, и настроен на использование TenantRoutingSpringSecurityContextSource в качестве источника контекста LDAP. Поэтому когда Spring Security пытается определить источник контекста, который следует использовать при аутентификации, метод TenantRoutingSpringSecurityContextSource (унаследованный от AbstractRoutingSpringSecurityContextSource) динамически возвращает правильный источник LDAP в соответствии с идентификатором пользователя, хранящимся в TenantSecurityContextHolder.

Зеленая полоска, сигнализирующая об успешном прохождении теста, не очень информативна, поэтому в листинге 10 приведен консольный вывод, сгенерированный в ходе выполнения Junit-теста MultiTenantAuthenticationTest в среде Eclipse:

Листинг 10. Работа теста
Authentication success for user tenant1admin
  [org.springframework.security.userdetails.ldap.LdapUserDetailsImpl@676437]
  
Authentication success for user tenant2admin
  [org.springframework.security.userdetails.ldap.LdapUserDetailsImpl@992bae]
  
Authentication failed for user invalidUser
  [org.springframework.security.userdetails.UsernameNotFoundException]
  
Authentication failed for user tenant1admin
  [org.springframework.security.BadCredentialsException]
  
Authentication failed for user tenant1admin
  [org.springframework.security.userdetails.UsernameNotFoundException]
  
Authentication success for user tenant2guest
  [org.springframework.security.userdetails.ldap.LdapUserDetailsImpl@1f64158]

Пример приложения

В этой статье мы приведем пример Web-приложения, демонстрирующий все концепции, с которыми вы познакомились к этому моменту (см. раздел Загрузки). В качестве бонуса оно включает также дополнительную функциональность по управлению пользователями и паролями, реализованную с помощью Spring LDAP. Spring LDAP – это инфраструктура, которая была разработана, чтобы избавить разработчиков Java от типовой настройки инфраструктуры при работе с LDAP в приложениях Java, основанных на Spring (см. раздел Ресурсы).

Хотя это Web-приложение проектировалось с учетом проблем безопасности, вам все же следует знать о множестве потенциальных уязвимостей, таких как межсайтовые сценарии, подделка запросов и перехват пользовательских сессий, которые следует принимать во внимание при разработке коммерческих Web-приложений. Открытый проект безопасности Web–приложений (Open Web Application Security Project или OWASP), с которым вы можете ознакомиться по ссылке в разделе Ресурсы, поддерживает множество справочных материалов, связанных с этими разновидностями рисков безопасности.

Приведенный пример Web-приложения основан на Servlet API 2.5, JavaServer Pages 2.1, Spring 2.5, Spring Web MVC 2.5 и Spring Security 2.0.1. С помощью Eclipse и Apache Maven 2 приложение было успешно развернуто и протестировано на следующих серверах:

  • Apache Tomcat 6, запущенном под Java SE 6
  • Apache Geronimo 2.1 со встроенным Tomcat 6 или Jetty 6, запущенном под Java SE 5

Загрузите и начните изучать это приложение. В пакете загрузки содержится файл readme.html, шаг за шагом описывающий процедуру запуска приложения. Также вы можете посмотреть Flash-видео, демонстрирующее работу приложения.


Заключение

В этой статье было изучено несколько важных аспектов безопасности приложений SaaS с несколькими арендаторами. Мы показали, как настроить совмещенный реестр пользователей на сервере Apache Directory, а также средствами инфраструктуры Spring Security реализовать аутентификацию и авторизацию, используя многопользовательский источник LDAP. Также вы узнали, как в многопользовательской экосистеме интегрировать Spring Security с сервером Apache Directory с помощью динамической маршрутизации LDAP.

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

Благодарности

Мы благодарим Дэвида Дженкса и Пола Брауни за высказанные замечания и предложения, а также Кевана Миллера и команду Geronimo and WebSphere Community Edition за помощь в работе над статьей.


Загрузка

ОписаниеИмяРазмер
SaaS Security PoC - Example Applicationj-saas.zip47KБ

Ресурсы

Научиться

Получить продукты и технологии

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=757558
ArticleTitle=Обеспечение безопасности в многопользовательском приложении SaaS
publish-date=09142011