Inject CDI управляется bean в пользовательском Shiro AuthorizingRealm

В приложении, которое я создаю, мы используем прямые Java 6 EE и JBoss (без Spring и т.д.), с JPA/Hibernate, JSF, CDI и EJB.

Я не нашел много хороших общих решений безопасности (рекомендации приветствуются), но лучшим выбором я нашел Apache Shiro.

Однако это, кажется, имеет ряд недостатков. Некоторые из которых вы можете прочитать в сайт Balus C:

http://balusc.blogspot.com/2013/01/apache-shiro-is-it-ready-for-java-ee-6.html

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

В принципе, у меня есть хорошо написанная пользовательская платформа JPDA, которая предоставляет все необходимое для аутентификации. Моя база данных аккуратно настроена в persistence.xml и mydatabase-ds.xml(для JBoss).

Кажется глупым дублировать всю эту конфигурационную информацию во второй раз и добавлять запросы пользовательских таблиц в shiro.ini. Вот почему я решил написать собственное царство вместо использования JdbcRealm.

Моя первая попытка в этом заключалась в подклассе AuthorizingRealm... что-то вроде:

@Stateless
public MyAppRealm extends AuthorizingRealm {
    @Inject private UserAccess userAccess;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
        AuthenticationToken token) throws AuthenticationException {

        UsernamePasswordToken userPassToken = (UsernamePasswordToken) token;

        User user = userAccess.getUserByEmail(userPassToken.getUsername());
        if (user == null) {
            return null;
        }

        AuthenticationInfo info = new SimpleAuthenticationInfo();
        // set data in AuthenticationInfo based on data from the user object

        return info;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // TODO
        return null;
    }
}

Таким образом, это не очень хорошо, потому что MyAppRealm не может быть проксированным, потому что в родительском классе есть окончательный метод init() в иерархии классов.

Моя вторая попытка состояла в том, чтобы MyAppRealm реализовал все необходимые интерфейсы и просто делегировал их экземпляру AuthorizingRealm. Мне это не понравилось, но я бы тоже попробовал.

Это добавляет меня дальше, webapp запускается, но все равно отстает. Причина в файле конфигурации: shiro.ini, я указываю класс для своего царства:

myAppRealm = com.myapp.MyAppRealm

Это в значительной степени говорит мне, что Shiro будет нести ответственность за создание экземпляра MyAppRealm. Поэтому он не будет управляться CDI и, следовательно, не будет инъецироваться, что я и вижу.

Я видел этот SO ответ, но я не вижу, как он может работать, потому что снова подкласс AuthorizationRealm наследует окончательный метод init() это означает, что подкласс не может быть проксирован.

Любые мысли о том, как я могу обойти это?

Ответ 1

Это классическая проблема: у вас есть две разные структуры, которые хотят управлять жизненными циклами объектов, и вам нужно заставить их взаимодействовать, но оба настаивают на том, чтобы иметь полный контроль (мой мысленный образ - это что-то вроде Godzilla и Gamera, сражающихся в центре Токио). Вы, возможно, не сразу думаете о Сиро как о конкуренте CDI, но поскольку он создает экземпляры его объектов, он по существу содержит крошечную, рудиментарную схему внедрения зависимостей (возможно, это версия DI Десятое правило Greenspun). Я столкнулся с аналогичной проблемой, создающей веб-фреймворки, которые создают и вводят примеры их поддержки beans, взаимодействуют с CDI.

Подход к решению этого вопроса - создать явный мост между двумя структурами. Если вам действительно повезло, инфраструктура, отличная от CDI, будет иметь крючки, позволяющие настраивать создание объектов, в которые вы можете подключить что-то, что использует CDI (например, в веб-фреймворке Stripes вы можете написать ActionResolver, который использует CDI).

Если нет, тогда мост должен иметь форму прокси. Внутри этого прокси-сервера вы можете выполнить явный поиск CDI. Вы можете загрузиться в CDI, получив BeanManager, который позволяет вам настроить контекст, а затем создать в нем beans. Что-то вроде этого:

BeanManager beanManager = (BeanManager) new InitialContext().lookup("java:comp/BeanManager");
Bean<UserDAO> userDAObean = (Bean<UserDAO>) beanManager.resolve(beanManager.getBeans(UserDAO.class));
CreationalContext<?> creationalContext = beanManager.createCreationalContext(null);
UserDAO userDAO = userDAObean.create(creationalContext);

userDAO - это вложенный, управляемый CDI bean, связанный с контекстом, который вы сейчас используете как creationalContext.

Когда вы закончите работу с bean (это зависит от вас, если вы выполните этот поиск один раз для каждого запроса или один раз на всю жизнь приложения), отпустите bean с помощью

creationalContext.release();

Ответ 2

Вы можете сделать это, инициализируя свое царство как часть жизненного цикла запуска приложения, а затем Shiro извлекает его через поиск имени JNDI.

Создайте настройку bean с помощью @Singleton и @Startup, чтобы принудительно создать ее как можно раньше в жизненном цикле приложения. В этом классе вы создадите новый экземпляр класса MyAppRealm и предоставите введенную ссылку UserAccess в качестве параметра конструкции. Это означает, что вам придется обновить свой класс "MyAppRealm", чтобы принять этот новый параметр конструктора.

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.naming.InitialContext;
import javax.naming.NamingException;

@Singleton
@Startup
public class ShiroStartup {

  private final static String CLASSNAME = ShiroStartup.class.getSimpleName();
  private final static Logger LOG = Logger.getLogger( CLASSNAME );

  public final static String JNDI_REALM_NAME = "realms/myRealm";

  // Can also be EJB...   
  @Inject private UserAccess userAccess;

  @PostConstruct
  public void setup() {
    final UserAccess service = getService();
    final Realm realm = new MyAppRealm( service );

    try {
      // Make the realm available to Shiro.
      bind(JNDI_REALM_NAME, realm );
    }
    catch( NamingException ex ) {
      LOG.log(Level.SEVERE, "Could not bind realm: " + JNDI_REALM_NAME, ex );
    }
  }

  @PreDestroy
  public void destroy() {
    try {
      unbind(JNDI_REALM_NAME );
    }
    catch( NamingException ex ) {
      LOG.log(Level.SEVERE, "Could not unbind realm: " + JNDI_REALM_NAME, ex );
    }
  }

  /**
   * Binds a JNDI name to an object.
   *
   * @param jndi The JNDI name.
   * @param object The object to bind to the JNDI name.
   */
  private static void bind( final String jndi, final Object object )
    throws NamingException {
    final InitialContext initialContext = createInitialContext();

    initialContext.bind( jndi, object );
  }

  private static void unbind( final String name ) throws NamingException {
    final InitialContext initialContext = createInitialContext();

    initialContext.unbind( name );
  }

  private static InitialContext createInitialContext() throws NamingException {
    return new InitialContext();
  }

  private UserAccess getService() {
    return this.userAccess;
  }
}

Обновить shiro.ini следующим образом:

realmFactory = org.apache.shiro.realm.jndi.JndiRealmFactory
realmFactory.jndiNames = realms/myRealm

Этот подход предоставит вам доступ ко всем вашим управляемым CDI beans без необходимости использования внутренней работы CDI. Причина этого заключается в том, что "shiro.ini" не загружается до тех пор, пока не появится веб-уровень, который после инициализации фреймворков CDI и EJB.