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>
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>
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>
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
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 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> getClasses() {
Set> classes = new HashSet<>();
// register Shiro
classes.add(ShiroFeature.class);
return classes;
}
@Override
public Set getSingletons() {
Set 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)
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"
},
...
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
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>
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:
- User Permissions with Apache Shiro and Stormpath
- The New RBAC: Resource-Based Access Control
- Apache Shiro Wildcard Permissions
- Other Stormpath + Apache Shiro examples
The post Protecting JAX-RS Resources with RBAC and Apache Shiro appeared first on Stormpath User Identity API.