Sunday, October 6, 2013

Apache Shiro with Spring framework, Java config and WebApplicationInitializer

Recently I was adding Apache Shiro security framework to Spring based web application which is using Java config and doesn't have xml configuration at all, not even web.xml

Apache Shiro documentation is mostly using xml examples so it took some time to put it all together in Java config based application.

Central part of Shiro security is  a realm. Here is how official Shiro documentation defines realms:

"A Realm is a component that can access application-specific security data such as users, roles, and permissions. The Realm translates this application-specific data into a format that Shiro understands so Shiro can in turn provide a single easy-to-understand Subject programming API no matter how many data sources exist or how application-specific your data might be."

Shiro comes with number of out-of-the-box Realm implementations that connects directly to database, to LDAP, etc, but in this example we will use custom Realm implementation since we want to access user data via our own user manager.

First, we have SecurityConfig. java where all security related beans are defined.
package com.xxx.yyy.config;

import com.xxx.yyy.security.CustomSecurityRealm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

@Configuration
public class SecurityConfig {

    @Bean
    public CustomSecurityRealm customSecurityRealm(){
        return new CustomSecurityRealm();
    }

    @Bean
    public WebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customSecurityRealm());
        return securityManager;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public MethodInvokingFactoryBean methodInvokingFactoryBean(){
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
        methodInvokingFactoryBean.setArguments(new Object[]{securityManager()});
        return methodInvokingFactoryBean;
    }

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

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

}

First bean defined is our custom security realm implementation. We will take a look at it in a moment, but for now let just look where is it used. And we don't have to go far, it is used by shiro security manager defined as second bean in SecurityConfig.java. We use DefaultWebSecurityManager since we plan to use Shiro for securing our applications URLs.
We just create an instance and inject our custom securtity realm bean to it.

After that we have few Shiro beans and we just inject our security manager bean wherever required.

Let's look how our custom security realm implementation looks like.
package com.xxx.yyy.security;

import com.xxx.yyy.security.Role;
import com.xxx.yyy.security.Permission;
import com.xxx.yyy.User;
import com.xxx.yyy.UserManager;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

public class CustomSecurityRealm extends AuthorizingRealm {

    @Autowired
    private UserManager userManager;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        Set roles = new HashSet<>();
        Set permissions = new HashSet<>();

        Collection principalsList = principals.byType(User.class);
        for (User user : principalsList) {
            for (Role role : user.getRoles()) {
                roles.add(role.getName());
                for (Iterator iterator = role.getPermissions().iterator(); iterator.hasNext(); ) {
                    Permission permission = iterator.next();
                    permissions.add(new WildcardPermission(permission.name()));
                }
            }
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        info.setRoles(roles);
        info.setObjectPermissions(permissions);

        return info;
    }

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

        UsernamePasswordToken upat = (UsernamePasswordToken) token;
        User user = userManager.getByUsername(upat.getUsername());
        if(user != null && user.getPassword().equals(new String(upat.getPassword()))) {
            return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        }
        else {
            throw new AuthenticationException("Invalid username/password combination!");
        }
    }
}

Our security  realm implementation will be used both for authentication and authorization so we extend  AuthorizingRealm which extends AuthenticatingRealm.
Autowired UserManager is our application service for accessing users and their roles and permissions.

doGetAuthenticationInfo method is used to authenticate user, and it has one argument - AuthenticationToken which holds username and password entered by user in login form.
Inside this method we check if user for given username exists and if password matches the password enetered by user. If those conditions are satisfied, we return AuthenticationInfo object with our user object as principal. We use Shiro's SimpleAuthenticationInfo implementation of AuthenticationInfo interface.
If user doesn't exist or password doesn't match we throw Authentication exception.
This is very simple example, in real project we will probably use Shiro's HashedCredentialsMatcher for checking username/password combination since we probably want to use encoded passwords.

doGetAuthorizationInfo method is used by Shiro to get roles and permissions for specific principal(s) so it has PrincipalCollection as argument. For every principal in given collection (usually there will be only one) we will get roles and permissions and set them to AuthorizationInfo which will be returned by this method. We use Shiro's SimpleAuthorizationInfo implementation for this purpose. The code should be pretty self-explanatory.

Now that we have our basic security infrastructure defined, we need to integrate it with our web application.
In order to protect urls we need to add Shiro filter to our web app descriptor.  As I mentioned in the beginning of this post, we don't use web.xml but instead we have WebApplicationInitializer.
here is how it looks like:

package com.xxx.yyy;

import com.xxx.yyy.config.DataConfig;
import com.xxx.yyy.config.SecurityConfig;
import com.xxx.yyy.config.WebConfig;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.DispatcherServlet;

public class WebInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {

        // Create the 'root' Spring application context
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register( DataConfig.class, SecurityConfig.class);

