Implementing Cross-Origin Policies, API Documentation, Exception Management, and JWT Authentication in Spring Boot

Understanding and Resolving Cross-Origin Requests

Cross-Origin Resource Sharing (CORS) is triggered when a browser attempts to fetch resources from an origin differing in protocol, domain, or port from the current page. In decoupled frontend-backend architectures, this restriction enforced by the Same-Origin Policy requires explicit configuration. Spring Boot provides several declarative and programmatic approaches to manage these policies.

Approach 1: Registering a Global CorsFilter

Defining a CorsFilter bean centralizes cross-origin configuration across the entire application lifecycle.

@Configuration
public class CorsSetup {
    @Bean
    public FilterRegistrationBean<CorsFilter> globalCorsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("*"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(new CorsFilter(source));
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return registration;
    }
}

Approach 2: Implementing WebMvcConfigurer

Extending the MVC configuration registry offers a concise, annotation-driven alternative.

@Configuration
public class MvcCorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .maxAge(3600);
    }
}

Approach 3: Utilizing the @CrossOrigin Annotation

For granular control, this metadata annotation can be applied directly to controller classes or individual endpoints.

@RestController
@CrossOrigin(origins = "*")
public class ProductController {
    @GetMapping("/inventory")
    public String getInventory() {
        return "Inventory data payload";
    }
    
    @GetMapping("/restricted")
    @CrossOrigin(origins = "https://trusted-frontend.example.com")
    public String getRestricted() {
        return "Authenticated data payload";
    }
}

Approach 4: Manually Injecting Response Headers

Direct manipulation of the HttpServletResponse object allows low-level header injection on specific routes.

@GetMapping("/manual-cors")
public ResponseEntity<String> manualCorsEndpoint(HttpServletResponse httpResponse) {
    httpResponse.addHeader("Access-Control-Allow-Origin", "*");
    return ResponseEntity.ok("Data returned with manual CORS headers");
}

Approach 5: Custom Servlet Filter

A dedicated Filter implementation can intercept inbound requests and attach necessary CORS headers before controller execution.

@Component
public class CorsInterceptorFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse httpRes = (HttpServletResponse) res;
        httpRes.setHeader("Access-Control-Allow-Origin", "*");
        httpRes.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
        httpRes.setHeader("Access-Control-Max-Age", "3600");
        httpRes.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
        chain.doFilter(req, res);
    }
}

Integrating API Documentation with Knife4j

Automating RESTful endpoint documentation streamlines collaboration between development teams. Knife4j extends standard Swagger capabilities with enhanced UI components and export features.

Step 1: Add Dependencies

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
    <version>4.1.0</version>
</dependency>

Step 2: Configure the Documentation Bean

@Configuration
@EnableSwagger2WebMvc
public class ApiDocConfiguration {
    @Bean
    public Docket apiDocumentation() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(buildMetadata())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.bookstore.controller"))
                .paths(PathSelectors.any())
                .build();
    }
    
    private ApiInfo buildMetadata() {
        return new ApiInfoBuilder()
                .title("Library Management System API")
                .description("Comprehensive endpoints for managing book inventory and user sessions")
                .version("2.0.0")
                .build();
    }
}

Step 3: Essential Annotations

Decorate controllers, models, and methods to generate structured, readable documentation.

Annotation Purpose
@Api Describes the controller class and groups endpoints
@ApiModel Documents entity, DTO, or VO classes
@ApiModelProperty Provides field-level descriptions within models
@ApiOperation Explains the functionality of a specific endpoint
@RestController
@RequestMapping("/volumes")
@Api(tags = "Book Management Endpoints")
public class VolumeController {
    @Autowired
    private VolumeService volumeService;
    
    @GetMapping("/retrieve")
    @ApiOperation(value = "Fetch all available books")
    public ApiResponse<List<Volume>> getAll() {
        return ApiResponse.success(volumeService.findAll());
    }
}

Centralized Exception Management

Uncaught runtime failures disrupt client-side rendering. Implementing a unified error response format ensures consistent API behavior across the application.

Strategy 1: @RestControllerAdvice with @ExceptionHandler

This aspect-oriented approach intercepts exceptions originating from controller methods and transforms them into stendardized JSON payloads.

@RestControllerAdvice
@Slf4j
public class ApplicationExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ApiResponse<?> handleValidation(IllegalArgumentException ex, HttpServletResponse response) {
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        log.warn("Validation constraint violated: {}", ex.getMessage());
        return ApiResponse.error(4000, ex.getMessage());
    }
    
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleGeneric(Exception ex, HttpServletResponse response) {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        log.error("Unexpected system error", ex);
        return ApiResponse.error(5000, "Internal processing failure");
    }
}

Strategy 2: Implementing ErrorController

Spring Boot's routing infrastructure can be overridden by implementing the ErrorController interface. This catches infrastructure-level faults (e.g., 404, 500) before they reach the MVC dispatcher.

@RestController
@Slf4j
public class CustomErrorController implements ErrorController {
    private static final String ERROR_ROUTE = "/custom-error";
    
    @RequestMapping(ERROR_ROUTE)
    public ApiResponse<?> handleErrors(HttpServletRequest request, HttpServletResponse response) {
        int status = response.getStatus();
        log.info("Encountered error route with status: {}", status);
        return ApiResponse.error(status, "Service unavailable or resource not found");
    }
    
    @Override
    public String getErrorPath() {
        return ERROR_ROUTE;
    }
}

Comparing the Approaches

  • @RestControllerAdvice exclusively handles runtime exceptions thrown within controller business logic.
  • ErrorController intercepts infrastructure and routing-level failures before request dispatch.
  • Both mechanisms can operate concurrently: the advice handles application logic errors, while the controller manages routing/infrastructure faults.
  • The advice pattern supports fine-grained routing by defining multiple @ExceptionHandler methods tailored to specific exception hierarchies.

Securing the Application with Spring Security and JWT

A stateless authentication architecture relies on verifying credentials, issuing cryptographic tokens, and validating subsequent requests. We will implement a secure flow using JSON Web Tokens (JWT) synchronized with a Redis cache for session invalidation.

Prerequisites and Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

JWT Utility Configuration

@Configuration
public class JwtTokenService {
    public static final long TOKEN_LIFESPAN = 86400000; // 24 hours in ms
    public static final String SECRET_KEY = "SecureSigningKeyForProductionUse";
    
    public static String generateToken(AppUser principal) {
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject(principal.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + TOKEN_LIFESPAN))
                .claim("userId", principal.getId())
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }
    
    public static boolean validateToken(String token) {
        if (StringUtils.hasText(token)) {
            try {
                Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
                return true;
            } catch (JwtException | IllegalArgumentException e) {
                return false;
            }
        }
        return false;
    }
    
    public static Claims extractClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }
}

User Details Implementation

Spring Security requires a domain class implementing UserDetails to bridge application entities with security contexts.

@Data
public class AuthenticatedUser implements UserDetails, Serializable {
    private AppUser applicationUser;
    private List<AccessRight> assignedPermissions;
    
    public AuthenticatedUser(AppUser user) {
        this.applicationUser = user;
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return assignedPermissions.stream()
                .map(permission -> new SimpleGrantedAuthority(permission.getName()))
                .collect(Collectors.toList());
    }
    
    @Override
    public String getPassword() { return applicationUser.getPassword(); }
    @Override
    public String getUsername() { return applicationUser.getUsername(); }
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

Authentication Service Layer

@Service
@Slf4j
public class AuthenticationServiceImpl implements UserDetailsService {
    @Resource private UserRepo userRepo;
    @Resource private PermissionRepo permissionRepo;
    @Resource private StringRedisTemplate redisCache;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("Loading security context for: {}", username);
        AppUser user = userRepo.findByUsername(username);
        if (user == null) throw new UsernameNotFoundException("Invalid credentials");
        
        List<AccessRight> permissions = permissionRepo.findByUserId(user.getId());
        redisCache.opsForValue().set("perms:" + user.getId(), JSON.toJSONString(permissions), 1, TimeUnit.HOURS);
        
        AuthenticatedUser securityUser = new AuthenticatedUser(user);
        securityUser.setAssignedPermissions(permissions);
        return securityUser;
    }
}

Custom Login Authentication Filter

Intercepts credential submission, delegates verification to the authentication manager, and provisions tokens upon successful validation.

@Slf4j
public class LoginProcessingFilter extends UsernamePasswordAuthenticationFilter {
    private final StringRedisTemplate cacheClient;
    
    public LoginProcessingFilter(AuthenticationManager manager, StringRedisTemplate cache) {
        super(manager);
        this.cacheClient = cache;
        setFilterProcessesUrl("/api/auth/sign-in");
        setPostOnly(true);
    }
    
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth)
            throws IOException {
        log.info("Authentication successful for principal: {}", auth.getName());
        AuthenticatedUser principal = (AuthenticatedUser) auth.getPrincipal();
        String token = JwtTokenService.generateToken(principal.getApplicationUser());
        cacheClient.opsForValue().set("auth-token:" + principal.getApplicationUser().getId(), token, 1, TimeUnit.DAYS);
        res.setContentType("application/json;charset=UTF-8");
        res.getWriter().write(JSON.toJSONString(ApiResponse.success(token)));
    }
    
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException exception)
            throws IOException {
        log.warn("Authentication failed: {}", exception.getMessage());
        res.setContentType("application/json;charset=UTF-8");
        res.setStatus(401);
        res.getWriter().write(JSON.toJSONString(ApiResponse.error(4001, "Authentication failed")));
    }
}

Token Validation Filter

Executes on protected routes to verify JWT presence, cryptographic validity, and synchronization with the Redis session store.

@Slf4j
public class JwtVerificationFilter extends BasicAuthenticationFilter {
    private final StringRedisTemplate cacheClient;
    
    public JwtVerificationFilter(AuthenticationManager manager, StringRedisTemplate cache) {
        super(manager);
        this.cacheClient = cache;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        String token = req.getHeader("Authorization");
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        
        if (!JwtTokenService.validateToken(token)) {
            throw new AccessDeniedException("Invalid or expired token");
        }
        
        String userId = JwtTokenService.extractClaims(token).get("userId").toString();
        String storedToken = cacheClient.opsForValue().get("auth-token:" + userId);
        
        if (!token.equals(storedToken)) {
            throw new AccessDeniedException("Token session mismatch");
        }
        
        String permJson = cacheClient.opsForValue().get("perms:" + userId);
        List<AccessRight> permissions = JSON.parseArray(permJson, AccessRight.class);
        List<GrantedAuthority> authorities = permissions.stream()
                .map(p -> new SimpleGrantedAuthority(p.getName()))
                .collect(Collectors.toList());
        
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userId, token, authorities);
        SecurityContextHolder.getContext().setAuthentication(authToken);
        chain.doFilter(req, res);
    }
}

Security Configuration Assembly

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class SecurityChainConfiguration extends WebSecurityConfigurerAdapter {
    @Resource private UserDetailsService userDetailsService;
    @Autowired private StringRedisTemplate redisTemplate;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers("/api/auth/sign-in", "/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new LoginProcessingFilter(authenticationManagerBean(), redisTemplate))
                .addFilter(new JwtVerificationFilter(authenticationManagerBean(), redisTemplate));
    }
}

Tags: Spring Boot cors JWT Spring Security Knife4j

Posted on Sun, 28 Jun 2026 17:30:32 +0000 by akimm