Did you know Apache Shiro is an easy-to-use security framework for authentication and authorization in Java applications?
Did you know
JBoss Arquillian makes testing
@Inject-enhanced Java code in a
CDI container very easy?
Making It Work Together
Okay, enough with the buzzwords. And sorry for the confusing article title, but really this is about an example of integrating 4 separate cool (and practical!) stuff:
- Shiro for security (authentication and authorization)
- Use of CDI interceptors so that you don't have to sprinkle your business logic code with orthogonal concerns
- Integration testing (actually it is unit testing inside an integration container) your application in Arquillian, demonstrating that it is just as easy as plain JUnit tests. "Look ma, no manual wiring!"(tm)
- How Maven project management build tool helps in doing the tasks above easier, no more searching for stuff and configuring classpaths
The Goal
The goal is making this very simple JavaBean class:
public class ContactManager {
String name = "Hendy Irawan";
public String getName() {
return name;
}
public void setName(String name) {
}
}
... "enterprise-ready".
How? By making sure it handles authentication and authorization of secured operations without changing a single character of the above code at all!
"You've got to be kidding me!" No, I'm not. The trick is that I will add the following exactly before that code:
@Secured @NamedResource("contact")
And the bean will get secured. The best part is you can do it with any bean you want, not just the above.
Introducing Shiro The Guardian
The awesome framework that will perform the job as our security guy is Apache Shiro.
Summon Shiro in the Maven project's pom.xml :
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.1.0</version>
</dependency>
Shiro is very configurable, but for this example I'll just use a basic INI style configuration with the following contents:
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
hendy = hendy, user
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
guest = view:*
user = view:*, edit:*
I think the above is pretty easy to understand. Create the Shiro SecurityManager that loads the above configuration with:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
...final String iniFile = "classpath:shiro.ini"; logger.info("Initializing Shiro INI SecurityManager using " + iniFile); securityManager = new IniSecurityManagerFactory(iniFile).getInstance(); SecurityUtils.setSecurityManager(securityManager);
It's now possible to check the permission by doing:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
...
Subject subject = SecurityUtils.getSubject();
try {
subject.checkPermission("edit:contact");
} catch (Exception e) {
logger.error("Access denied - {}: {}", e.getClass().getName(), e.getMessage());
throw e;
}
Wiring It All with CDI
I don't want to manually assign objects by doing setSomedependency(object) all over the place. I want to use CDI dependency injection:
which is provided by:
@Produces
public Subject getSubject() {
return SecurityUtils.getSubject();
}
You may argue that it's just as concise as:
Subject subject = SecurityUtils.getSubject();
However that code is not flexible, because it is tied directly to Shiro API. The use of dependency injection makes it easier if you want to change the Subject implementation to another (for example, during testing, a mock object).
Grab the JARs
Add the JBoss repository (btw, it's recommended to add this as a profile in your ~/.m2/settings.xml instead of adding directly to the project POM):
<repositories>
<repository>
<id>jboss-public-repository-group</id>
<name>JBoss Public Maven Repository Group</name>
<layout>default</layout>
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</snapshots>
</repository>
</repositories>
Here's the Maven dependencies:
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>1.0-SP4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.interceptor</groupId>
<artifactId>jboss-interceptors-api_1.1_spec</artifactId>
<version>1.0.0.Final</version>
<scope>provided</scope>
</dependency>
By the way, if you use
M2Eclipse plugin in Eclipse IDE, you don't have to worry about memorizing those because you can just right-click Project > Maven > Add Dependency and merrily search away the Maven repositories.
Using CDI Interceptors to Add Security Layer
Instead of putting the security code in the business logic (or in this case, entity) beans themselves, I'd like to use declarative security. First create an annotation that will serve as interceptor binding:
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@InterceptorBinding
public @interface Secured { }
Use it to secure the bean that needs it:
@Named @Secured
public class ContactManager { ...
Now implement the interceptor itself:
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Secured @Interceptor
public class SecurityInterceptor {
@Inject Subject subject;
@Inject SecurityManager securityManager;
Logger logger = LoggerFactory.getLogger(SecurityInterceptor.class);
@AroundInvoke
public Object interceptGet(InvocationContext ctx) throws Exception {
logger.info("Securing {} {}", new Object[] { ctx.getMethod(), ctx.getParameters() }); logger.debug("Principal is: {}", subject.getPrincipal());
final Class<? extends Object> runtimeClass = ctx.getTarget().getClass();
logger.debug("Runtime extended classes: {}", runtimeClass.getClasses());
logger.debug("Runtime implemented interfaces: {}", runtimeClass.getInterfaces());
logger.debug("Runtime annotations ({}): {}", runtimeClass.getAnnotations().length, runtimeClass.getAnnotations());
final Class<?> declaringClass = ctx.getMethod().getDeclaringClass();
logger.debug("Declaring class: {}", declaringClass);
logger.debug("Declaring extended classes: {}", declaringClass.getClasses());
logger.debug("Declaring annotations ({}): {}", declaringClass.getAnnotations().length, declaringClass.getAnnotations());
String entityName;
try {
NamedResource namedResource = runtimeClass.getAnnotation(NamedResource.class);
entityName = namedResource.value();
logger.debug("Got @NamedResource={}", entityName);
} catch (NullPointerException e) {
entityName = declaringClass.getSimpleName().toLowerCase(); // TODO: should be lowerFirst camelCase
logger.warn("@NamedResource not annotated, inferred from declaring classname: {}", entityName);
}
String action = "admin";
if (ctx.getMethod().getName().matches("get[A-Z].*"))
action = "view";
if (ctx.getMethod().getName().matches("set[A-Z].*"))
action = "edit";
String permission = String.format("%s:%s", action, entityName);
logger.info("Checking permission '{}' for user '{}'", permission, subject.getPrincipal()); try {
subject.checkPermission(permission);
} catch (Exception e) {
logger.error("Access denied - {}: {}", e.getClass().getName(), e.getMessage());
throw e;
}
return ctx.proceed();
}
}
At this point you may think: "OMG! so much code just to check authorization. Can't you shorten the code a little bit?"
And that is exactly why you don't want to litter your business code with security or other concerns. So you can write as complex or long code as you want in the interceptor and not worry about it when you write the business logic.
You may also find bugs in the implementation above or change the requirements (e.g. to use Spring Security instead of Apache Shiro). No sweat, just fix the interceptor (this is just one alternative, read below why), and all the beans that use it will have it right after save the interceptor .java source file (since Eclipse IDE will auto-build your project, right? ;-).
The last step is activating the interceptor itself in the META-INF/beans.xml file:
<?xml version="1.0" encoding="UTF-8"?>
xsi:schemaLocation="
<interceptors>
<class>id.co.bippo.security.SecurityInterceptor</class>
</interceptors>
</beans>
Interceptors will be ignored by CDI container unless it is listed in beans.xml. Now I said that to change the implementation of the interceptor is just one way you can change the behavior of an interceptor binding annotation. Another perfectly valid way is just to activate a different interceptor class that implements the same interceptor binding, for example:
<interceptors>
<class>id.co.bippo.security.SpringSecurityInterceptor</class>
</interceptors>
You may notice I defined another annotation that is @NamedResource :
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NamedResource {
String value();
}
My security implementation will check the permission using the value set by @NamedResource annotation, with the lowercase-name of the class used as fallback. The following:
@Named @Secured @NamedResource("contact")
public class ContactManager { ...
Tells the security interceptor to check the permission using "contact" as the resource name, not "contactmanager" inflected from the class name ContactManager.
From Great Power Comes Great Responsibility
Traditionally, running CDI unit tests using JUnit, outside a Java EE container means you either have to:
- Instantiate and configure a CDI implementation; or...
- Do it the old school POJO way --> someObject.setDependency(toAnother); all over the unit test
Unfortunately option #2 breaks down when you use other features than dependency injection, like interceptors.
And option #1 is just boring for your highly valuable time.
Arquillian Tames The Game
Of course, the movie won't be exciting unless you have a cool protagonist as your buddy:
JBoss Arquillian. Let it handle the boring responsibility of dealing with the container and JUnit/TestNG integration, while you
keep the power of easy unit testing and full CDI (and more!) features.
Summon the mighty Arquillian inside your pom.xml :
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<arquillian.version>1.0.0.Alpha5</arquillian.version>
<junit.version>4.8.2</junit.version>
</properties>
<dependencies>
<!-- Test Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian</groupId>
<artifactId>arquillian-junit</artifactId>
<version>${arquillian.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>weld-se-embedded-11</id>
<dependencies>
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>arquillian-weld-se-embedded-1.1</artifactId>
<version>${arquillian.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-core</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>el-impl</artifactId>
<version>2.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>0.9.28</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-core-bom</artifactId>
<version>1.1.0.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-ext</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
</profiles>
The above POM snippet will:
- depend on JUnit 4.8.2
- depend on Arquillian 1.0.0.Alpha5 (don't be deceived by the "alpha" qualifier. it's excellently usable already!)
- declare a weld-se-embedded-11 Maven profile that runs tests under Weld SE Embedded 1.1.0.Final CDI container. Note that Arquillian supports a whole host of other container configurations including GlassFish, JBoss, OpenWebBeans, etc.
I also made sure to use
SLF4J 1.6.1 and the powerful
Logback, but this is optional and you can replace it with slf4j-simple or log4j if you want.
Coding The Test
Fortunately, with a minor change, the test code is just plain JUnit 4 test case conventions you all know and love. (OK, some of you may love TestNG better, thankfully Arquillian also supports it)
import javax.inject.Inject;
import junit.framework.Assert;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.jboss.arquillian.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
@RunWith(Arquillian.class)
public class ContactManagerTest {
@Inject ContactManager cm;
@Inject Subject subject;
/**
* Since Arquillian actually creates JAR files under the covers, the @Deployment
* is your way of controlling what is included in that Archive. Note, each
* class utilized in your test case - whether directly or indirectly - must
* be added to the deployment archive.
*/
@Deployment
public static JavaArchive createTestArchive()
{
return ShrinkWrap.create(JavaArchive.class, "test.jar")
.addPackage("id.co.bippo.security")
//.addClasses(new Class[] {ContactManager.class, SecurityInterceptor.class, SecurityFacade.class})
.addAsManifestResource("META-INF/beans.xml");
}
@Test
public void guestCanView() {
subject.login(new UsernamePasswordToken("guest", "guest"));
Assert.assertEquals("Hendy Irawan", cm.getName());
}
@Test(expected=AuthorizationException.class)
public void guestCannotEdit() {
subject.login(new UsernamePasswordToken("guest", "guest"));
cm.setName("Pak Boss");
Assert.assertEquals("Pak Boss", cm.getName());
}
@Test
public void userCanEdit() {
subject.login(new UsernamePasswordToken("hendy", "hendy"));
cm.setName("Pak Boss");
Assert.assertEquals("Pak Boss", cm.getName());
}
}
There are only two things that make this JUnit test different than vanilla JUnit tests:
- Annotate the test class with @RunWith(Arquillian.class)
- Implement the public static @Deployment method. Declare all the classes (or packages) and files/resources that you need in the to test what you will deploy. It may take some time to get used to but after that you'll realize that it's so powerful. For example you can test how the beans behave with different beans.xml configuration! (putting the "C"ontext back in CDI!)
Launch The Test Away
Testing the project with Arqullian is as simple as:
mvn test -Pweld-se-embedded-11
Notice the profile argument. It means it's very easy to test your application in different containers at will just by changing the profile! Neatness to the Extreme! I'd say it's the definition of neatness.
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running id.co.bippo.security.ContactManagerTest
16 Apr 11 16:03:14 org.jboss.arquillian.impl.client.container.ContainerRegistryCreator getActivatedConfiguration
INFO: Could not read active container configuration: null
16:03:14 [main] INFO org.jboss.weld.Version - WELD-000900 1.1.0 (Final)
16:03:14 [main] INFO org.jboss.weld.Bootstrap - WELD-000101 Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.
16:03:15 [main] WARN o.j.i.util.InterceptionTypeRegistry - Class 'javax.ejb.PostActivate' not found, interception based on it is not enabled
16:03:15 [main] WARN o.j.i.util.InterceptionTypeRegistry - Class 'javax.ejb.PrePassivate' not found, interception based on it is not enabled
16:03:15 [main] INFO id.co.bippo.security.SecurityFacade - Initializing Shiro INI SecurityManager using classpath:shiro.ini
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Securing public java.lang.String id.co.bippo.security.ContactManager.getName() []
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Principal is: null
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime implemented interfaces: interface java.io.Serializable
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime annotations (2): [@id.co.bippo.security.Secured(), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring class: class id.co.bippo.security.ContactManager
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring annotations (3): [@id.co.bippo.security.Secured(), @javax.inject.Named(value=), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Got @NamedResource=contact
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Checking permission 'view:contact' for user 'null'
16:03:15 [main] ERROR i.c.b.security.SecurityInterceptor - Access denied - org.apache.shiro.authz.UnauthenticatedException: This subject is anonymous - it does not have any identifying principals and authorization operations require an identity to check against. A Subject instance will acquire these identifying principals automatically after a successful login is performed be executing org.apache.shiro.subject.Subject.login(AuthenticationToken) or when 'Remember Me' functionality is enabled by the SecurityManager. This exception can also occur when a previously logged-in Subject has logged out which makes it anonymous again. Because an identity is currently not known due to any of these conditions, authorization is denied.
16:03:15 [main] INFO o.a.s.s.m.AbstractValidatingSessionManager - Enabling session validation scheduler...
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Securing public java.lang.String id.co.bippo.security.ContactManager.getName() []
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Principal is: guest
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime implemented interfaces: interface java.io.Serializable
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime annotations (2): [@id.co.bippo.security.Secured(), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring class: class id.co.bippo.security.ContactManager
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring annotations (3): [@id.co.bippo.security.Secured(), @javax.inject.Named(value=), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Got @NamedResource=contact
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Checking permission 'view:contact' for user 'guest'
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Securing public void id.co.bippo.security.ContactManager.setName(java.lang.String) [Pak Boss]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Principal is: guest
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime implemented interfaces: interface java.io.Serializable
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime annotations (2): [@id.co.bippo.security.Secured(), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring class: class id.co.bippo.security.ContactManager
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring annotations (3): [@id.co.bippo.security.Secured(), @javax.inject.Named(value=), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Got @NamedResource=contact
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Checking permission 'edit:contact' for user 'guest'
16:03:15 [main] ERROR i.c.b.security.SecurityInterceptor - Access denied - org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [edit:contact]
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Securing public java.lang.String id.co.bippo.security.ContactManager.getName() []
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Principal is: hendy
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime implemented interfaces: interface java.io.Serializable
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime annotations (2): [@id.co.bippo.security.Secured(), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring class: class id.co.bippo.security.ContactManager
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring annotations (3): [@id.co.bippo.security.Secured(), @javax.inject.Named(value=), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Got @NamedResource=contact
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Checking permission 'view:contact' for user 'hendy'
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Securing public void id.co.bippo.security.ContactManager.setName(java.lang.String) [Pak Boss]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Principal is: hendy
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime implemented interfaces: interface java.io.Serializable
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime annotations (2): [@id.co.bippo.security.Secured(), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring class: class id.co.bippo.security.ContactManager
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring annotations (3): [@id.co.bippo.security.Secured(), @javax.inject.Named(value=), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Got @NamedResource=contact
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Checking permission 'edit:contact' for user 'hendy'
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Securing public java.lang.String id.co.bippo.security.ContactManager.getName() []
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Principal is: hendy
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime implemented interfaces: interface java.io.Serializable
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Runtime annotations (2): [@id.co.bippo.security.Secured(), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring class: class id.co.bippo.security.ContactManager
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring extended classes: {}
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Declaring annotations (3): [@id.co.bippo.security.Secured(), @javax.inject.Named(value=), @id.co.bippo.security.NamedResource(value=contact)]
16:03:15 [main] DEBUG i.c.b.security.SecurityInterceptor - Got @NamedResource=contact
16:03:15 [main] INFO i.c.b.security.SecurityInterceptor - Checking permission 'view:contact' for user 'hendy'
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.621 sec
Results :
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
The Example Project
Now I'd argue that wasn't hard... compared to all the manual work that Java developers had to go through if doing it conventionally.
Simply:
cd arquillian-shiro-example
mvn test -Pweld-se-embedded-11
And watch the magic unfold.
Conclusion
Java EE and its supporting technologies (like CDI) and tools (like Arquillian and Maven, note they are not specific to Java EE at all) continue to help developers become more productive by reducing unnecessary technical tasks.
To know more about Java EE, I highly recommend Java EE 7 Essentials: Enterprise Developer Handbook by Arun Gupta.
Do you think CDI / Arquillian / Shiro / Maven or this article helpful to you? Let me know what you have in mind. :-)