Introduction
When the requirements are given for a piece of software, they usually cover only a minor section of what the software should do and be. The requirements could be to create a chat room within a game for players to communicate. Most of the time the initial requirements are from the point of view of what users will be able to do with the application, but they do not answer a lot of questions that the developer of the system may have. How should the data storage look? What kind of authentication will this new application use? What events do we need to track? What information needs to be logged? Aspect-Oriented Programming (AOP) does NOT answer these questions, but it provides a way of thinking about application development to optimize the solutions to some of these problems.
Aspect-Oriented Programming is a software development paradigm for modularizing crosscutting concerns.
Crosscutting Concerns
Crosscutting concerns are functionalities that do not cleanly fit into a single layer but may be relevant across various sections of the code. Logging, event-tracking, authentication, and authorization are examples of such crosscutting concerns. For example, to authorize a user to a web application, multiple layers of the application will be concerned. The authentication credentials will need to be extacted from the HTTP request. The resolved credentials may need to be checked across a stored list in the database.
Modularization
Modularization is grouping related features together. Object-Oriented Programming (OOP) also promised to improve code modularization and has enabled the advancement of many useful design patterns without which modern software development is almost unthinkable. AOP is not a replacement for OOP. In fact, most of the more successful AOP implemtations are used in conjunction with OOP.
Implementations
Most of the popular programming languages today have AOP support natively or through libraries. However, there is no standard specification for what an Aspect Oriented Programming Language (AOPL) should or should not be able to do. If a language or specification allows proper modularization of crosscutting concerns, then it supports AOP. Therefore, there could be radical differences between the various AOPLs. For the sake of this article we will limit our discussion and examples to AspectJ, the Java-based AOPL.
The AspectJ Model
The anatomy of AOP as interpreted by the AspectJ contains the following parts:
- Join Point: A step in program execution where something distinct happens e.g. entering/exiting a method or throwing an exception.
- Pointcut: A specification of where an advice should be applied e.g. before execution begins of a particular method, or after an exception is thrown. Pointcut expressions defined in a pointcut expression language are used to identify pointcuts.
- Advice: The actual code that should be executed.
Case Study: Securing Endpoints
To demonstrate AOP, we will use the case study of a Spring Boot web service for centrally logging errors that occur on different systems. Note that the use case here is for the purpose of demonstration. Spring Security should always be the first point of call for Spring projects. However, I have also experienced that in many cases, when retrofitting existing projects with Spring Boot, Spring Security does not always seamlessly cover existing requirements while maintaining the interface.
Problem Definition
The requirements can be summarized in the following points.
- The webservice has protected endpoints for POSTing logs or GETting a list of logs.
- The endpoint clients are individual games or services.
- Each individual client has its own authentication credentials.
- Clients should only be able to access their own logs.
- Some other services may be able to aggregate logs accross all games.
Step 0: Unsecured Webservice Endpoint
The following snippet shows a stripped down version of the controller that supports the required actions of creating and retrieving logs.
@RestController @RequestMapping(path = "/games/{gameName}/logs") public class GameLogController { private final GameLogService gameLogService; // other services and constructors omitted for simplicity @RequestMapping( method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE ) public ResponseEntity<LogResponse> createLog( @PathVariable final String gameName, @RequestBody final CreateLogRequest request) { LogResponse response = gameLogService.createLog(request); return ResponseEntity.ok(response); } @RequestMapping( method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE ) public ResponseEntity<List<LogResponse>> getLogs(@PathVariable final String gameName) { List<LogResponse> responseList = gameLogService.getLogs(gameName); return ResponseEntity.ok(responseList); } }
As mentioned earlier, Spring Boot heavily uses AOP itself. Therefore, once the proper dependencies are included in a project, one has to merely add the appropriate annotations, and additional functionality is added to otherwise simple Java code. In the snippet above @RestController is a marker annotation. When the program is run, a proxy is created around certain classes and the appropriate advices get interleaved.
Step 1: Specifying the Join point
The first step to securing the endpoints in an aspect-oriented way, would be to add a join point. This is done by adding a marker annotation to the methods that need to be secured. In this case we create an annotation called @SecurityRole which accepts a collection of roles allowed to access to access the endpoint. (How the additional functionality is added to the annotated method is explained in Step 3: weaving)
@Retention(RetentionPolicy.RUNTIME) public @interface SecurityRole { AuthorizationRole[] value() default {AuthorizationRole.GAME}; }
The public methods (actions) in the controller are then annotated with this annotation.
@RequestMapping( ... ) @SecurityRole(Role.GAME) public ResponseEntity<LogResponse> createLog(...) { ... } @RequestMapping( ... ) @SecurityRole({Role.GAME, Role.SERVICE}) public ResponseEntity<List<LogResponse>> getLogs(...) { ... }
- The @SecurityRole annotation has been added to each of the methods.
- The @SecurityRole allows the specification of the list of Roles that can access the endpoint.
Step 2: Defining the Advice
The code below shows the Advice with comments explaining what each line does.
@Aspect @Component public class AuthorizationAspect { private final AuthorizationService authorizationService; public AuthorizationAspect(final OauthConsumerService oauthConsumerService) { this.oauthConsumerService = oauthConsumerService; } @Around("@annotation(securityRole))") public Object authorizeRequest(final ProceedingJoinPoint proceedingJoinPoint, final SecurityRole securityRole) throws Throwable { // Retrieve authentication parameters from HTTP request header final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); final String authorizationHeader = request.getHeader("Authorization"); // Delegate authentication to appropriate service final boolean isAuthenticated = oauthConsumerService.authenticate(authorizationHeader); // Throw exception if authentication fails if (!isAuthenticated) { throw new ForbiddenException(authorizationHeader); } // Continue with program execution if authentication is successful return proceedingJoinPoint.proceed(arguments); } }
From the code above shows what a very simple authentication sequence could look like. More complex actions such as verifying if the role of the user can access the requested action, and also injecting the authenticated user into the method can be performed. But these won’t be covered in this article :).
Step 3: Weaving
At this point, you’re probably wondering how you would get the program execution to jump from the business logic to the Aspect. This is done by the AOP framework. Developers actually don’t need to do anything else once the join points and advices are properly defined. This process is called Weaving. Many modern AOP frameworks provide runtime weaving, but compile-time weaving is quite popular as well.
Justification
Separation of Concerns
From the example above, we can see how AOP can lead to clearer boundaries in coding. To imbue applications with certain “cross-cutting” functionalities such as authentication, logging, event-tracking, etc, developers do not need to clutter business logic with additional services or use convoluted inheritance heirarchies. This allows developers writing or reading sections of code pertaining to business logic to actually focus on that logic.
Extensibility
AOP enables developers to delay certain decisions to later points in the lifecycle of a project. For instance, the specific mechanisms for adding metrics to a particular method could be added at any point in the development process. Need to check how fast it takes to respond to requests, or how many hits are received in a particular time period? This can be achieved without touching the actual class itself, but by targeting it with the appropriate join point.
Limitations of AOP
Testability
The nature of AOP makes certain kinds of testing difficult. Writing adequate unit tests can be difficult or impossible if certain functionalities only get added at runtime when the AOP framework gets loaded. This may necessitate complicated and contrived tests to cover what would otherwise be straightforward behaviours in the application.
Side Effects
Since the code that gets executed at certain points of the program may not be immediately discerned by merely reading the source code at the point of execution, troubleshooting can be difficult and frustrating. Furthermore, individuals may update the business code and name methods, or use annotations covered by certain aspects and therefore introduce unwanted and unexpected behaviours into their code.
Conclusion
While Aspect-oriented Programming has several benefits, like all other software paradigms it should be used carefully. AOP in particular, should be used only when absolutely necessary. Developers applying AOP should ask themselves whether the benefits of extensibility and modularization outweigh the increased testing complexity and the risk of side effects.