POC showing approach to remove boilerplate validations using custom annotations and wiring in business logic inside aspects that execute before methods annotated with created annotation.
We want certain APIs to execute only if the logged-in user's account is enabled in the database i.e is_enabled column is set to true (user-id claim extracted from JWT received in headers)
- Make dummy annotation with name CheckIfEnabled
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckIfEnabled {
}
- Create Aspect using @Before containing business logic for validation that runs before all controller methods annotated with @CheckIfEnabled
- We fetch the JWT from the controller method argument (We specifically name it header, can be placed in a constant class)
- We extract the user-id from the JWT
- We check if account of the user is enabled, if not we throw an exception which stops the controller method from being executed
@Before("execution(* *.*(..)) && @annotation(checkIfEnabled)")
public void checkIfEnabled(JoinPoint joinPoint, CheckIfEnabled checkIfEnabled) {
log.info("Running Is Enabled Check On Method {}() in {}.class", joinPoint.getSignature().getName(),
joinPoint.getSignature().getDeclaringType().getSimpleName());
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
AtomicInteger count = new AtomicInteger(0);
for (int i = 0; i < codeSignature.getParameterNames().length; i++) {
if (codeSignature.getParameterNames()[i].equalsIgnoreCase("header")) {
headerArgumentPosition = count.get();
break;
} else
count.incrementAndGet();
}
if (headerArgumentPosition != null) {
final var header = (String) joinPoint.getArgs()[headerArgumentPosition];
final var userId = jwtUtils.extractUserId(header.replace("Bearer ", ""));
if (!userRepository.existsByIdAndIsEnabled(userId, true))
throw new RuntimeException();
}
}
- We annotate the controllers with above annotation to apply the validation thus saving bolierplate/repeated if else statements inside the service layer
@CheckIfEnabled
@GetMapping(value = "/user/details", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.OK)
@Operation(summary = "Returns details of logged in user")
public ResponseEntity<?> userDetailRetreivalHandler(
@Parameter(hidden = true) @RequestHeader(required = true, name = "Authorization") final String header) {
return userService.retreiveDetails(jwtUtils.extractUserId(header));
}
It is a common approach to remove 'Bearer ' using .replace() or .substring inside the service layer and extract the required claim with an utiliy method. We can remove the 'Bearer ' String from the JWT using @Around aspect that modifies the arguments recieved by a method
- Install Java 16
- Install Maven
Recommended way is to use sdkman for installing both maven and java
Run the below commands in the core
mvn clean install
mvn spring-boot:run
server port is configured to 9090 (can be changed in application.properties file)
Go to the below url to view swagger-ui (API docs)
http://localhost:9090/swagger-ui.html