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
@RestControllerAdviceexclusively handles runtime exceptions thrown within controller business logic.ErrorControllerintercepts 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
@ExceptionHandlermethods 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));
}
}