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