Implementing a CORS filter in Spring Boot

Implementing a Cross-Origin Resource Sharing (CORS) filter in Spring Boot lets a browser call an API on another origin. It is common for an Australian React or Vue storefront to use a separate Spring backend, especially when the public .com.au site and staff portal use different domains.

Spring Boot offers @CrossOrigin, MVC configuration, the servlet CorsFilter, and CorsConfigurationSource with Spring Security. Choose according to whether the policy is local to a controller or must cover authentication, errors, and every API route consistently.

A sound policy names trusted origins explicitly, restricts methods and headers, and decides whether cookies can be sent. It should expose only response headers the frontend reads. CORS governs browser access; it does not replace authentication, authorisation, CSRF defences, or input validation.

Australian deployments add practical considerations. Teams in Sydney and Melbourne often connect through corporate VPNs. Prices may include GST and Australian dollar values, while services such as Stripe, PayPal, ZIP, and Afterpay can return metadata used by checkout clients. The Privacy Act 1988 and Australian Privacy Principles remain relevant whenever an API handles personal information; CORS itself is not a privacy safeguard.

How browsers enforce cross-origin requests

The same-origin policy compares scheme, host, and port. A page at https://shop.acme.com.au cannot read a response from https://api.acme.com.au merely because both use .com.au. The browser sends the requesting origin and checks the server’s CORS headers.

Non-simple requests trigger a preflight OPTIONS request first. It asks permission for the HTTP method and headers, such as JSON content or an authorisation token. The server must approve both that check and the actual response; passing one does not automatically pass the other.

Choosing a Spring Boot strategy

Configuration location affects maintainability. A local annotation suits one endpoint, while centralised configuration applies consistently across APIs.

Approach Best fit Main benefit Main caution
@CrossOrigin Small controllers Fast, local setup Rules can drift
CorsRegistry MVC-only APIs Central Spring MVC setup May miss non-MVC paths
CorsConfigurationSource Spring Security APIs Runs before authorisation Must be exposed as a bean
CorsFilter Explicit servlet control Handles preflight early Can duplicate security handling

Most production APIs benefit from one shared source of truth. Avoid layering annotation, MVC, and filter policies unless their overlap is deliberate, because duplicate response headers obscure the effective rule.

Building a centralised CORS filter

Start with a shared CorsConfigurationSource. It keeps origin and header rules together and works with Spring Security or the servlet filter.

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of(
        "https://shop.acme.com.au",
        "https://admin.acme.com.au"
    ));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of(
        "Authorization", "Content-Type", "X-CSRF-TOKEN"
    ));
    config.setExposedHeaders(List.of("Location", "X-Correlation-Id"));
    config.setAllowCredentials(true);
    config.setMaxAge(3600L);

    UrlBasedCorsConfigurationSource source =
        new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

For an application without Spring Security, expose the source through Spring’s servlet filter:

@Bean
CorsFilter corsFilter(CorsConfigurationSource source) {
    return new CorsFilter(source);
}

allowCredentials is enabled for cookie-based sessions. If the frontend uses only a bearer token, disable it unless cookies are also sent. Credentials require an explicit origin rather than *, so list each HTTPS client exactly, including its port when relevant.

Keep http://localhost:3000 in development only. Expose Location only if JavaScript follows created resources, and X-Correlation-Id only if the client reads it.

Integrating CORS with Spring Security

Spring Security can own the policy. Keep the CorsConfigurationSource bean and add http.cors(Customizer.withDefaults()) to the existing SecurityFilterChain. Security locates the source and handles CORS before evaluating access rules.

Do not also register the standalone servlet filter in that secured application. Two processors can add duplicate headers and make browser diagnosis harder. The source-backed Security path suits token- and session-protected APIs.

Cross-origin cookies also need suitable attributes, commonly SameSite=None; Secure, plus a CSRF strategy. CORS approval is not proof of authorisation and does not weaken Spring Security’s CSRF protection.

Testing preflight and real requests

Test the preflight against the deployed API, not only localhost. Use the requesting origin and the method the browser will really call.

curl -i -X OPTIONS "https://api.acme.com.au/api/orders" \
  -H "Origin: https://shop.acme.com.au" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

A successful preflight normally returns 200 or 204, an Access-Control-Allow-Origin value matching the request, and the permitted methods and headers. The actual request must independently include matching CORS headers.

Inspect DevTools because curl does not enforce browser CORS rules. Check credentialed calls and error responses: missing headers on a 4xx or 5xx can hide server details from JavaScript. Repeat for staging and production, and check that a CDN or gateway is not caching preflight responses incorrectly.

Deployment and verification checklist

Define the policy with these checks:

Verify the release with these checks: