EJB 3.0 introduced a standard interceptor model for session bean components. Interceptors are a great way of performing extra functionality. I will not go into detail of what Interceptors are. If you want to know more, google is your friend.
Prerequisites you should know:
- Seam has a few built in Interceptors, these include but are not limited to:
- BijectionInterceptor
- MethodContextInterceptor
- ConversationInterceptor
- SynchronizationInterceptor
- ConversationalInterceptor
- RemoveInterceptor
- SeamInterceptor
- SecurityInterceptor
- TransactionInterceptor
- EventInterceptor
- HibernateSessionProxyInterceptor
- @BypassInterceptors will skip Interceptors
First example
Imagine this simple model. A user is connected to one organization, and only the user connected to that Organization is allowed to view information about that organization.
Let's say you print out the users organization, and you have a page with detailed information about that organization like this:
<s:link view="myOrganization.xhtml" value="#{organizationAction.choose}">
<f:param name="id" value="#{organization.id}"/>
</s:link>
In the request, something like this will appear:
http://mydomain.com/myOrganization.xhtml?actionMethod=myOrganization.xhtml%3organizationAction%choose()&organizationId=3
And this is how your simple OrganizationAdmin.java component looks like:
@Name("organizationAdmin")
@Scope(ScopeType.CONVERSATION)
@Restrict("#{s:hasRole('user')}&;quot;)
public class OrganizationAction {
@RequestParameter
Long id;
@In
EntityManager entityManager;
Organization myOrganization;
@Begin
public void choose() {
if(id != null) {
this.myOrganization = (Organization) entityManager.find(Organization.class, id);
}
}
public Organization getMyOrganization() {
return this.myOrganization;
}
}
Now, imagine changing the organizationId in the request to show information about an organization you should initially not be allowed to see.
This is obviously very bad. We have restricted the component to role user, but another user can easily change the request to another id, and get hold of another organization.
To fix this, you can add this extra security check:
@Name("organizationAdmin")
@Scope(ScopeType.CONVERSATION)
@Restrict("#{s:hasRole('user')}")
public class OrganizationAction {
@RequestParameter
Long id;
@In
EntityManager entityManager;
Organization myOrganization;
@In(create = true)
User currentUser; //The current user logged in
@Begin
public void choose() {
if(id != null) {
this.myOrganization = (Organization) entityManager.find(Organization.class, id);
if(!currentUser.getOrganization().getId().equals(myOrganization.getId()))
throw new SecurityException("You are not allowed to view this page");
}
}
public Organization getMyOrganization() {
return this.myOrganization;
}
}
Voila! Now if an authorized user tries to "hack" the system, you will throw a SecurityException. This is fine and works just fine. However, it is cumbersome to do and you have to remember to do this everywhere where these exploits exists.
Wouldn't it be nice to just do this:
@Name("organizationAdmin")
@Scope(ScopeType.CONVERSATION)
@Restrict("#{s:hasRole('user')}")
@MySecurityAuthorized
public class OrganizationAction {
@RequestParameter
Long id;
@In
EntityManager entityManager;
Organization myOrganization;
@Begin
public void choose() {
if(id != null) {
this.myOrganization = (Organization) entityManager.find(Organization.class, id);
}
}
@AuthorizeOrganization
public Organization getMyOrganization() {
return this.myOrganization;
}
}
I for one think this is much easier to do. You can even reuse the @AuthorizeOrganization annotation to apply the same security other places. You only need to write the logic once.
Our first Interceptor
We can do this by using @Interceptor or @Interceptors. The latter is pre Java EE 5 compatible. I will cover both, but will show the latter in this example.
We start by creating our two annotations.
@AuthorizeOrganization
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthorizeOrganization {}
@MySecurityAuthorized/**
* This annotation can be added to any Seam component to hook in interception of our custom annotations.
* Important: Unless a custom specific @AuthorizeX annotation (like @AuthorizeOrganization) is also present, this annotation won't do anything
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@org.jboss.seam.annotations.intercept.Interceptors(MyInterceptor.class)
public @interface MySecurityAuthorized {}
Note the @Interceptors(MyInterceptor.class). It is here we register this annotation to the Interceptor MyInterceptor.
AnnotationHandler.java
Note that this is just an interface that all our custom annotations will inherit from
public interface AnnotationHandler {
public void preProceed();
public void postProceed(Object retObj);
public boolean isCurrentlyUsed();
}
BasicHandler.java
An abstract class all handler will extend from. Read javadoc and comments in code to get real understanding of what's going on. Much of this code is really not necessary for this simple example. But this is a good portion of the one we are using, and it is quite flexible.
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.interceptor.InvocationContext;
import org.jboss.seam.Component;
import org.jboss.seam.core.Events;
import org.jboss.seam.log.Logging;
/**
* Class that can be extended when creating new custom managed annotations.
* Add extending classes to MyInterceptor.
*
* @param <T> The Annotation that should be looked for
*/
public abstract class BasicHandler<T extends Annotation, U> {
private User user;
private Boolean isAdmin;
private boolean isMethodAnnotated;
private List<U> allInstanceParams = new ArrayList<U>();
private List<U> annotatedInstanceParams = new ArrayList<U>();
private List<Long> annotatedIdParams = new ArrayList<Long>();
private Class<U> authorizedClass;
/**
* This constructor parses the method signature for annotations,
* validated annotated types and
* if needed verifies the current User
* @param ic, the current InvocationContext
* @param annotationClazz, the class of the annotation to be scanned for
* @param authorizedClass, the class of the object to authorize
*/
@SuppressWarnings("unchecked")
public BasicHandler(InvocationContext ic, Class<T> annotationClazz, Class<U> authorizedClass) {
this.authorizedClass = authorizedClass;
//method annotation
Annotation annotation = ic.getMethod().getAnnotation(annotationClazz);
if (annotation != null) {
isMethodAnnotated = true;
}
//parameter annotations - why why why is there no Parameter class with methods for annotations, values, types?
Annotation[][] aaa = ic.getMethod().getParameterAnnotations();
for (int i = 0; i < aaa.length; i++) {
Object obj = ic.getParameters()[i];
if (isMethodAnnotated) {
allInstanceParams.add((U) obj);
}
Annotation[] aa = aaa[i];
for (int j = 0; j < aa.length; j++) {
Annotation a = aa[j];
if (a.annotationType().equals(annotationClazz) && ic.getParameters()[i] != null) {
//So we've found an annotated not null param
if (authorizedClass.isInstance(obj)) {
//instance of class to be authorized
annotatedInstanceParams.add((U) obj);
} else if (obj instanceof Long) {
//id of instance of class to be authorized
annotatedIdParams.add((Long) obj);
} else {
//Annotation put on erronous type
illegalObjectAnnotated((U) obj, annotationClazz);
}
//params.add(ic.getParameters()[i]);
}
}
}
//Verify user
if (isCurrentlyUsed()) {
getUser();
}
}
//public methods
public boolean isCurrentlyUsed() {
return (isMethodAnnotated || !annotatedInstanceParams.isEmpty() || !annotatedIdParams.isEmpty() ? true : false);
}
//protected methods
@SuppressWarnings("unchecked")
protected List<U> getAllReturnInstances(Object retObj) {
List<U> l = new ArrayList<U>();
if (isMethodAnnotated()) {
if (authorizedClass.isInstance(retObj)) {
//Simple object
l.add((U) retObj);
} else if (retObj instanceof Collection<?>) {
//Collections
for (Object obj : (Collection<?>) retObj) {
if (authorizedClass.isInstance(obj)) {
l.add((U) obj);
}
}
}
}
return l;
}
/**
* Authorizing Organization
* Role "admin" may access everything
* Other (like "user") must be within the organization to have privilege
* @param organization
*/
protected boolean authorize(Organization organization) {
if (organization != null) {
return authorizeUser(organization.getId().toString());
} else {
//null object need no authorization
return true;
}
}
/**
* Getting current logged in user
* @return Getting current logged in user
*/
protected User getUser() {
if (user == null) {
user = (User) Component.getInstance("currentUser", true);
if (user.getId() == null || user.getId() == 0) {
//annotation is present but we don't have an user
logAndThrow("User is null, must exsist to enable authentication");
}
}
return user;
}
/**
* Is user admin?
* @return
*/
protected boolean isAdmin() {
if (isAdmin == null) {
return isRoleInRoles(user.getRoles(), "admin");
}
return isAdmin;
}
/**
* Checks to see if the role is in the Collection<Role>
* If one of the roles appears in the Collection it returns true
*
* @param allroles - All the roles
* @param roles - varargs containing the roles to check for
*
* @return true if any of the role is found
*/
private boolean isRoleInRoles(Collection<Role> allroles, String ...roles) {
if (allroles == null || roles == null || roles.length == 0)
return false;
for (Role r : allroles)
for(String role : roles)
if (r.getName().equals(role))
return true;
return false;
}
protected boolean logAndThrow(String s) {
Logging.getLogProvider(this.getClass()).warn(s);
throw new SecurityException(s);
}
//private methods
private boolean authorizeUser(String orgId) {
User user = getUser();
if (isAdmin()) {
//Admin can do whatever
return true;
} else if (user.getOrganization().getId().equals(orgId)) {
//Correct organization, can also check role here if you want
return true;
} else {
//wrong organization, fail authorization
return logAndThrow("User '" + user.getUsername() + "' is not authorized to view this page");
}
}
private boolean isMethodAnnotated() {
return isMethodAnnotated;
}
private void illegalObjectAnnotated(U obj, Class<? extends Annotation> annotationClass) {
logAndThrow("Program error, annotation " + annotationClass.getSimpleName() + " cannot handle object of type " + obj.getClass().getSimpleName());
}
//Getters
public List<U> getAllInstanceParams() {
return allInstanceParams;
}
public List<U> getAnnotatedInstanceParams() {
return annotatedInstanceParams;
}
public List<Long> getAnnotatedIdParams() {
return annotatedIdParams;
}
}
@AuthorizeOrganizationHandler.java
/**
* This AnnotationHandler handles the @AuthorizeOrganization annotation.
* It authorizes a User for an Organization
*
*/
public class AuthorizeOrganizationHandler extends BasicHandler<AuthorizeOrganization, Organization> implements AnnotationHandler {
/**
* This constructor looks for @AuthorizeOrganization markers on method
* If any exists, User is validated directly.
* @param ic
*/
public AuthorizeOrganizationHandler(InvocationContext ic) {
super(ic, AuthorizeOrganization.class, Organization.class);
}
//Public methods
public void preProceed() {
//Method level - input parameters
for (Organization obj : getAllInstanceParams()) {
authorize(obj);
}
}
public void postProceed(Object retObj) {
//return value(s)
for (Organization obj : getAllReturnInstances(retObj)) {
authorize(obj);
}
}
}
Last but not least, our interceptor.
MyInterceptor.java
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
/**
* This interceptor handles method/param level annotations
* (Seam only provides Class (ElementType.TYPE) level annotation support)
* Thus this interceptor must be added to those beans that wish to use these annotations
* If we implement new annotations, their corresponding AnnotationHandler must be registered below
*
*/
public class MyInterceptor {
@AroundInvoke
public Object aroundInvoke(InvocationContext ic) throws Exception {
//gather custom annotation handlers
List<AnnotationHandler> ahs = new ArrayList<AnnotationHandler>();
addHandlerIfNeeded(new AuthorizeOrganizationHandler(ic), ahs);
//add new annotation handlers here - TODO: Use more clever generics instead
//handle pre proceed
for (AnnotationHandler ah : ahs) {
ah.preProceed();
}
//proceed
Object obj = ic.proceed();
//handle post proceed
Collections.reverse(ahs); //Reversing list in order to get same behavior as interceptors/filters
for (AnnotationHandler ah : ahs) {
ah.postProceed(obj);
}
return obj;
}
private void addHandlerIfNeeded(AnnotationHandler handler, List<AnnotationHandler> ahs) {
if (handler.isCurrentlyUsed()) {
ahs.add(handler);
}
}
}
Thats it! Now you can easily add new annotation and handlers and just add login in BasicHandler. If you look closely you will see that we automatically give all privileges to admin, but only check user's organization if they are role user. You can modify and do whatever fits your need.
Second Example
Finally I want to also mention the other Seam interceptor.
This is taken directly from the Seam forum (Link here). Which will print something like this:
284.94 ms 1 FooBean.getRandomDroplets() 284.56 ms 1 GahBean.getRandomDroplets() 201.60 ms 2 SohBean.searchRatedDoodlesWithinHead() 185.94 ms 1 FroBean.doSearchPopular() 157.63 ms 1 FroBean.doSearchRecent() 42.34 ms 1 FooBean.fetchMostRecentYodel() 41.94 ms 1 GahBean.getMostRecentYodel() 15.89 ms 1 FooBean.getNoOfYodels() 15.00 ms 1 GahBean.getNoOfYodels() 9.14 ms 1 SohBean.mainYodels() 1.11 ms 2 SohBean.trackHoorayEvent() 0.32 ms 1 FroBean.reset() 0.22 ms 43 NohBean.thumbPicture() 0.03 ms 18 FooBean.getMostRecentYodels() 0.01 ms 1 NohBean.profilePicture() 0.01 ms 1 FroBean.setToDefault() 0.01 ms 1 FroBean.getRecentMarker()
The first column contains the accumulated time spent in this method. The second column contains the number of times the method has been called, and the final column shows which method was called.
This Interceptor will measure how long each method will take to execute in milliseconds.
TimingInterceptor.java
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.jboss.seam.annotations.intercept.AroundInvoke;
import org.jboss.seam.annotations.intercept.Interceptor;
import org.jboss.seam.core.BijectionInterceptor;
import org.jboss.seam.core.ConversationInterceptor;
import org.jboss.seam.core.ConversationalInterceptor;
import org.jboss.seam.core.EventInterceptor;
import org.jboss.seam.core.MethodContextInterceptor;
import org.jboss.seam.core.SynchronizationInterceptor;
import org.jboss.seam.ejb.RemoveInterceptor;
import org.jboss.seam.ejb.SeamInterceptor;
import org.jboss.seam.intercept.InvocationContext;
import org.jboss.seam.persistence.HibernateSessionProxyInterceptor;
import org.jboss.seam.security.SecurityInterceptor;
import org.jboss.seam.transaction.TransactionInterceptor;
/**
* This interceptor will calculate how long it takes to execute each method
*
* Copied and modified from @link http://www.seamframework.org/Community/SeamPerformanceProblemRewardingWorkaround
*
*/
@Interceptor(
around = {
BijectionInterceptor.class,
MethodContextInterceptor.class,
ConversationInterceptor.class,
SynchronizationInterceptor.class,
ConversationalInterceptor.class,
RemoveInterceptor.class,
SeamInterceptor.class,
SecurityInterceptor.class,
TransactionInterceptor.class,
EventInterceptor.class,
HibernateSessionProxyInterceptor.class
})
public class TimingInterceptor {
public final static CallChain callChain = new CallChain();
@AroundInvoke
public Object timeCall(InvocationContext invocation) throws Exception {
long t0 = System.nanoTime();
try {
return invocation.proceed();
} finally {
long dt = System.nanoTime() - t0;
callChain.addInvocation(invocation, dt);
}
}
// -----------------------------------------------------------------------------
/**
* A call chain is the set of invocations on methods (annotated
* with MeasureCalls) that a request issued on its way through
* the application stack.
*/
public static class CallChain extends ThreadLocal<Map<Method, TimedInvocation>> {
@Override
protected Map<Method, TimedInvocation> initialValue() {
return new HashMap<Method, TimedInvocation>();
}
public void addInvocation(InvocationContext invocation, long dt) {
Map<Method, TimedInvocation> invocations = get();
Method method = invocation.getMethod();
if (!invocations.containsKey(method)) {
invocations.put(method, new TimedInvocation(invocation.getMethod(), dt));
} else {
TimedInvocation timedInvocation = invocations.get(method);
timedInvocation.anotherCall(dt);
}
}
public int totalNumberOfInvocations() {
Map<Method, TimedInvocation> invocations = get();
Collection<TimedInvocation> timedInvocationCollection = invocations.values();
int totCalls = 0;
for (TimedInvocation invocation : timedInvocationCollection)
totCalls += invocation.getCalls();
return totCalls;
}
}
}
TimedInvocation.java
import java.lang.reflect.Method;
import org.apache.commons.lang.StringUtils;
/**
* TimedInvocation is an invocation (i.e. a method call) which is being
* counted and timed.
*/
public class TimedInvocation implements Comparable<TimedInvocation> {
private long dt;
private int calls = 1;
private Method method;
public TimedInvocation(Method method, long dt) {
this.method = method;
this.dt = dt;
}
public long getDt() {
return dt;
}
public Method getMethod() {
return method;
}
/**
* The first column contains the accumulated time spent in this method.
* The second column contains the number of times the method has been called.
* The third column contains the method which was called
*/
public String toString() {
String className = method.getDeclaringClass().getName();
String shortendName = className.substring(method.getDeclaringClass().getPackage().getName().length() + 1);
String duration = StringUtils.leftPad(Double.valueOf(dt / 1e6) + " ms", 11);
String nCallStr = StringUtils.leftPad(String.valueOf(calls), 4);
return duration + nCallStr + " " + shortendName + "." + method.getName() + "()";
}
public void anotherCall(long dt) {
this.dt += dt;
calls++;
}
public int compareTo(TimedInvocation o) {
return -Long.valueOf(dt).compareTo(o.dt);
}
public int getCalls() {
return calls;
}
}
Lastly, we will create a simple filter that will print out the result. The filter is only installed in debug mode so that it doesn't run on production. Be aware that the interceptor will still run if you don't forget to uncomment the interceptor.
TimingFilter.java
/**
* This filter is used in conjunction with the TimingInterceptor. *
* It is defaulted to only work in debug mode (debug = true)
* To use this filter you need to set value to true
*
*/
@Startup
@Scope(ScopeType.APPLICATION)
@Name("timingFilter")
@BypassInterceptors
@Filter
@Install(debug = true)
public class TimingFilter extends AbstractFilter {
private static final LogProvider log = Logging.getLogProvider(TimingFilter.class);
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if (!(req instanceof HttpServletRequest)) {
chain.doFilter(req, res);
return;
}
TimingInterceptor.callChain.remove();
chain.doFilter(req, res);
Map<Method, TimedInvocation> invocations = TimingInterceptor.callChain.get();
for(TimedInvocation timedInvocation : invocations.values()) {
log.debug(timedInvocation.toString());
}
}
}
Finally create an annotation where you register the interceptor.
@MeasureCalls
/**
* This is an annotation that is used with the Interceptor TimingInterceptor,
* that will measure time it takes to execute each method in a seam component.
*
* The filter that uses this interceptor only works in debug mode and you will need to set value to true
* on the TimingFilter. Otherwize you will not see anything when running this in production or test.
* However, the interceptor will still run, so be "careful" where you put this annotation.
*
* Usage:
* @AutoCreate
* @Name("theBeanILikeToInvestigate")
* @MeasureCalls
* public class TheBeanILikeToInvestigare
*
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Interceptors(TimingInterceptor.class)
public @interface MeasureCalls {}
Just put @MeasureCalls on your component and all methods on that component will be invoked on.
No comments:
Post a Comment