        // Manage the lifecycle of the root application context
        container.addListener(new ContextLoaderListener(rootContext));


        // Create the dispatcher servlet's Spring application context
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.setServletContext(container);
        dispatcherContext.setParent(rootContext);
        dispatcherContext.register(WebConfig.class);


        // Register and map the dispatcher servlet
        ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");


        container.addFilter("shiroFilter", new DelegatingFilterProxy("shiroFilterBean", dispatcherContext))
                           .addMappingForUrlPatterns(null, false, "/*");

    }
}
The code speaks for itself. We have root context with DataConfig (which contains JPA configuration, but this is not relevant for this story) and our SecurityConfig explained earlier.

In order to configure our Spring MVC  we added dispatcher context,and registered WebConfig class which contains required beans.

Last bean is most relevant since it defines Shiro filter which is configured to intercept all URLs.
We use DelegatingFilterProxy as filter implementation, and we provide "shiroFilterBean" for bean name.
This bean is defined in our WebConfig class, so let's take a look at it:
package com.xxx.yyy;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.xxx.yyy.web"})
public class WebConfig extends WebMvcConfigurerAdapter {


    @Autowired
    private WebSecurityManager securityManager;


    @Bean
    public VelocityConfigurer velocityConfig() {
        VelocityConfigurer configurer = new VelocityConfigurer();
        configurer.setResourceLoaderPath("/WEB-INF/templates");
        Properties props = new Properties();
        props.put("output.encoding", "UTF-8");
        props.put("input.encoding", "UTF-8");
        configurer.setVelocityProperties(props);
        return configurer;
    }

    @Bean
    public VelocityViewResolver viewResolver() {
        VelocityViewResolver resolver = new VelocityLayoutViewResolver();
        resolver.setExposeSpringMacroHelpers(true);
        resolver.setContentType("text/html;charset=UTF-8");
        resolver.setSuffix(".vm");
        return resolver;
    }

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("/WEB-INF/localization/messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(10);
        return messageSource;
    }

    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("LOCALE");
        return localeResolver;
    }


    @Bean
    public ShiroFilterFactoryBean shiroFilterBean(){
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        Map<String, String> definitionsMap = new HashMap<>();
        definitionsMap.put("/login.jsp", "authc");
        definitionsMap.put("/admin/**", "authc, roles[admin]");
        definitionsMap.put("/**", "authc");
        shiroFilter.setFilterChainDefinitionMap(definitionsMap);
        shiroFilter.setLoginUrl("/login.jsp");
        shiroFilter.setSecurityManager(securityManager);
        return shiroFilter;
    }
}


This is typical java config based Spring MVC configuration.
Beside usual Spring MVC beans, we added ShiroFilterFactoryBean (at the end) which will be referenced  from our WebApplicationInitializer , remember ?
ShiroFilterFactoryBean requires WebSecurityManager and since we defined it in SecurityConfig.java , all we need to do here is to autowire it to private field (line 12) and inject it to shiroFilterBean (line 60)

In between we just have velocity template engine configuration beans as well as localization beans which are not relevant for security framework.

Now, when user attempts to  access any application URL, Shiro filter will intercept it, and delegate security checking to shiroFilterBean which will use securityManager bean to determine if the user has right to access this specific URL.
If user is not authenticated yet, Shiro filter will redirect user to login page. Here is simple example of login.jsp:

<form action="" method="post" name="loginform">
  <table align="left" border="0" cellpadding="3" cellspacing="0">
    <tr>
 <td>Username:</td>
 <td><input maxlength="30" name="username" type="text" /></td>
    </tr>
    <tr>
 <td>Password:</td>
 <td><input maxlength="30" name="password" type="password" /></td>
    </tr>
    <tr>
 <td align="left" colspan="2">
   <input name="rememberMe" type="checkbox" /><span style="font-size: x-small;">Remember Me</span>
 </td>
    </tr>
    <tr>
 <td align="right" colspan="2"><input name="submit" type="submit" value="Login" /></td>
    </tr>
  </table>
</form>


And that's it... we have basic Shiro security setup. The way how application persists users, their roles and permission is application specific. All that Shiro has to know about this will get it from our security realm implementation.


UPDATE: Added /login.jsp to  ShiroFilterFactoryBean definitionsMap.  Without it Shiro will not handle login form submit correctly.  Although it looks like we are restricting anonymous access to it, Shiro will know that it should allow it.


UPDATE 2: Added imports to code snippets .

14 comments:

Kevin Burke said...

Thank you so much for this write-up, it saved me tons of time getting this all setup.

beegor said...

I'm glad it was useful for you. Notice the update I just added, it's important if you use form based login.

David Clark said...

