Статьи

Создание делегирующего модуля входа в систему (для JBoss EAP 6.1)

[Если вы хотите видеть только код, просто прокрутите вниз]

мотивация

В RHQ нам был нужен домен безопасности, который можно использовать для защиты REST-api и его веб-приложения с помощью управляемой контейнером безопасности. В прошлом я только что использовал классический DatabaseServerLoginModule для аутентификации в базе данных. Теперь RHQ также позволяет иметь пользователей в каталогах LDAP, которые не были рассмотрены вышеупомянутым модулем. У меня было два варианта для начала:

  • Скопируйте модули входа LDAP в домен безопасности для REST
  • Используйте домен безопасности для REST-API, который уже используется для пользовательского интерфейса и интерфейса командной строки

Последний вариант, конечно, был выгоден для предотвращения дублирования кода, поэтому я пошел по этому пути. И не удалось.

Я потерпел неудачу, потому что RHQ был при сбросе запуска и воссоздании домена безопасности, и сервер обнаружил это и пожаловался на то, что домен безопасности, на который ссылается rhq-rest.war, внезапно исчез.

Итак, следующая попытка: не создавайте заново домен при запуске, а только добавляйте / удаляйте модули ldap-login (я говорю о модулях, потому что у нас фактически есть два, которые нам нужны).

Это также не сработало, как ожидалось:

  • Базовый AS иногда переходил в необходимый режим перезагрузки и не применял изменения
  • Когда модули ldap были удалены, участники из них все еще кэшировались
  • Очистка кеша не сработала и сервер перешел в режим перезагрузки

Итак, сейчас я реализовал модуль входа для домена rest-security, который просто делегирует другому домен для аутентификации, а затем добавляет роли в случае успеха.

Таким образом, rhq-rest.war имеет фиксированную ссылку на этот домен rest-security, а другой домен безопасности может быть обработан, как и раньше.

Реализация

Давайте начнем с фрагмента файла standalone.xml, описывающего домен безопасности и параметризации модуля.

1
2
3
4
5
6
7
8
<security-domain name="RHQRESTSecurityDomain" cache-type="default">
   <authentication>
     <login-module code="org.rhq.enterprise.server.core.jaas.DelegatingLoginModule" flag="sufficient">
       <module-option name="delegateTo" value="RHQUserSecurityDomain"/>
       <module-option name="roles" value="rest-user"/>
     </login-module>
   </authentication>
 </security-domain>

Таким образом, это определение устанавливает домен безопасности RHQRESTSecurityDomain, который использует модуль DelegatingLoginModule, который я сейчас опишу. Передано два параметра:

  • DelegateTo: имя другого домена для аутентификации пользователя
  • Роли: список модулей, разделенных запятыми, которые нужно добавить к принципалу (и которые необходимы в разделе ограничений безопасности файла web.xml).

Для кода я не показываю полный список; Вы можете найти это в Git .

Чтобы упростить нашу жизнь, мы не реализуем все функции самостоятельно, а расширяем уже существующий UsernamePasswordLoginModule и переопределяем только определенные методы.

