How to Use a Custom Annotation for Anonymous Access in Spring Security

This article explains two Spring Security configuration approaches, introduces a custom @IgnoreAuth annotation, and shows how to automatically whitelist annotated endpoints by scanning RequestMappingHandlerMapping, complete with code examples and workflow details.

Programmer DD
Programmer DD
Programmer DD
How to Use a Custom Annotation for Anonymous Access in Spring Security

In real projects using Spring Security, you often need to allow certain interfaces to be accessed anonymously, but modifying the security configuration each time is cumbersome.

Two ways to configure Spring Security

The first method configures configure(WebSecurity web) to ignore static resources, bypassing the Spring Security filter chain:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");
}

The second method configures configure(HttpSecurity http) to permit specific URLs while still passing through the filter chain:

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
        .authorizeRequests()
            .antMatchers("/hello").permitAll()
            .anyRequest().authenticated();
}

The main difference is that the first approach does not go through the filter chain, so SecurityContextHolder cannot retrieve the logged‑in user, while the second does.

Creating a custom annotation

@Target({ElementType.METHOD}) // target methods
@Retention(RetentionPolicy.RUNTIME) // runtime retention
@Documented // include in Javadoc
public @interface IgnoreAuth {
}

This annotation should be placed on methods that also have @RequestMapping.

Security configuration using the custom annotation

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Override
    public void configure(WebSecurity web) throws Exception {
        WebSecurity and = web.ignoring().and();
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
        handlerMethods.forEach((info, method) -> {
            // Methods annotated with @IgnoreAuth are allowed directly
            if (StringUtils.isNotNull(method.getMethodAnnotation(IgnoreAuth.class))) {
                // Handle different HTTP methods
                info.getMethodsCondition().getMethods().forEach(requestMethod -> {
                    switch (requestMethod) {
                        case GET:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.GET, pattern);
                            });
                            break;
                        case POST:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.POST, pattern);
                            });
                            break;
                        case DELETE:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.DELETE, pattern);
                            });
                            break;
                        case PUT:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.PUT, pattern);
                            });
                            break;
                        default:
                            break;
                    }
                });
            }
        });
    }
}

The above code uses RequestMappingHandlerMapping to retrieve all handler methods, checks for the @IgnoreAuth annotation, and registers the corresponding URL patterns to be ignored for each HTTP method.

RequestMappingHandlerMapping workflow

public void afterPropertiesSet() {
    this.initHandlerMethods();
}
protected void initHandlerMethods() {
    String[] beanNames = this.getCandidateBeanNames();
    for (String beanName : beanNames) {
        if (!beanName.startsWith("scopedTarget.")) {
            this.processCandidateBean(beanName);
        }
    }
    this.handlerMethodsInitialized(this.getHandlerMethods());
}
protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        beanType = this.obtainApplicationContext().getType(beanName);
    } catch (Throwable ex) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
        }
    }
    if (beanType != null && this.isHandler(beanType)) {
        this.detectHandlerMethods(beanName);
    }
}
protected boolean isHandler(Class<?> beanType) {
    return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
           AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class);
}
protected void detectHandlerMethods(Object handler) {
    Class<?> handlerType = (handler instanceof String) ?
        this.obtainApplicationContext().getType((String) handler) : handler.getClass();
    if (handlerType != null) {
        Class<?> userType = ClassUtils.getUserClass(handlerType);
        Map<Method, T> methods = MethodIntrospector.selectMethods(userType, method -> {
            try {
                return this.getMappingForMethod(method, userType);
            } catch (Throwable ex) {
                throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex);
            }
        });
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(this.formatMappings(userType, methods));
        }
        methods.forEach((method, mapping) -> {
            Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
            this.registerHandlerMethod(handler, invocableMethod, mapping);
        });
    }
}
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap();
public Map<T, HandlerMethod> getHandlerMethods() {
    this.mappingRegistry.acquireReadLock();
    try {
        return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
    } finally {
        this.mappingRegistry.releaseReadLock();
    }
}

Finally, the configuration iterates over the map of handler methods, checks for the @IgnoreAuth annotation, and adds the appropriate ignoring().antMatchers calls for each HTTP method.

The above diagram shows the inheritance hierarchy of RequestMappingHandlerMapping and related classes.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentCustom AnnotationSpring SecurityAnonymous Access
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.