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) { Setroles = 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 .