[Если вы хотите видеть только код, просто прокрутите вниз]
мотивация
В 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 также содержит больше информации обо всей этой истории.