1
public class DelegatingLoginModule extends UsernamePasswordLoginModule {

Сначала мы инициализируем модуль с переданными параметрами и создаем новый LoginContext с доменом, которому мы делегируем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Override
    public void initialize(Subject subject, CallbackHandler callbackHandler,
        Map<String, ?> sharedState,
        Map<String, ?> options)
    {
        super.initialize(subject, callbackHandler, sharedState, options);
 
        /* This is the login context (=security domain) we want to delegate to */
        String delegateTo = (String) options.get("delegateTo");
 
        /* Now create the context for later use */
        try {
            loginContext = new LoginContext(delegateTo, new DelegateCallbackHandler());
        } catch (LoginException e) {
            log.warn("Initialize failed : " + e.getMessage());
        }

Интересная часть — это метод login() где мы получаем имя пользователя / пароль и сохраняем его для дальнейшего использования, затем мы пытаемся войти в домен делегата, и если это удалось, мы сообщаем super, что у нас получился успех, чтобы он мог творить чудеса ,

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Override
    public boolean login() throws LoginException {
        try {
            // Get the username / password the user entred and save if for later use
            usernamePassword = super.getUsernameAndPassword();
             
            // Try to log in via the delegate
            loginContext.login();
 
            // login was success, so we can continue
            identity = createIdentity(usernamePassword[0]);
            useFirstPass=true;
 
            // This next flag is important. Without it the principal will not be
            // propagated
            loginOk = true;

здесь loginOk флаг loginOk чтобы суперкласс вызывал LoginModule.commit() и выбирал принципала вместе с ролями.

Если для этого параметра не задано значение true, будет выполнен успешный login() но принципал не подключен.

01
02
03
04
05
06
07
08
09
10
11
12
13
if (debugEnabled) {
                log.debug("Login ok for " + usernamePassword[0]);
            }
 
            return true;
        } catch (Exception e) {
            if (debugEnabled) {
                LOG.debug("Login failed for : " + usernamePassword[0] + ": " + e.getMessage());
            }
            loginOk = false;
            return false;
        }
    }

После успеха super вызовет следующие два метода для получения принципала и его ролей:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Override
    protected Principal getIdentity() {
        return identity;
    }
 
 
    @Override
    protected Group[] getRoleSets() throws LoginException {
 
        SimpleGroup roles = new SimpleGroup("Roles");
 
        for (String role : rolesList ) {
            roles.addMember( new SimplePrincipal(role));
        }
        Group[] roleSets = { roles };
        return roleSets;
    }

И теперь последняя часть — это обработчик обратного вызова, который другой домен, которому мы делегируем, будет использовать для получения учетных данных от нас. Это классический обработчик обратного вызова для входа в систему JAAS. Одна вещь, которая сначала полностью смутила меня, была то, что этот обработчик вызывался несколько раз во время входа в систему, и я думал, что это глючит. Но на самом деле количество его вызовов соответствует количеству модулей входа, настроенных в домене RHQUserSecurityDomain .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private class DelegateCallbackHandler implements CallbackHandler {
        @Override
        public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
 
            for (Callback cb : callbacks) {
                if (cb instanceof NameCallback) {
                    NameCallback nc = (NameCallback) cb;
                    nc.setName(usernamePassword[0]);
                }
                else if (cb instanceof PasswordCallback) {
                    PasswordCallback pc = (PasswordCallback) cb;
                    pc.setPassword(usernamePassword[1].toCharArray());
                }
                else {
                    throw new UnsupportedCallbackException(cb,"Callback " + cb + " not supported");
                }
            }
        }
    }

Опять же, полный код доступен в git-репозитории RHQ .

Отладка (в EAP 6.1 alpha или новее)

Если вы пишете такой модуль входа в систему, и он не работает, вы хотите отладить его. Начал с обычных средств, чтобы узнать, что мой метод login() работал как положено, но вход в систему просто не удался. Я добавил операторы печати и т. Д., Чтобы узнать, что метод getRoleSets() никогда не вызывался. Но все равно все выглядело нормально. Я немного погуглил и нашел эту хорошую вики-страницу . Можно указать веб-приложению вести аудит

1
2
3
<jboss-web>
    <context-root>rest</context-root>
    <security-domain>RHQRESTSecurityDomain</security-domain><disable-audit>false</disable-audit>

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

1
2
16:33:33,918 TRACE [org.jboss.security.audit] (http-/0.0.0.0:7080-1) [Failure]Source=org.jboss.as.web.security.JBossWebRealm;
principal=null;request=[/rest:….

Таким образом, стало очевидно, что модуль входа не установил принципал. Просмотр кода в суперклассах привел меня к упомянутому выше флагу loginOk .

Теперь, когда все правильно настроено, журнал авторизации выглядит так

1
22:48:16,889 TRACE [org.jboss.security.audit] (http-/0.0.0.0:7080-1) [Success]Source=org.jboss.as.web.security.JBossWebRealm;Step=hasRole; principal=GenericPrincipal[rhqadmin(rest-user,)]; request=[/rest:cookies=null:headers=authorization=user-agent=curl/7.29.0,host=localhost:7080,accept=*/*,][parameters=][attributes=];

Итак, здесь вы видите, что основной rhqadmin вошел в систему и получил роль rest-user , которая соответствует той, которая соответствует элементу security-constraint в web.xml.

Дальнейший просмотр

Я представил это как видеовстречу в эфире . К сожалению, G + время от времени отключал меня, когда я печатал во время объяснения.

После того, как видео было сделано, я получил еще несколько вопросов, которые в конце заставили меня переосмыслить этап запуска для случая, когда у пользователя установлена ​​предыдущая версия RHQ с включенным LDAP. В этом случае установщик по-прежнему будет устанавливать исходный RHQUserSecurityDomain только для DB, а затем в компоненте запуска проверяем, включен ли а) LDAP в настройках системы и б) присутствуют ли модули входа в систему. Если а) совпадает, а их нет, мы их устанавливаем.

Эта запись Bugzilla также содержит больше информации обо всей этой истории.