问题 在自定义Shiro AuthorizingRealm中注入CDI托管bean


在我正在构建的应用程序中,我们使用Java 6 EE和JBoss(没有Spring等),使用JPA / Hibernate,JSF,CDI和EJB。

我没有找到很多好的通用安全解决方案(欢迎推荐),但我发现最好的选择是Apache Shiro。

然而,这似乎有许多缺点。其中一些你可以阅读 Balus C's 现场:

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

但我偶然发现了已经提到的另一个大问题 这里 关于依赖注入和代理。

基本上我有一个写得很好的基于JPA的UserDAO,它提供了身份验证所需的一切。我的数据库在persistence.xml和mydatabase-ds.xml(对于JBoss)中整齐地配置。

再次复制所有这些配置信息并将用户表查询添加到shiro.ini中似乎很愚蠢。所以这就是我选择编写自己的Realm而不是使用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管理,因此不会被注入,这正是我所看到的。

我已经看过这个了 回答但是我不知道它是如何工作的,因为AuthorizingRealm的子类将继承最终的init()方法,这意味着子类不能被代理。

有关如何解决这个问题的任何想法?


3188
2017-08-29 09:51


起源



答案:


这是一个经典问题:你有两个不同的框架,它们都想要管理对象生命周期,你需要让它们互动,但都坚持完全控制(我的心理形象就像哥斯拉和Gamera在东京市中心作战)。您可能不会立即认为Shiro是CDI的竞争对手,但因为它创建了对象的实例,它实际上包含一个微小的,基本的依赖注入框架(也许这是一个DI版本的 格林斯普的第十条规则)。我遇到了类似的问题,制作Web框架,创建和注入其支持bean的实例,与CDI交互。

解决此问题的方法是在两个框架之间创建一个明确的桥梁。如果你真的很幸运,非CDI框架将有钩子让你自定义对象创建,你可以插入使用CDI的东西(例如在Stripes web框架中,你可以写一个 ActionResolver它使用CDI)。

如果没有,那么桥必须采用代理的形式。在该代理中,您可以执行显式CDI查找。你可以通过抓住它来引导到CDI BeanManager,它允许您设置上下文,然后在其中创建bean。像这样的东西:

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

8
2017-08-29 10:36



感谢您的答复。这是一个非常好的理论答案,在这方面击中了头部。我想我已经使用beanmanager成功构建了一个“桥梁”。它不是很漂亮,但我希望随着时间的推移不断发展和完善它。 - lostdorje


答案:


这是一个经典问题:你有两个不同的框架,它们都想要管理对象生命周期,你需要让它们互动,但都坚持完全控制(我的心理形象就像哥斯拉和Gamera在东京市中心作战)。您可能不会立即认为Shiro是CDI的竞争对手,但因为它创建了对象的实例,它实际上包含一个微小的,基本的依赖注入框架(也许这是一个DI版本的 格林斯普的第十条规则)。我遇到了类似的问题,制作Web框架,创建和注入其支持bean的实例,与CDI交互。

解决此问题的方法是在两个框架之间创建一个明确的桥梁。如果你真的很幸运,非CDI框架将有钩子让你自定义对象创建,你可以插入使用CDI的东西(例如在Stripes web框架中,你可以写一个 ActionResolver它使用CDI)。

如果没有,那么桥必须采用代理的形式。在该代理中,您可以执行显式CDI查找。你可以通过抓住它来引导到CDI BeanManager,它允许您设置上下文,然后在其中创建bean。像这样的东西:

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

8
2017-08-29 10:36



感谢您的答复。这是一个非常好的理论答案,在这方面击中了头部。我想我已经使用beanmanager成功构建了一个“桥梁”。它不是很漂亮,但我希望随着时间的推移不断发展和完善它。 - lostdorje


您可以通过将您的领域初始化为应用程序的启动生命周期的一部分,然后让Shiro通过JNDI名称查找来检索它。

使用@Singleton和创建一个安装bean @启动 在应用程序生命周期中尽早强制创建它。在这个类中,您将实例化“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托管bean,而无需利用CDI的内部工作。这样做的原因是因为在Web层出现之后才加载'shiro.ini',这是在CDI和EJB框架初始化之后。


8
2017-12-01 14:41



然后,如何将工厂提供的领域分配给安全管理员? - billdoor
上面的'shiro.ini'条目可以解决这个问题。如果你想看看Shiro是如何做的,请查看'org.apache.shiro.config.IniSecurityManagerFactory'的源代码。 - justin.hughey