【问题标题】:Inject CDI managed bean in custom Shiro AuthorizingRealm在自定义 Shiro AuthorizingRealm 中注入 CDI 托管 bean
【发布时间】:2013-09-01 16:35:16
【问题描述】:

在我正在构建的应用程序中,我们直接使用 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

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

基本上,我有一个写得很好的基于 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,因为在类层次结构的父类中有一个 final init() 方法。

我的第二次尝试是让 MyAppRealm 实现所有需要的接口,并将它们委托给 AuthorizingRealm 实例。我不喜欢这个,但不妨试一试。

这让我更进一步,webapp 启动了,但仍然不足。原因是在配置文件 shiro.ini 中,我为我的领域指定了类:

myAppRealm = com.myapp.MyAppRealm

这几乎告诉我 Shiro 将负责创建 MyAppRealm 实例。因此它不会由 CDI 管理,因此不会被注入,这正是我所看到的。

我已经看到了这个 SO answer,但我不明白它是如何工作的,因为 AuthorizingRealm 的子类将再次继承最终的 init() 方法,这意味着子类不能被代理。

有什么想法可以解决这个问题吗?

【问题讨论】:

    标签: jakarta-ee cdi shiro


    【解决方案1】:

    这是一个经典问题:你有两个不同的框架,它们都想管理对象的生命周期,你需要让它们交互,但都坚持拥有完全的控制权(我对此的印象是哥斯拉和加美拉的战斗在东京市中心)。您可能不会立即将 Shiro 视为 CDI 的竞争对手,但由于它创建其对象的实例,它本质上包含一个微小的、基本的依赖注入框架(也许这是Greenspun's tenth rule 的 DI 版本)。我在制作 Web 框架时遇到了类似的问题,该框架创建和注入其支持 bean 的实例,与 CDI 交互。

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

    如果不是,那么网桥必须采用代理的形式。在该代理中,您可以进行显式 CDI 查找。您可以通过获取BeanManager 引导进入CDI,它允许您设置一个上下文,然后在其中创建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();
    

    【讨论】:

    • 感谢您的回复。这是一个非常好的理论答案,在这方面一针见血。我想我已经成功地使用 beanmanager 建立了一个“桥梁”。它不是很漂亮,但我希望随着时间的推移不断发展和完善。
    【解决方案2】:

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

    使用@Singleton 和@Startup 创建一个设置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 的内部工作原理。之所以可行,是因为在初始化 CDI 和 EJB 框架之后的 Web 层出现之前,“shiro.ini”不会被加载。

    【讨论】:

    • 然后如何将工厂提供的领域分配给安全管理器?
    • 上面的“shiro.ini”条目处理了这个问题。如果你想了解 Shiro 是如何做到的,请查看 'org.apache.shiro.config.IniSecurityManagerFactory' 的源代码。
    猜你喜欢
    • 2015-08-06
    • 2011-10-16
    • 2015-04-20
    • 2013-03-14
    • 1970-01-01
    • 1970-01-01
    • 2012-01-30
    • 2015-10-28
    • 1970-01-01
    相关资源
    最近更新 更多