adding authentication to spring controller methods with aspectj

Thu, Jan 23, 2014

The latest integration project I’m working on is to wire up the Blackboard VLE to the SITS student records system, mainly to synchronise courses and users. The first version I did was a couple of years ago, using the Blackboard SIS framework but as it’s a sysadmin thingy, file based and incapable of reporting errors programmatically, for this new version I decided to develop a building block with a nice REST interface.

Spring is great for this kind of stuff, so a few mins later I had a controller to hand, servicing requests for user information. A typical method is like the getUser one below:


@RequestMapping(value = "/user/{username}", method = RequestMethod.GET)
public ModelAndView getUser(@PathVariable String username) {
  ModelAndView mav;
  try {
    MatrixUser mxUser = blackboard.getUserInfo(username);
    mav = new ModelAndView("json/user");
    mxUser.setMode("get");
    mxUser.setStatus("ok");
    mav.getModel().put("user", mxUser);
  }
  catch(BlackboardException be) {
    mav = new ModelAndView("json/error");
    mav.getModel().put("error", be.getMessage());
  }
  return mav;
}

It just digs into Blackboard and gets the info but you’ll notice there isn’t any authentication. It’s wide open to anyone who knows the url, in this case /ws/user/USERNAME.

In the first version I’d added BASIC AUTH and queried the request headers for username/password in each REST method but there’s a much nicer way to do this using Aspects and PointCuts. i.e. Aspect Oriented Programming using AspectJ.

What you do is ‘wrap’ the getUser method using an Around advice with a PointCut:


@Aspect
public class WrapperAspect {
  @Around("execution(* com.codebrane.UserController.getUser(..))")
  public ModelAndView auth(ProceedingJoinPoint joinPoint) {
    HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

    if (checkCredentials(request)) {
      try {
        return (ModelAndView)joinPoint.proceed();
      }
      catch(Throwable theEnd) {
        ModelAndView mav = new ModelAndView("json/error");
        mav.getModel().put("error", theEnd.getMessage());
        return mav;
      }
    }
    else {
      ModelAndView mav = new ModelAndView("json/error");
      mav.getModel().put("error", "unauthorised");
      return mav;
    }
  }
}

@Around(“execution(* com.codebrane.UserController.getUser(..))“)

This is saying ‘for the getUser method in the com.codebrane.UserController class, use this advice’.

What happens is, the aspect gets hold of the HttpServletRequest that’s part of the REST call to getUser and hands it to the checkCredentials method in the aspect. That’ll return true/false based on the BASIC AUTH username/password in the request.

You can also get hold of the request by declaring it as a param in getUser and declaring the PointCut in such a way that AspectJ gives you access to it. That’s bad in this case though as getUser doesn’t need the request object. The only reason it would be declared as a param on getUser would be to pass it on to a method it knows nothing about. So in this case it’s better to let the aspect get the request itself.

If authentication is fine, the aspect ‘continues’ by handing control back to getUser:

return (ModelAndView)joinPoint.proceed();

and returning the result that getUser would normally return. In this case a ModelAndView with the user JSON.

However, if the authentication fails, the aspect ‘blocks’ access to getUser and instead returns its own ModelAndView with an appropriate JSON response saying ‘get lost’, or words to that effect! The Throwable is in there in case proceed() blows up.

But that’s a fair bit of hard wired config for just one PointCut. I’ll need lots of them for all the methods to be ‘advised’. Much cleaner to put the configuration in the Spring context file:


<beans xmlns="http://www.springframework.org/schema/beans"
       ...
      xmlns:aop="http://www.springframework.org/schema/aop"
      xsi:schemaLocation=" ...
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
        
  <aop:aspectj-autoproxy />

  <aop:config>
    <aop:pointcut id="userControllerPointCut" expression="execution(* com.codebrane.UserController.*(..))" />
    <aop:pointcut id="courseControllerPointCut" expression="execution(* com.codebrane.CourseController.*(..))" />

    <aop:aspect id="authenticationAspect" ref="wrapperAspect">
      <aop:around method="auth" pointcut-ref="userControllerPointCut" />
      <aop:around method="auth" pointcut-ref="courseControllerPointCut" />
    </aop:aspect>
  </aop:config>
  
  <bean id="wrapperAspect" class="com.codebrane.WrapperAspect">
    <property name="username" value="harrymcd"/>
    <property name="password" value="hohoho"/>
  </bean>
        
</beans>

Now, rather than have each controller injected with username/password to check and contain duplicated code for checkCredentials, the whole lot is now in the Aspect. The same method, auth, is now ‘advising’ all Controller methods, adding authentication functionality in a very nice and clean way.

That lets me clean up the Aspect itself now, removing the annotations as all the advice config is in applicationContext.xml:


public class WrapperAspect {
  public ModelAndView auth(ProceedingJoinPoint joinPoint) {
    HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

    if (checkCredentials(request)) {
      try {
        return (ModelAndView)joinPoint.proceed();
      }
      catch(Throwable theEnd) {
        ModelAndView mav = new ModelAndView("json/error");
        mav.getModel().put("error", theEnd.getMessage());
        return mav;
      }
    }
    else {
      ModelAndView mav = new ModelAndView("json/error");
      mav.getModel().put("error", "unauthorised");
      return mav;
    }
  }
}

So there we have it. Spring Controller method authentication and authorisation using Aspect Oriented Programming. Done!

References:

comments powered by Disqus