Security is probably the most important thing for your application, but it doesn’t have to be the hardest thing. Today I’ll show you how to use Shiro’s wildcard permissions to enable fine grained Role-Based Access Control (RBAC) which makes granting user permissions trivial (a single line). This will also make your application’s security policy more flexible, so when your business rules change (and you know they will) your code does not have to. You can read more about RBAC and Roles vs Permissions here.

Last week I compared the differences between a JAX-RS resource and the equivalent Spring REST controller. Today I’m going to reuse the same Stormtrooper JAX-RS example and walk through setting up authentication and authorization using Apache Shiro.

Many Shiro users are already familiar with Stig Inge Lea Bjørnsen‘s great work on silb/shiro-jersey. The Apache Shiro team has incorporated this module into the 1.4 release. Our implementation differs slightly in that we not supporting the Jersey specific @Auth annotation, in turn the new module is portable between JAX-RS implementations, for example we are able to support Jersey, RestEasy, and Apache CXF.

The source for this example is on Github: stormpath/shiro-jaxrs-example. You can run the example with Apache Maven using mvn jetty:run .

Set Up and Configure Your Apache Shiro Project

Lets jump right into the code! You can grab it from the link above or create a new project and follow along.

Add Maven Dependencies

I’m going to use Apache Maven, but the instructions are similar if you are using Gradle. First, we need a few dependencies to compile our code where ${shiro.version} is 1.4.0-RC2 or greater:

<dependency> <groupId>javax.ws.rs</groupId> <artifactId>javax.ws.rs-api</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-servlet-plugin</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-jaxrs</artifactId> <version>${shiro.version}</version> </dependency> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 < dependency > < groupId > javax . ws . rs < / groupId > < artifactId > javax . ws . rs - api < / artifactId > < version > 2.0.1 < / version > < / dependency > < dependency > < groupId > org . apache . shiro < / groupId > < artifactId > shiro - servlet - plugin < / artifactId > < version > $ { shiro . version } < / version > < / dependency > < dependency > < groupId > org . apache . shiro < / groupId > < artifactId > shiro - jaxrs < / artifactId > < version > $ { shiro . version } < / version > < / dependency >

You also need a couple runtime dependencies for the JAX-RS implementation of your choice, this example is portable meaning it will run using any JAX-RS implementation (I’ve tested this one with Jersey and RestEasy). NOTE: the runtime scope.

If you want to use Jersey:

<dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-grizzly2-servlet</artifactId> <version>${jersey.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>${jersey.version}</version> <scope>runtime</scope> </dependency> 1 2 3 4 5 6 7 8 9 10 11 12 13 < dependency > < groupId > org . glassfish . jersey . containers < / groupId > < artifactId > jersey - container - grizzly2 - servlet < / artifactId > < version > $ { jersey . version } < / version > < scope > runtime < / scope > < / dependency > < dependency > < groupId > org . glassfish . jersey . media < / groupId > < artifactId > jersey - media - json - jackson < / artifactId > < version > $ { jersey . version } < / version > < scope > runtime < / scope > < / dependency >

Or if RestEasy is more your thing:

<dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxrs</artifactId> <version>${resteasy.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-servlet-initializer</artifactId> <version>${resteasy.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> <version>${resteasy.version}</version> <scope>runtime</scope> </dependency> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 < dependency > < groupId > org . jboss . resteasy < / groupId > < artifactId > resteasy - jaxrs < / artifactId > < version > $ { resteasy . version } < / version > < scope > runtime < / scope > < / dependency > < dependency > < groupId > org . jboss . resteasy < / groupId > < artifactId > resteasy - servlet - initializer < / artifactId > < version > $ { resteasy . version } < / version > < scope > runtime < / scope > < / dependency > < dependency > < groupId > org . jboss . resteasy < / groupId > < artifactId > resteasy - jackson2 - provider < / artifactId > < version > $ { resteasy . version } < / version > < scope > runtime < / scope > < / dependency >

Configure Apache Shiro

Apache Shiro can be configured with Spring, Guice, or an INI file. In this example I’ll use a shiro.ini file for the configuration. I’ll also be using username/passwords that are statically defined, while this is great for a tutorial, it isn’t great for your production server, later in this post I’ll show you how to hook up Stormpath’s Shiro integration instead.

[main] cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager securityManager.cacheManager = $cacheManager [urls] # use permissive mode to NOT require authentication, our resource Annotations will decide that /** = noSessionCreation, authcBasic[permissive] [users] # format: username = password, role1, role2, ..., roleN root = secret,admin emperor = secret,admin officer = secret,officer guest = secret [roles] # format: roleName = permission1, permission2, ..., permissionN admin = * officer = troopers:create, troopers:read, troopers:update 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [ main ] cacheManager = org . apache . shiro . cache . MemoryConstrainedCacheManager securityManager . cacheManager = $ cacheManager [ urls ] # use permissive mode to NOT require authentication, our resource Annotations will decide that /** = noSessionCreation , authcBasic [ permissive ] [ users ] # format: username = password, role1, role2, ..., roleN root = secret , admin emperor = secret , admin officer = secret , officer guest = secret [ roles ] # format: roleName = permission1, permission2, ..., permissionN admin = * officer = troopers : create , troopers : read , troopers : update

The /** = noSessionCreation, authcBasic[permissive] line in the [urls] section instructs Shiro to NOT track the user’s session and to allow basic authentication (but not require it), more on that below. The rest of the configuration sets up the static users, roles, and permissions. That is all it takes add authentication to any application with with Apache Shiro.

Authorize the Stormtroopers (JAX-RS Resource)

Lets chat about permissions for a moment. Apache Shiro uses Wildcard Permissions out of the box. What this means, is while you could use simple strings to represent a permission such as trooperReader , you should probably use something like trooper:read . This way you can structure your permissions with common parts and then assign any portion of that string to your users. In the example below we will be using the following permission strings (one for each of the CRUD actions):

trooper:create

trooper:read

trooper:update

trooper:delete .

If we wanted to grant a user access to all of the stormtrooper CRUD actions, we could grant them the trooper:* permission, the wildcard implies all trooper permissions. If we added an additional TIE Fighter resource, we would model its CRUD permissions the same way:

tiefighter:create

tiefighter:read

tiefighter:update

tiefighter:delete

Similar to granting all stormtrooper permissions using trooper:* we could grant users read only access to all resources using *:read .

Now we could grant permissions to users individually, it typically easier to manage them aggregated in roles. You will notice in the previous section The Emperor has the admin role, which in turn has the * permission (which makes him all powerful). Officers on the other hand can only create, read, or update stormtroopers, but NOT delete them.

To keep things simple and readable for this example I’m only working with two layers of strings, the resource trooper and the action read , but it doesn’t have to end there, for example if we wanted to model all stormtroopers on the Death Star we could use deathstar:troopers:read . Similarly combining the tiefighter example above, deathstar:*:read would grant a user read access to all resources on the Death Star.

Tip: Remember, your fully qualified permissions (for example trooper:read ) are assigned to the resource and the more flexible version assigned to your user or role: trooper:* , * , *:read , or even just trooper:read . Take a look at the WildcardPermission documentation on the Shiro site for more info.

Let’s get back to the code!

@Path("/troopers") @Produces("application/json") public class StormtroooperResource { private final StormtrooperDao trooperDao; public StormtroooperResource(StormtrooperDao trooperDao) { this.trooperDao = trooperDao; } @GET @RequiresPermissions("troopers:read") public Collection<Stormtrooper> listTroopers() { return trooperDao.listStormtroopers(); } @Path("/{id}") @GET @RequiresPermissions("troopers:read") public Stormtrooper getTrooper(@PathParam("id") String id) throws NotFoundException { Stormtrooper stormtrooper = trooperDao.getStormtrooper(id); if (stormtrooper == null) { throw new NotFoundException(); } return stormtrooper; } @POST @RequiresPermissions("troopers:create") public Stormtrooper createTrooper(Stormtrooper trooper) { return trooperDao.addStormtrooper(trooper); } @Path("/{id}") @POST @RequiresPermissions("troopers:update") public Stormtrooper updateTrooper(@PathParam("id") String id, Stormtrooper updatedTrooper) throws NotFoundException { return trooperDao.updateStormtrooper(id, updatedTrooper); } @Path("/{id}") @DELETE @RequiresPermissions("troopers:delete") public void deleteTrooper(@PathParam("id") String id) { trooperDao.deleteStormtrooper(id); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Path ( "/troopers" ) @Produces ( "application/json" ) public class StormtroooperResource { private final StormtrooperDao trooperDao ; public StormtroooperResource ( StormtrooperDao trooperDao ) { this . trooperDao = trooperDao ; } @GET @RequiresPermissions ( "troopers:read" ) public Collection <Stormtrooper> listTroopers ( ) { return trooperDao . listStormtroopers ( ) ; } @Path ( "/{id}" ) @GET @RequiresPermissions ( "troopers:read" ) public Stormtrooper getTrooper ( @PathParam ( "id" ) String id ) throws NotFoundException { Stormtrooper stormtrooper = trooperDao . getStormtrooper ( id ) ; if ( stormtrooper == null ) { throw new NotFoundException ( ) ; } return stormtrooper ; } @POST @RequiresPermissions ( "troopers:create" ) public Stormtrooper createTrooper ( Stormtrooper trooper ) { return trooperDao . addStormtrooper ( trooper ) ; } @Path ( "/{id}" ) @POST @RequiresPermissions ( "troopers:update" ) public Stormtrooper updateTrooper ( @PathParam ( "id" ) String id , Stormtrooper updatedTrooper ) throws NotFoundException { return trooperDao . updateStormtrooper ( id , updatedTrooper ) ; } @Path ( "/{id}" ) @DELETE @RequiresPermissions ( "troopers:delete" ) public void deleteTrooper ( @PathParam ( "id" ) String id ) { trooperDao . deleteStormtrooper ( id ) ; } }

As you can see the highlighted lines, we have added authorization to our JAX-RS resource using a single line, an annotation for each method. The Shiro @RequiresPermissions annotation, which binds a permission string to a given method, that method in turn gets bound to an HTTP request path by the JAX-RS implementation. Apache Shiro also has similar annotations to require roles and users, those are detailed in the Apache Shiro authorization guide.

Configure Your JAX-RS Application

The only thing left is to create a JAX-RS Application class:

@ApplicationPath("/") public class JaxrsApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> classes = new HashSet<>(); // register Shiro classes.add(ShiroFeature.class); return classes; } @Override public Set<Object> getSingletons() { Set<Object> singletons = new HashSet<>(); // This example does NOT use DI, so we will just create an instance to use as a singleton. singletons.add(new StormtroooperResource(new DefaultStormtrooperDao())); return singletons; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @ApplicationPath ( "/" ) public class JaxrsApplication extends Application { @Override public Set < Class <? > > getClasses ( ) { Set < Class <? > > classes = new HashSet < > ( ) ; // register Shiro classes . add ( ShiroFeature . class ) ; return classes ; } @Override public Set <Object> getSingletons ( ) { Set <Object> singletons = new HashSet < > ( ) ; // This example does NOT use DI, so we will just create an instance to use as a singleton. singletons . add ( new StormtroooperResource ( new DefaultStormtrooperDao ( ) ) ) ; return singletons ; } }

In the highlighted line above we are adding the ShiroFeature which will configure the annotation processing, as well as Shiro Exception mapping ( UnauthorizedException are mapped to HTTP status code 403, and all other thrown AuthorizationException use 401).

NOTE: if you are using Stormpath’s integration you should replace ShiroFeature.class with StormpathShiroFeature.class from the stormpath-shiro-jaxr module.

Fire it Up!

That’s it! If you have followed along this far, or have just grabbed the code from Github, you can start up the example by running mvn jetty:run . Now you are ready to start poking around the /troopers resource.

List all stormtroopers:

$ curl http://localhost:8080/troopers HTTP/1.1 401 Unauthorized Content-Length: 0 Date: Wed, 09 Nov 2016 18:31:49 GMT Server: Jetty(9.3.14.v20161028) 1 2 3 4 5 6 7 $ curl http : //localhost:8080/troopers HTTP / 1.1 401 Unauthorized Content - Length : 0 Date : Wed , 09 Nov 2016 18 : 31 : 49 GMT Server : Jetty ( 9.3.14.v20161028 )

Don’t forget this resource requires authentication:

$ curl --user emperor:secret http://localhost:8080/troopers HTTP/1.1 200 OK Content-Length: 3941 Content-Type: application/json Date: Wed, 09 Nov 2016 18:33:47 GMT Server: Jetty(9.3.14.v20161028) Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Tue, 08-Nov-2016 18:33:47 GMT [ { "id": "FN-0089", "planetOfOrigin": "Felucia", "species": "Twi'lek", "type": "Space" }, { "id": "FN-0386", "planetOfOrigin": "Coruscant", "species": "Human", "type": "Sand" }, { "id": "FN-0579", "planetOfOrigin": "Serenno", "species": "Twi'lek", "type": "Marine" }, ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 $ curl -- user emperor : secret http : //localhost:8080/troopers HTTP / 1.1 200 OK Content - Length : 3941 Content - Type : application / json Date : Wed , 09 Nov 2016 18 : 33 : 47 GMT Server : Jetty ( 9.3.14.v20161028 ) Set - Cookie : rememberMe = deleteMe ; Path =/ ; Max - Age = 0 ; Expires = Tue , 08 - Nov - 2016 18 : 33 : 47 GMT [ { "id" : "FN-0089" , "planetOfOrigin" : "Felucia" , "species" : "Twi'lek" , "type" : "Space" } , { "id" : "FN-0386" , "planetOfOrigin" : "Coruscant" , "species" : "Human" , "type" : "Sand" } , { "id" : "FN-0579" , "planetOfOrigin" : "Serenno" , "species" : "Twi'lek" , "type" : "Marine" } , . . .

The Emperor of course has access to manipulate any of our resources, he has the * permission. If we try the same thing with the ‘guest’ user we will get a 403 response:

$ curl --user guest:secret http://localhost:8080/troopers HTTP/1.1 403 Forbidden Content-Length: 0 Date: Wed, 09 Nov 2016 18:36:40 GMT Server: Jetty(9.3.14.v20161028) Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Tue, 08-Nov-2016 18:36:40 GMT 1 2 3 4 5 6 7 8 $ curl -- user guest : secret http : //localhost:8080/troopers HTTP / 1.1 403 Forbidden Content - Length : 0 Date : Wed , 09 Nov 2016 18 : 36 : 40 GMT Server : Jetty ( 9.3.14.v20161028 ) Set - Cookie : rememberMe = deleteMe ; Path =/ ; Max - Age = 0 ; Expires = Tue , 08 - Nov - 2016 18 : 36 : 40 GMT

Secure it with User Authentication from Stormpath

Everything I’ve talked about here works with any Apache Shiro realm (A realm is a DAO for a given user store). Using the Stormpath Shiro integration automatically gets you all of the features you expect from Stormpath: SAML, Social Login, OAuth2, etc. Just replace shiro-servlet-plugin dependency with these:

<dependency> <groupId>com.stormpath.shiro</groupId> <artifactId>stormpath-shiro-servlet-plugin</artifactId> </dependency> <dependency> <groupId>com.stormpath.shiro</groupId> <artifactId>stormpath-shiro-jaxrs</artifactId> </dependency> 1 2 3 4 5 6 7 8 9 < dependency > < groupId > com . stormpath . shiro < / groupId > < artifactId > stormpath - shiro - servlet - plugin < / artifactId > < / dependency > < dependency > < groupId > com . stormpath . shiro < / groupId > < artifactId > stormpath - shiro - jaxrs < / artifactId > < / dependency >

You can then add your user, roles, and permissions directly into Stormpath. I’ll chat more about this next time, but if you cannot wait, take a look at the Stormpath JAX-RS example.

Learn More About Apache Shiro and Stormpath

This example shows that just a single annotation is all it takes to add authentication and authorization for your JAX-RS resource!

Next time, I’ll continue with this Stormtrooper example and add a AngularJS frontend into the mix! If you have questions on this example you can send them to Apache Shiro’s user list, me on Twitter, or just leave them in the comments section below!

To learn more, check out these posts: