Implementing Login Enforcement in Spring Boot 2.x with a Custom Annotation and HandlerInterceptor
Most endpoints in many applications require authentication. In Spring Boot 2.x, a common pattern is to decorate controllers or handler methods with a custom annotation and enforce that contract via a HandlerInterceptor.
This guide shows how to:
- Declare a marker annotation to indicate that an endpoint requires login
- Implement an interceptor that inspects the annotation at runtime
- Register the interceptor via WebMvcConfigurer
- Demonstrate usage with sample controllers
1. Declare a marker annotation
Place this annotation on either a controller class or an individual handler method to require authentication.
package com.acme.demo.security;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
2. Implement the interceptor
The interceptor checks whether the current handler is annotated with @LoginRequired (either on the method or on the declaring type). If authentication is required and no user is present in the session, the interceptor blocks the request.
package com.acme.demo.web;
import com.acme.demo.security.LoginRequired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final String SESSION_PRINCIPAL_KEY = "AUTH_USER";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// Non-handler resources (e.g., static content) are ignored
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod method = (HandlerMethod) handler;
boolean requiresAuth = hasLoginAnnotation(method);
if (!requiresAuth) {
return true;
}
HttpSession session = request.getSession(false);
Object principal = (session != null) ? session.getAttribute(SESSION_PRINCIPAL_KEY) : null;
if (principal != null) {
return true;
}
// Not authenticated -> block the request
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("UTF-8");
response.getWriter().write("Unauthorized: login required");
return false;
}
private boolean hasLoginAnnotation(HandlerMethod method) {
// Method-level annotation has priority
LoginRequired onMethod = method.getMethodAnnotation(LoginRequired.class);
if (onMethod != null) {
return true;
}
// Fallback: class-level annotation
LoginRequired onType = method.getBeanType().getAnnotation(LoginRequired.class);
return onType != null;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) {
// no-op
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) {
// no-op
}
}
3. Register the interceptor
Register the interceptor and define the paths where it applies. You may optionaly exclude public routes.
package com.acme.demo.config;
import com.acme.demo.web.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Autowired
public MvcConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/public/**",
"/error",
"/favicon.ico"
);
}
}
4. Example controllers
The sample below exposes a public endpoint and two protected endpoints. For a real application, a seperate login handler would put a user object/ID into the session under AUTH_USER.
package com.acme.demo.controller;
import com.acme.demo.security.LoginRequired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class SampleController {
// Open to everyone
@GetMapping("/open")
public String open() {
return "OK: no authentication required";
}
// Method-level protection
@LoginRequired
@GetMapping("/secure")
public String secure() {
return "OK: you are authenticated";
}
}
Class-level annotation also works; all handler methods in the class will require authnetication unless you isolate them to a different controller or design separate public routes.
package com.acme.demo.controller;
import com.acme.demo.security.LoginRequired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@LoginRequired
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/dashboard")
public String dashboard() {
return "OK: admin dashboard";
}
}
If you want to simulate login for testing, you can add a temporary endpoint that writes a principle into the session:
package com.acme.demo.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/login")
public String login(HttpSession session) {
session.setAttribute("AUTH_USER", "demo-user");
return "logged in";
}
@PostMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "logged out";
}
}
With these pieces in place, requests to routes annotated with @LoginRequired will be intercepted and verified against the session state.