Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring is ignoring @Transactional annotations in Apache Shiro Realm class

I am using Spring for IOC and transaction management, and am planning to use Apache Shiro as the security library.

Whenever I want to check a user's permissions, I call subject.isPermitted("right"), whereupon Shiro checks for the permission using a datastore. Within these calls, a database connection is established, and I have annotated the method with @Transactional. However, I always receive an error that there is no Hibernate session bound to the thread whenever I execute the permission check.

The method is in the Realm class. I defined a custom Shiro Realm class:

@Component
public class MainRealm extends AuthorizingRealm {

@Autowired
protected SysUserDao userDao;

@Transactional
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
        throws AuthenticationException {
    ...
    final SysUser user = this.userDao.findByUsername(un);
    ...
    return authInfo;
}

@Transactional
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    ...
    permissions = this.userDao.getAccessRights(un);
    ...
    return authInfo;
}
}

Apache Shiro uses a Servlet Filter, so I have the following defined in web.xml:

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

I am using programmatic configuration for Spring. Here is my App Config class:

@Configuration //Replaces Spring XML configuration
@ComponentScan(basePackages = "com.mycompany")
@EnableTransactionManagement //Enables declarative Transaction annotations
public class SpringAppConfig {

@Bean
public DataSource sqlServerDataSource() throws Exception {...}
@Bean
@Autowired
public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) {...}
@Bean
public AnnotationSessionFactoryBean getSessionFactory() throws Exception {...}
@Bean
public static PersistenceExceptionTranslationPostProcessor exceptionTranslation() {...}

@Bean
@Autowired
public DefaultWebSecurityManager securityManager(MainRealm mainRealm) {
    final HashedCredentialsMatcher hcm = new HashedCredentialsMatcher(shiroHash);
    hcm.setHashIterations(shiroIter);
    hcm.setStoredCredentialsHexEncoded(shiroHexEncoded);
    mainRealm.setCredentialsMatcher(hcm);
    final DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
    sm.setRealm(mainRealm);
    return sm;
}

@Bean
@Autowired
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
    final ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
    filter.setSecurityManager(securityManager);
    return filter;
}

/**
 * This method needs to be static due to issues defined here:<br>
 * https://issues.apache.org/jira/browse/SHIRO-222
 */
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    LifecycleBeanPostProcessor lbpp = new LifecycleBeanPostProcessor();
    return lbpp;
}

@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    return new DefaultAdvisorAutoProxyCreator();
}

@Bean
@Autowired
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager secMan) {
    AuthorizationAttributeSourceAdvisor advBean = new AuthorizationAttributeSourceAdvisor();
    advBean.setSecurityManager(secMan);
    return advBean;
}
}

To summarize the issue, I believe my MainRealm class is being wired properly (it has an @Autowired dependency to a DAO object and I verified that it is not null) with the exception of the @Transactional annotation. Due to this, I cannot directly call user.isPermitted("") as it prompts an error: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here.

Would like to ask for help in checking whether I missed anything in my Spring configuration.

In the meantime I have hacked over this issue by calling the user.isPermitted("") function within a method in my Service class that's correctly bound by a @Transactional.

EDIT When I checked the logs for Spring initialization I can see this:

Bean 'mainRealm' of type [class com.x.security.MainRealm] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

According to this SO answer it means MainRealm isn't being postprocessed by the transaction manager therefore any @Transactional annotations get ignored. If so, how can I correct this?

EDIT 2 According to this SO question: "In other words, if I write my own BeanPostProcessor, and that class directly references other beans in the context, then those referenced beans will not be eligible for auto-proxying, and a message is logged to that effect." I just checked ShiroFilterFactoryBean and it is in fact a BeanPostProcessor. And the problem is it requires a SecurityManager instance that in turn requires a MainRealm instance. So both beans are autowired and are thus ineligible for proxying. I feel like I'm closer to the solution but I still can't resolve it.

like image 959
Jensen Ching Avatar asked Apr 10 '13 09:04

Jensen Ching


2 Answers

The root cause of the issue is in fact due to the following:

All BeanPostProcessors and their directly referenced beans will be instantiated on startup... Since AOP auto-proxying is implemented as a BeanPostProcessor itself, no BeanPostProcessors or directly referenced beans are eligible for auto-proxying (and thus will not have aspects 'woven' into them.

Referenced SO question is here.

I have resolved this issue by decoupling the Realm bean creation from the SecurityManager bean creation.

The relevant change is from the following code:

@Bean
@Autowired
public DefaultWebSecurityManager securityManager(MainRealm mainRealm) {
    final HashedCredentialsMatcher hcm = new HashedCredentialsMatcher(shiroHash);
    hcm.setHashIterations(shiroIter);
    hcm.setStoredCredentialsHexEncoded(shiroHexEncoded);
    mainRealm.setCredentialsMatcher(hcm);
    final DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
    sm.setRealm(mainRealm);
    return sm;
}

to the following code:

@Bean
public DefaultWebSecurityManager securityManager() {
    final DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
    //sm.setRealm(mainRealm); -> set this AFTER Spring initialization so no dependencies
    return sm;
}

Then I use a ServletContextListener which listens to when the Spring context initialization completes and I have the both the MainRealm and SecurityManager beans. Then I just plug one bean inside another.

@Override
public void contextInitialized(ServletContextEvent sce) {
    try {

        //Initialize realms
        final MainRealm mainRealm = (MainRealm)ctx.getBean("mainRealm");
        final DefaultWebSecurityManager sm = (DefaultWebSecurityManager)ctx.getBean("securityManager");
        sm.setRealm(mainRealm);
    } catch (Exception e) {
        System.out.println("Error loading: " + e.getMessage());
        throw new Error("Critical system error", e);
    }
}
like image 190
Jensen Ching Avatar answered Sep 20 '22 14:09

Jensen Ching


@Transactional annotation can be used before :

  • An interface def
  • An interface method
  • A class def
  • A PUBLIC method of a class

As explained in the documentation

The fact that your method is protected must be the reason of your problem, and your service method was maybe declared as public which would explained why it worked in that case

like image 28
DessDess Avatar answered Sep 22 '22 14:09

DessDess