Thank you very much for the example, I am trying to learn how to use Spring and Shiro with JavaConfig, and this is a great start. Unfortunately, I couldn't determine all the imports to help me compile and step thru it. Do you have it posted somewhere ?

beegor said...

David, your are absolutely right. I added imports to code snippets. Thanks for pointing that out!

Patrick Hummer said...

Thanks for the write up beegor it was really helpful.

Do you happen to know if there is any documentation anywhere to add a sessionManager and or cacheManager through java config?

Or are you using the shiro.ini for this?

Thanks appreciate it!

Patrick Hummer said...

Sorry one other thing how does one setup these in the java config.

/*
REQUEST FORWARD INCLUDE ERROR

do we add these to the definitionsMap in your example?

Thanks again.

beegor said...

Hi Patrick,
AFAIK Shiro docmentation uses xml configuration. I don't know if there is any java config specific documentation on sessionManager and cacheManager.
In this example we are using DefaultWebSecurityManager which uses org.apache.shiro.web.session.mgt.ServletContainerSessionManager by default. However you can set another implementation via setSessionManager method on securityManager bean (Defined in SecurityConfig in my post).
With DefaultWebSecurityManager you can only set implementations of org.apache.shiro.web.session.mgt.WebSessionManager.
You can also use setCacheManager method of securityManager bean to set cache manager.
Something like this:

@Bean
public WebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(mobillionSecurityRealm());
securityManager.setSessionManager(someWebSessionmanagerIml());
securityManager.setCacheManager(someCacherManagerImpl());
return securityManager;
}

Regarding REQUEST FORWARD INCLUDE ERROR question: You configure this on shiro filter (defined in WebInitializer class)

just change it to something like this:
container.addFilter("shiroFilter", new DelegatingFilterProxy("shiroFilter", dispatcherContext)).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR ), false, "/*");

Patrick Hummer said...

Great that works wonderfully. I had a fairly customized shiro.ini and was able to fully convert it to beans via java config.

We use Memcached for our cachemanger since we are using AWS.

Here was my Shiro.ini before
http://pastebin.com/vZacv2xv

Here is the SecurityConfig.java that replaces it:
http://pastebin.com/m1Kt1L1h

Also that other tidbit was spot on. Just needed to form an EnumSet to pass to the filter.

Thanks a ton!

beegor said...

Hi Patrick,
I am happy that my post was helpful for your migration to java based configuration. Good luck with your project.

David Clark said...

Thank you for the update and tutorial. When I try to implement this,I get errors that my @Autowired doesn't work. Do I need to do anything for my @Component and @Service classes to be picked up?

Here is a typical exception: Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.company.repository.people.UserRepository] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

beegor said...

Hi David,

Since the focus of this post is on shiro security, it doesn't cover all aspects of application.
In my example Usermanager is interface and implementation behind it is app specific.

If you are using spring data jpa, you need to configure datasource, entity manager, transaction manager and enable jpa repositories.


Here is example of DataConfig class:


import com.mchange.v2.c3p0.DriverManagerDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@PropertySource("classpath:application.properties")
@EnableTransactionManagement
@EnableJpaRepositories("com.xxx.yyy.dao")
public class DataConfig {


@Autowired
Environment env;

@Autowired
private DataSource dataSource;


@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
try {
dataSource.setDriverClass(env.getProperty("jdbc.driverClassName"));
dataSource.setJdbcUrl(env.getProperty("jdbc.url"));
dataSource.setUser(env.getProperty("jdbc.username"));
dataSource.setPassword(env.getProperty("jdbc.password"));
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}


@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManagerFactoryBean.setPackagesToScan("com.xxx.yyy.domain");
return entityManagerFactoryBean;
}

@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager();
}

@Bean
public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
}

}

David Clark said...

Thank you for the response and tutorial. I actually have a couple of @Configuration files for datasource, transactions, and web. They all played nicely but when I introduced Shiro, I can no longer deploy because the container no longer is able to autowire any beans due to proxy errors. I have changed my code to be very similar to yours, but still I get these errors.

beegor said...

It's hard to say what causing your problems without seeing the code, but generally two things could be wrong:
1. Repositories are nor initialized
2. Bean from one config class don't see bean from another config class.


Alex Sherwin said...

Order matters when defining the URL mapping rules... using a HashMap is wrong, it should be LinkedHashMap

Unfortunately the Javadoc on org.apache.shiro.spring.web.ShiroFilterFactoryBean.setFilterChainDefinitionMap(Map) does not clarify this, but you can see it in the org.apache.shiro.spring.web.ShiroFilterFactoryBean.ShiroFilterFactoryBean() constructor, the Shiro developers have this in there:

this.filterChainDefinitionMap = new LinkedHashMap(); //order matters!

An alternative, since that map already exists as a LinkedHashMap, is to call getFilterChainDefinitionMap() and add your entries to that