When building commercial applications the twin issues of authentication and authorization are almost certain to arise at some point, the topic becomes even livelier when you throw a legacy infrastructure into the mix and want everything to work seamlessly.
So what’s a really nice and simple way of doing this? Let’s look at Apache Shiro and see how it simplifies things for us.
What is Apache Shiro? I think they say it best themselves
Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application
Right - so let’s get our hands dirty, and see how we can leverage Shiro.
Create a Web application
New Project -> Maven -> Maven Web Application
Create a new Stateless Session Bean
HelloResource class
@Stateless
@Path("/message")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayHello() {
return "hello someone";
}
}
Note that under your PersonalHello web project Netbeans has created a RESTful web services node, under which your HelloResource lives.
Right click the RESTful Web Services node and Select REST Resources configuration.
Change the name of the REST Resources Path, this will make NetBeans generate the web.xml file with your servlet adaptor.
Run your web app and you should see the Standard ‘Hello World!’ index page.
Go to
http://localhost:8080/PersonalHello/rest/message and you should see your ‘hello someone’ message.
So now we know that our simple web service with no authentication is working, lets add Shiro into the mix and see how complicated things become!
Adding Shiro
Add the following dependencies to your projects pom.xml
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.0.0-incubating</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.6.1</version>
<scope>runtime</scope>
</dependency>
Note: Shiro has just graduated from the Apache Incubator, so expect a non-incubating release sometime soon.
Go to your web.xml and add the following
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
<init-param>
<param-name>config</param-name>
<param-value>
# The IniShiroFilter configuration is very powerful and flexible, while still remaining succinct.
# Please read the org.apache.shiro.web.servlet.IniShiroFilter JavaDoc for information.
# Quick Tip: Instead of having this configuration here in web.xml, you can instead
# move all of this to a 'shiro.ini' file at the root of the classpath and remove
# the 'config' init-param. Or you can specify the 'configPath' init-param and specify the
# path to a resource at any location (url, file or classpath). This may be desired if the
# config gets long and you want to keep web.xml clean.
[users]
# format: username = password, role1, role2, ..., roleN
root = secret,admin
guest = guest,guest
presidentskroob = 12345,president,admin
darkhelmet = ludicrousspeed,darklord,schwartz
lonestarr = vespa,goodguy,schwartz
[roles]
# format; roleName = permission1, permission2, ..., permissionN
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
[urls]
/rest/** = authcBasic
</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Go New File -> Other -> properties
And add the following text to the file
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# This file is used to format all logging output
log4j.rootLogger=TRACE, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %-5p [%c]: %m%n
# =============================================================================
# 3rd Party Libraries
# OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
# =============================================================================
# ehcache caching manager:
log4j.logger.net.sf.ehcache=WARN
# Most all Apache libs:
log4j.logger.org.apache=WARN
# Quartz Enterprise Scheular (java 'cron' utility)
log4j.logger.org.quartz=WARN
# =============================================================================
# Apache Shiro
# =============================================================================
# Shiro security framework
log4j.logger.org.apache.shiro=TRACE
#log4j.logger.org.apache.shiro.realm.text.PropertiesRealm=INFO
#log4j.logger.org.apache.shiro.cache.ehcache.EhCache=INFO
#log4j.logger.org.apache.shiro.io=INFO
#log4j.logger.org.apache.shiro.web.servlet=INFO
log4j.logger.org.apache.shiro.util.ThreadContext=INFO
And finally let’s make our hello a little more personal. Change the sayHello method to the following
public String sayHello() {
StringBuilder builder = new StringBuilder();
builder.append("Hello ");
builder.append(SecurityUtils.getSubject().getPrincipal().toString());
return builder.toString();
}
Run your web-app again and go to
http://localhost:8080/PersonalHello/rest/message you will now see a login prompt. Use ‘lonestarr’ as the user and ‘vespa’ as the password (or any of the other combinations that are in the config that you placed in the web.xml)
You will now be greeted by a personalised Hello, pretty nifty me think!
What have we done to achieve this?
1. Added the dependencies to our project
2. Made sure that log4j was configured
3. Modified our web.xml with the Shiro specific config
- The important things to note here are:
1. The Filter that in our case says every request must go through Shiro
2. /rest/** = authcBasic - This says that the urls here require basic authentication
3. The users section
So not much work to achieve the authentication at all then.
Adding Authorization
Create a new class in your Web App:
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import org.apache.shiro.SecurityUtils;
public class AuthorizationInterceptor {
@AroundInvoke
public Object authenticate(InvocationContext context) throws Exception{
if (!SecurityUtils.getSubject().hasRole("admin")){
StringBuilder exBuilder = new StringBuilder();
exBuilder.append("User: \'");
exBuilder.append(SecurityUtils.getSubject().getPrincipal().toString());
exBuilder.append("\' is not authorized to use this resource");
throw new SecurityException(exBuilder.toString());
}
return context.proceed();
}
}
And add the
@Interceptors({AuthorizationInterceptor.class}) Annotation to your HelloResource class
package sparg.tim.personalhello;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.shiro.SecurityUtils;
/**
*
* @author tsparg
*/
@Interceptors({AuthorizationInterceptor.class})
@Stateless
@Path("/message")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayHello() {
StringBuilder builder = new StringBuilder();
builder.append("Hello ");
builder.append(SecurityUtils.getSubject().getPrincipal().toString());
return builder.toString();
}
}
Run your application and go to
http://localhost:8080/PersonalHello/rest/message and login with ‘guest’ username ‘guest’ password (or any user that is not a part of the admin group).
You should now see an error page telling you that you are not authenticated to view this page.
If you log in with any of the other user codes that are members of the admin role (look in your web.xml to see which) you will proceed to the normal screen.
A couple of things to note:
- Shiro has some very nifty hooks into both Authentication and Authorization so that you can leverage any existing infrastructure that you may have.
- With the Way I have done the Authorization the App Server returns a HTTP 500(Internal Error), it would be nice if I could figure out how to return a HTTP 401 (Unauthorized)
- You should most probably make the AuthorizationInterceptor a Default Interceptor, meaning you wouldn't have to annotate your classes for authorization.
I quite like this approach, as the authorization and authentication can be quite clearly moved away from any domain specific code.