Dave Johnson on open web technologies, social software and software development
This is the third of my 2014 side projects that I'm sharing and one that involves the Apache Roller blog server and the Apache Shiro security framework. You might find this interesting if you're considering using Shiro for authentication and authorization, or if your interested in how security works in Apache Roller.
Inspired by my work with Ember.js in Fall 2014, I started thinking about what it would take to build an Ember.js-based editor/admin interface for Apache Roller. To do that, I'd need to add a comprehensive REST API to Roller, and I'd need a way to implement secrity for the new API. I've enjoyed working with Apache Shiro, so I decided that a good first step would be to figure out how to use Apache Shiro in Roller for Roller's existing web interface.
Working over the winter break I was able to replace Roller's existing Spring security implementation with Shiro and remove all Spring dependencies from my Rollarcus fork of Roller. Below I'll describe what I had to do get Shiro working for Form-base Authentication in Roller.
The first step in hooking Shiro into Roller is to implement a Shiro interface called ShiroAuthorizingRealm
.
This interface enables Shiro to do username and password checks for users when they attempt to login, and to get the user's roles.
Below is the first part of the class, which includes the doGetAuthenticationInfo()
method, which returns the AuthenticationInfo
for a user specified by an AuthenticationToken
that includes the user's username.
In other words, this method allows Shiro to look-up a user by providing a username and get back the user's (hashed) password, so that Shiro can validate a user's username and password.
public class ShiroAuthorizingRealm extends AuthorizingRealm { public ShiroAuthorizingRealm(){ setName("ShiroAuthorizingRealm"); setCredentialsMatcher( new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME)); } @Override public AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authToken; User user; try { user = loadUserByUsername( token.getUsername() ); } catch (WebloggerException ex) { throw new AuthenticationException( "Error looking up user " + token.getUsername(), ex); } if (user != null) { return new SimpleAuthenticationInfo( user.getUserName(), user.getPassword(), getName()); } else { throw new AuthenticationException( "Username not found: " + token.getUsername()); } }
In the code above you can see how we pull the username out of the authToken
provided by Shiro and we call a method, loadUserByUserName()
, which uses Roller's Java API to load a Roller user object specified by name.
The next method of interest is doGetAuthorizationInfo()
, which allows Shiro to look-up a user's Role. This allows Shiro to detmerine if the user is a Roller admin user or a blog editor.
public AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { String userName = (String) (principals.fromRealm(getName()).iterator().next()); User user; try { user = loadUserByUsername( userName ); } catch (WebloggerException ex) { throw new RuntimeException("Error looking up user " + userName, ex); } Weblogger roller = WebloggerFactory.getWeblogger(); UserManager umgr = roller.getUserManager(); if (user != null) { List roles; try { roles = umgr.getRoles(user); } catch (WebloggerException ex) { throw new RuntimeException( "Error looking up roles for user " + userName, ex); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); for ( String role : roles ) { info.addRole( role ); } log.debug("Returning " + roles.size() + " roles for user " + userName + " roles= " + roles); return info; } else { throw new RuntimeException("Username not found: " + userName); } }
In the code above you can see that we use the loadUserByUsername()
too look-up a user by username, then we use Roller's Java API to get the user's roles. We add those roles to an instance of the Shiro class SimpleAuthorizationInfo
and return it to Shir.
Now that we've implementated a realm, we've provided Shiro with everything needed to authenticate Roller users and get access to Roller user role information. Next, we need to configure Shiro to enforce roles for the URL apths found in Roller. Shiro includes a RolesAuthorizationFilter
, which is close to what we need but not exactly right for Roller. I had to extend Shiro's roles filter so that we can allow a user who has any (not all) of the required roles for a resource.
public class RollerRolesAuthorizationFilter extends RolesAuthorizationFilter { @Override public boolean isAccessAllowed( ServletRequest request, ServletResponse response, Object mappedValue) throws IOException { final Subject subject = getSubject(request, response); final String[] roles = (String[]) mappedValue; if (roles == null || roles.length == 0) { return true; } // user is authorized if they have ANY of the roles for (String role : roles) { if (subject.hasRole(role)) { return true; } } return false; } }
Now that we've seen the Java code needed to hook Shiro into Roller, lets look at how we configure Shiro to use that code. We do that using the Shiro configuration file: shiro.ini, as shown below.
shiro.ini (link)[main] defaultRealm = org.apache.roller.weblogger.auth.ShiroAuthorizingRealm securityManager.realms = $defaultRealm cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager securityManager.cacheManager = $cacheManager authc = org.apache.shiro.web.filter.authc.FormAuthenticationFilter authc.loginUrl = /roller-ui/login.rol authc.successUrl = /roller-ui/menu.rol rollerroles = org.apache.roller.weblogger.rest.auth.RollerRolesAuthorizationFilter [urls] /roller-ui/login.rol = authc /roller-ui/login-redirect.rol = authc, rollerroles[admin,editor] /roller-ui/profile** = authc, rollerroles[admin,editor] /roller-ui/createWeblog** = authc, rollerroles[admin,editor] /roller-ui/menu** = authc, rollerroles[admin,editor] /roller-ui/authoring/** = authc, rollerroles[admin,editor] /roller-ui/admin/** = authc, rollerroles[admin] /rewrite-status/** = authc, rollerroles[admin] /roller-services/rest/** = authcBasic, rollerroles[admin,editor]
In the configuration file above, you see how we hook in the new ShiroAuthorizingRealm
on line 3.
The next couple lines are boiler-plate code to hook in Shiro's caching mechanism and then, on line 9, we configure an authentication method called authc
, which is configured to use Shiro's Form Authentication feature.
And, on line 13, we hook in our new RollerRolesAuthorizationFilter
.
Next, we tell Shiro that the login page for Roller is /roller-ui/login.rol
and which page to direct a user to on a successful login, /roller-ui/menu.rol
, if the user did not specify which page they wanted to access.
And finally, on lines 17-25, you see the list of Roller URL patterns that need protection, which authentication method to use (authc or authcBasic) and the authorization filter and roles required for access to the URL pattern.
That's all there is to the story of Roller and Shiro so far. I was able to get Roller's form-based authentication working with Shiro, but I did not try to test with OpenID or LDAP, so I assume more work will be necessary to get them working. I did the work in my experimental Rollarcus fork of Roller. You can get the code from the shiro_not_spring branch. Pull requests are quite welcome as are suggestions for improvement. Please let me know if you see anything wrong in the above code.
This work may not find its way into Roller proper, but it plays a part in my the next side-project that I will share: A REST API for Roller with JAX-RS.
Dave Johnson in Roller
02:27AM Feb 09, 2015
Comments [0]
Tags:
asf
opensource
shiro