@CrossOrigin

Allows browsers on other origins to call a controller or method, by configuring CORS headers and preflight handling.

Signature
@CrossOrigin(origins = "https://app.example.com", maxAge = 3600)
  • Added in Spring Boot Spring Framework 4.2

Parameters

ParameterTypeDescription
$valueoptionalString[]= {}Alias for origins.
$originsoptionalString[]= {}Allowed origins, such as "https://app.example.com". "*" allows any origin. If neither origins nor originPatterns is set, all origins are allowed.
$originPatternsoptionalString[]= {}Origin patterns with wildcards, such as "https://*.example.com". Unlike "*" in origins, these can be combined with credentials.
$allowedHeadersoptionalString[]= {}Request headers the browser may send. Defaults to all requested headers.
$exposedHeadersoptionalString[]= {}Response headers, beyond the simple ones, that browser scripts may read (for example Location).
$methodsoptionalRequestMethod[]= {}Allowed HTTP methods. Defaults to the methods the mapping supports.
$allowCredentialsoptionalString= """true" lets the browser send cookies and read the response. Not allowed with origins = "*". Not set by default.
$allowPrivateNetworkoptionalString= """true" permits requests from public sites to private network addresses, where the browser asks for it. Not set by default.
$maxAgeoptionallong= -1Seconds the browser may cache the preflight response. A negative value means the default of 1800 seconds.

Return value

n/aEnables CORS for the annotated controller or method: Spring answers preflight OPTIONS requests and adds Access-Control-* headers to matching responses from allowed origins.

Errors

allowCredentials = "true" together with origins = "*" is rejected with an IllegalArgumentException; use originPatterns instead.

Browsers block a script on https://app.example.com from reading responses from https://api.example.com unless the API says it is allowed. That permission system is CORS. @CrossOrigin turns it on for one controller or method: Spring answers the browser's preflight OPTIONS request and adds Access-Control-Allow-* headers to real responses.

CORS is enforced by browsers only. It does not protect your API from curl, scripts or other servers, so it is never a replacement for authentication.

Defaults

A bare @CrossOrigin allows every origin, all requested headers and the methods the mapping supports, with a 30-minute preflight cache and no credentials. That is fine for truly public, read-only data. For anything else, list the origins. Values on a method are combined with those on the class.

@CrossOrigin(origins = "https://www.example.com", maxAge = 3600)
class NewsletterController { ... }

Annotation or global configuration

The annotation suits one or two controllers that differ from the rest. For a whole API, configure it once in a WebMvcConfigurer with addCorsMappings, so a new controller cannot forget it. Keep allowed origins in configuration properties so each environment lists its own front end.

With Spring Security

The security filter chain runs before Spring MVC. Spring Boot's fallback chain picks up Spring MVC's CORS settings, but as soon as you declare your own SecurityFilterChain without http.cors(...), a preflight request (which carries no credentials) is rejected with 401 before @CrossOrigin is consulted, and the browser reports a CORS error. Call http.cors(...) in every chain that serves browser clients; it uses a CorsConfigurationSource bean, or Spring MVC's configuration when none is defined.

Mistakes people make

  • Credentials with *. Browsers forbid it and Spring rejects it. Use explicit origins or originPatterns.
  • Unreadable headers. Scripts cannot read Location or custom headers unless they are listed in exposedHeaders.
  • Debugging from curl. curl ignores CORS. Reproduce with the browser's network tab and look at the preflight response.

Example

Example · java
import java.util.List;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

record Newsletter(String email) {}

@RestController
@RequestMapping("/api/newsletter")
@CrossOrigin(origins = "https://www.example.com", maxAge = 3600)
public class NewsletterController {

    @GetMapping("/topics")
    List<String> topics() {
        return List.of("releases", "events");
    }

    // Method-level values are combined with the class-level ones.
    @PostMapping
    @CrossOrigin(origins = "https://shop.example.com", exposedHeaders = "Location")
    Newsletter subscribe(@RequestBody Newsletter body) {
        return body;
    }
}

When to use it

  • Letting a React app on its own domain call the API
  • Allowing a partner's site to read a public product feed
  • Sharing a session cookie between app.example.com and api.example.com

Frequently asked questions

Why does curl work but the browser reports a CORS error?

Only browsers enforce CORS. The browser sent a preflight OPTIONS request and the response lacked the right Access-Control headers, often because security rejected it.

Should I use @CrossOrigin or a global configuration?

Use global configuration for an API with one front end, and @CrossOrigin for the few endpoints that need a different policy.

More examples

Global CORS for the whole API

addCorsMappings applies one policy to every /api/** endpoint. Controllers no longer need the annotation, and a new endpoint cannot be left out.

Example · java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

// One place for the whole API instead of annotations on every controller.
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://app.example.com")
                .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE")
                .allowedHeaders("Content-Type", "Authorization")
                .exposedHeaders("Location")
                .maxAge(3600);
    }
}

CORS with Spring Security

http.cors(Customizer.withDefaults()) adds the CORS filter to the security chain and uses the CorsConfigurationSource bean, so preflight requests are answered before authentication runs.

Example · java
import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class SecurityConfig {

    // With Spring Security, CORS must run in the filter chain, before
    // authentication, or preflight OPTIONS requests are rejected with 401.
    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http.cors(Customizer.withDefaults())
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        return http.build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://app.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

Cookies across subdomains

allowCredentials needs a concrete origin in the response. originPatterns matches every subdomain and echoes the caller's exact origin, which browsers accept. Only use a wildcard if you control every subdomain, and keep CSRF protection on: credentialed cookies are exactly what CSRF abuses.

Example · java
import java.util.Map;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SessionInfoController {

    // Cookies need allowCredentials, and credentials cannot be combined with "*".
    // originPatterns allows a family of origins while echoing the exact one back.
    @CrossOrigin(originPatterns = "https://*.example.com", allowCredentials = "true")
    @GetMapping("/api/session")
    Map<String, Boolean> session() {
        return Map.of("active", true);
    }
}

Discussion

  • Be the first to comment on this lesson.