Search code examples
javaspringaop

How to enforce Aspect implementation is loaded when its corresponding annotation is used?


I have a created an annotation that verifies whether certain security aspects are correct.

For example, @RequireClientCertificate, with an Aspect implementation RequireClientCertificateAspect that verifies whether the correct HTTP header is indeed passed in to the Spring REST controller.

This works totally fine, IF the RequireClientCertificateAspect is actually loaded, i.e. if its package is mentioned somewhere in @ComponentScan().

However, if someone forgets to add this package to @ComponentScan, or the aspect is moved to another package, or someone (accidentally) removes the package from @ComponentScan, the aspect bean isn't loaded, and the aspect is completely not applied.

I have this annotation in a common library, shared by several microservices, so it's easy for one of the microservices to accidentally get it wrong. In that case, no checking of the client certificate would be performed.

Question: How can I enforce that, if the @RequireClientCertificate annotation is used, its corresponding Aspect implementation is also loaded?


Simplified usage example:

@Controller
@RequestMapping(value = "/v1.0", produces = MediaType.APPLICATION_JSON_VALUE)
@RequireClientCertificate
public class SomeApiController {

    @ResponseBody
    @PostMapping("/get-token/")
    public ResponseEntity<Token> getToken() {
        return ResponseEntity.ok(...get token...);
    }
}

Simplified version of the aspect:

@Aspect
@Component
public class RequireClientCertificateAspect {
    @Around("execution(* (@RequireClientCertificate *).*(..))")
    public Object requireClientCertificateAspectImplementation(ProceedingJoinPoint joinPoint) throws Throwable {
        ... verify request header ...
        try {
            return joinPoint.proceed();
        finally {
            ... some other things I need to check ...
        }
    }
}

Things I've tried/considered:

I can detect 'usage' of the annotation by adding a static field with an initializer to the interface. For example:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RestFactoryGatewaySecurityContext {
    static public final boolean dummy = SomeClass.checkAspectIsLoaded();
}

However, such initializers are called very early, and I don't think Spring DI is 'up and running' far enough at that stage that I could even reliably determine whether the aspect bean is loaded.

Another option is to use @Autowired to inject the RequireClientCertificateAspect bean on the main app class explicitly. If somehow the bean isn't on the component scan, this will prevent Spring from instantiating the app.

So that does work, but requires someone to explicitly add this 'dummy' autowire, which in itself is easy to forget, in addition to being a bit 'ugly'.


Solution

  • If you use spring boot you can create your own starter.

    Create file META-INF/spring.factories:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyCustomConfiguration
    

    Then just add any validation you want to your configuration

    @Configuration
    public class MyCustomConfiguration{
    }
    

    You can @Autowired your RequireClientCertificateAspect into it, which will cause error if it isn't defined.

    You can create method with @PostConstruct and do any validation you want.

    If you went so far as creating custom starter, you can just initialize your bewns there.

    More about it you can read here