Inject CDI managed bean in custom Shiro AuthorizingRealm

时光总嘲笑我的痴心妄想 提交于 2019-12-28 13:23:44

问题


In an app I'm building we're using straight Java 6 EE and JBoss (no Spring, etc), with JPA/Hibernate, JSF, CDI and EJBs.

I haven't found many good general security solutions (recommendations are welcome), but the best bet I found is Apache Shiro.

However this seems to have a number of shortcomings. Some of which you can read about at Balus C's site:

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

But I've stumbled on another big problem which is already mentioned here regarding dependency injection and proxying.

Basically I have a nicely written JPA-based UserDAO that provides everything necessary for authentication. My database is neatly configured in persistence.xml and mydatabase-ds.xml (for JBoss).

It seems silly to duplicate all this config info a second time and add user tables queries into shiro.ini. So this is why I have opted to write my own Realm instead of using JdbcRealm.

My first attempt at this was to subclass AuthorizingRealm...something like:

@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;
    }
}

So this fails pretty bad, because MyAppRealm cannot be proxied because there is a final init() method in a parent class up the class hierarchy.

My second attempt was to have MyAppRealm implement all the needed interfaces and just delegate them to instance of AuthorizingRealm. I didn't like this, but might as well give it a try.

This gets me further, the webapp starts up, but still falls short. The reason is in the config file, shiro.ini, I specify the class for my realm:

myAppRealm = com.myapp.MyAppRealm

This pretty much tells me that Shiro will be responsible for creating the MyAppRealm instance. Therefore it will not be CDI managed and thus not injected, which is exactly what I'm seeing.

I've seen this SO answer, but I don't see how it could possibly work because again a subclass of AuthorizingRealm will inherit a final init() method meaning the subclass cannot be proxied.

Any thoughts on how I can get around this?


回答1:


You can do this by initializing your realm as a part of the start-up life cycle of the application and then have Shiro retrieve it via JNDI name lookup.

Create a setup bean with @Singleton and @Startup to force its creation as early as possible in the application life cycle. In this class you'll be instantiating a new instance of your "MyAppRealm" class and providing an injected UserAccess reference as a construction parameter. Which means you'll have to update your "MyAppRealm" class to take this new constructor parameter.

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;
  }
}

Update shiro.ini as follows:

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

This approach will provide you access to all of your CDI managed beans without having to leverage the inner workings of CDI. The reason this works is because the 'shiro.ini' isn't loaded until the web layer comes up which is after initialization of the CDI and EJB frameworks.




回答2:


This is a classic problem: you have two different frameworks which both want to manage object lifecycles, and you need to make them interact, but both insist on having complete control (my mental image of this is something like Godzilla and Gamera battling in downtown Tokyo). You might not immediately think of Shiro as being a competitor to CDI, but because it creates instances of its objects, it essentially contains a tiny, rudimentary dependency injection framework (perhaps this is a DI version of Greenspun's tenth rule). I have encountered a similar problem making web frameworks, which create and inject instances of their backing beans, interact with CDI.

An approach to solving this is to create an explicit bridge between the two frameworks. If you're really lucky, the non-CDI framework will have hooks to let you customise object creation, into which you can plug something that uses CDI (eg in the Stripes web framwork, you can write an ActionResolver which uses CDI).

If not, then the bridge must take the form of a proxy. Within that proxy, you can do an explicit CDI lookup. You can bootstrap into CDI by getting hold of the BeanManager, which lets you set up a context and then create beans in it. Something like this:

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);

The userDAO is an injected, CDI-managed bean bound to the context you now hold as creationalContext.

When you're finished with the bean (it's up to you if you do this lookup once per request or once per application lifetime), release the bean with:

creationalContext.release();


来源:https://stackoverflow.com/questions/18507629/inject-cdi-managed-bean-in-custom-shiro-authorizingrealm

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!