About Me

My photo
I'm project manager of a software development team at www.researchspace.com. I've been developing bioinformatics software for the last 9 years or so, and I'm hoping to share some of the experience and knowledge I've gained with Eclipse RCP, Java web app development and software development in general on these blogs.

Wednesday 22 October 2014

Apache Shiro and Spring Boot

Spring Boot is a great way to get a Spring web application up and running, with many default settings to make the configuration of  standard functionality such as logging, view resolution, and database configuration as painless as possible.

It's also possible to add in SpringSecurity. However, since I've been using Apache Shiro for some time in other projects, and didn't particularly want to learn a new security library, I wanted to see if I could get it set up with  a Spring Boot application.

Basic setup

My environment is Java 7, Spring 4.0.5, using Shiro 1.2,  deploying  to a Servlet 3 container.

Differences between configuration described in current Shiro 1.2 documentation and Spring Boot.

 Boot encourages pure Java configuration, with no Spring XML files or even a web.xml file. So we need to declare all the beans needed for Shiro in a class annotated with Spring's @Configuration annotation.
Here is my class to set up Shiro:
 @Configuration  
 public class SecurityConfig {  
      @Bean()  
      public ShiroFilterFactoryBean shiroFilter (){  
           ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean ();  
           factory.setSecurityManager(securityManager());  
           factory.setLoginUrl("/ui/login");  
           factory.setSuccessUrl("/ui/listView");  
           factory.setUnauthorizedUrl("/ui/login");   
           factory.setFilterChainDefinitions(  
                 "/assets/scripts/**=anon\n"+  
                 "/license/**=anon\n"+  
                 "/manage/health/=anon\n"+  
              "/assets/static/*=authc\n"+  
                 "/manage/metrics/**=authc\n"+  
                 "/manage/beans/**=authc\n"+  
                 "/manage/trace/**=authc\n"+  
                 "/manage/mappings/**=authc\n"+  
                 "/manage/dump/**=authc\n"+  
                 "/manage/autoconfig/**=authc\n"+  
                 "/manage/env/**=authc\n"+  
                 "/manage/info/**=authc");  
           return factory;  
      }  
      @Bean  
      public SecurityManager securityManager() {  
           DefaultWebSecurityManager rc = new DefaultWebSecurityManager();  
           rc.setRealm(realm());  
           return rc;  
      }  
     @Bean public AuthorizingRealm realm() {  
           AuthorizingRealm realm = new AuthorizingRealm() {  
                @Override  
                protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)  
                          throws AuthenticationException {  
                     return new SimpleAuthenticationInfo("user", "password", "login");  
                }  
                @Override  
                protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {  
                     // TODO Auto-generated method stub  
                     return null;  
                }  
           };  
           realm.setName("login");  
           return realm;  
      }  
 }  

As you can see, you just need to set up 3 beans as a minimum, constructing a ShiroFilterFactoryBean, a SecurityManager and a Realm. In this example I've created a trivial Realm implementation, in practice you'll probably want to connect to a backend database to verify credentials. If you'r econfiguring a Realm that needs initialization, or want to add in any Spring Bean that implements the Initializable interface, you'll need to add in one more definition, e.g.,:

 @Bean  
 public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {  
     return new LifecycleBeanPostProcessor();  
 }  
  @Bean  
  @DependsOn("lifecycleBeanPostProcessor")  
  public TextConfigurationRealm realm() {  
     IniRealm realm = new IniRealm() ;  
     realm.setResourcePath("classpath:users.ini");      
     return realm;  
 }  


Instead of XML Configuration, we can just use setters in the classes to set property values.

SpringBoot can expose a set of URLs for monitoring and health checking. By default these are '/health', '/info' etc, but by setting a property in application.properties:
 management.context-path=/manage  

we can set these URLs with a prefix, and configure them to be authenticated. This is important as they give away a lot of sensitive information.

Customizing Shiro

For my application, I wanted to provide a custom filter that extended FormAuthenticationFilter .
So, I set a new @Bean definition to create my Filter. By giving it the name 'authc' it should replace the existing FormAuthenticationFilter  with my subclass.

In order to get this to work, it was crucial to define the filter after the ShiroFilterFactoryBean in the code. If it was defined first, then it seemed to prevent the correct behaviour of the FactoryBean to produce SpringShiroFilter instances. 
But, by defining after the Factory bean, everything works properly. The reason I'm stressing this point is that in the old  standard XML configuration, the order didn't seem to matter.
 management.context-path=/managepackage com.researchspace.licenseserver;  
 import java.util.HashMap;  
 import java.util.Map;  
 import javax.servlet.Filter;  
 import org.apache.shiro.authc.AuthenticationException;  
 import org.apache.shiro.authc.AuthenticationInfo;  
 import org.apache.shiro.authc.AuthenticationToken;  
 import org.apache.shiro.authc.SimpleAuthenticationInfo;  
 import org.apache.shiro.authz.AuthorizationInfo;  
 import org.apache.shiro.mgt.SecurityManager;  
 import org.apache.shiro.realm.AuthorizingRealm;  
 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;  
 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;  
 import org.apache.shiro.subject.PrincipalCollection;  
 import org.apache.shiro.web.mgt.DefaultWebSecurityManager;  
 import org.springframework.context.annotation.Bean;  
 import org.springframework.context.annotation.Configuration;  
 import com.researchspace.licenseserver.controller.ShiroFormFilterExt;  
 @Configuration  
 public class SecurityConfig {  
      @Bean()  
      public ShiroFilterFactoryBean shiroFilter (){  
           ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean ();  
           factory.setSecurityManager(securityManager());  
           factory.setLoginUrl("/ui/login");  
           factory.setSuccessUrl("/ui/listView");  
           factory.setUnauthorizedUrl("/ui/login");  
           // this is ordered, better to do like this.  
           factory.setFilterChainDefinitions(  
                 "/assets/scripts/**=anon\n"+  
                 "/license/**=anon\n"+  
                 "/manage/health/=anon\n"+  
              "/assets/static/*=authc\n"+  
                 "/manage/metrics/**=authc\n"+  
                 "/manage/beans/**=authc\n"+  
                 "/manage/trace/**=authc\n"+  
                 "/manage/mappings/**=authc\n"+  
                 "/manage/dump/**=authc\n"+  
                 "/manage/autoconfig/**=authc\n"+  
                 "/manage/env/**=authc\n"+  
                 "/manage/info/**=authc");  
           Map<String,Filter> filters= new HashMap<>();  
           filters.put("authc", authc());  
           factory.setFilters(filters);  
           return factory;  
      }  
      @Bean  
      public SecurityManager securityManager() {  
           DefaultWebSecurityManager rc = new DefaultWebSecurityManager();  
           rc.setRealm(realm());  
           return rc;  
      }  
      @Bean public AuthorizingRealm realm() {  
           AuthorizingRealm realm = new AuthorizingRealm() {  
                @Override  
                protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)  
                          throws AuthenticationException {  
                     return new SimpleAuthenticationInfo("user", "password", "login");  
                }  
                @Override  
                protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {  
                     // TODO Auto-generated method stub  
                     return null;  
                }  
           };  
           realm.setName("login");  
           return realm;  
      }  
      @Bean(name="authc") // this must be AFTER the factory bean definition 
      public ShiroFormFilterExt authc(){  
           return new ShiroFormFilterExt();  
      }  
 }  

I hope other people trying to use Shiro with SpringBoot will find this useful.

No comments: