What is CORS, and how do you enable cross-origin requests for a single Spring MVC controller or handler method?
answer
- SOP = scheme+host+port; browser-only
- @CrossOrigin on class or method
- bare default = all origins/headers, mapping's methods, 30min, no creds
- adds Access-Control-Allow-* headers
- server-to-server ignores CORS
basics
~20 sCORS (Cross-Origin Resource Sharing) is a browser rule that blocks JavaScript from calling a different origin unless the server allows it. In Spring you allow it by adding @CrossOrigin on a controller class or handler method.
solid answer
~40 sCORS is a browser security mechanism enforced by the Same-Origin Policy: a page from origin A (scheme+host+port) can't read responses from origin B unless origin B sends Access-Control-Allow-* headers permitting it. In Spring MVC, @CrossOrigin declares that permission. Put it on a @RestController class to allow all its methods, or on a single @GetMapping/@PostMapping method for finer scope. Method-level attributes are combined with class-level ones. By default (Spring's DEFAULT_CORS), it allows all origins, all headers, and the HTTP methods declared in the mapping, with a 30-minute preflight cache and credentials off. You narrow it with attributes like origins, methods, allowedHeaders, and maxAge. Under the hood Spring writes the response headers and short-circuits preflight OPTIONS requests — you don't set headers manually.
code
java · 17 lines@CrossOrigin(origins = "https://app.example.com", maxAge = 3600)
@RestController
@RequestMapping("/api/items")
class ItemController {
@GetMapping("/{id}")
Item get(@PathVariable long id) {
return service.find(id);
}
// method-level widens/overrides for this one endpoint
@CrossOrigin(origins = "https://admin.example.com")
@DeleteMapping("/{id}")
void delete(@PathVariable long id) {
service.delete(id);
}
}go deeper
Know CORS is a browser rule, an origin is scheme+host+port, and @CrossOrigin adds the allow headers.
Know the defaults of a bare @CrossOrigin and that class+method annotations combine.
Stress that non-browser clients ignore CORS and that @CrossOrigin is orthogonal to auth.
Frame @CrossOrigin as fine for exceptions but a governance smell at scale — centralize policy.
## The problem CORS solves Browsers enforce the **Same-Origin Policy (SOP)**: JavaScript running on a page whose *origin* is `https://app.example.com` may not read responses from a different origin. An **origin** is the triple **scheme + host + port** — `https://app.example.com`, `http://app.example.com`, and `https://app.example.com:8443` are three different origins. SOP is a browser thing; `curl`, Postman, and server-to-server calls ignore it entirely. **CORS (Cross-Origin Resource Sharing)** is the standard by which a server *opts in* to being called cross-origin. The server sends response headers such as `Access-Control-Allow-Origin` telling the browser "this origin is allowed to read my response." If the header is missing, the browser makes the request but **blocks the JavaScript from reading the response** (you see a CORS error in the console). The request may still have hit the server — CORS is about reading responses, not always about preventing the call. ## @CrossOrigin in Spring MVC `@CrossOrigin` is Spring's declarative way to emit those headers. It can go on: - a **controller class** (`@RestController`) — applies to every handler method, or - a **single handler method** (`@GetMapping`, etc.) — narrower scope. When both are present, Spring **combines** them (method attributes override/merge with class attributes). ### Default behavior (important) A bare `@CrossOrigin` with no attributes uses `CorsConfiguration.applyPermitDefaultValues()`-style defaults: - **All origins** allowed (`*`), - **All headers** allowed, - The **HTTP methods declared by the mapping** allowed (e.g. only GET if the method is `@GetMapping`), - **maxAge** = 1800 seconds (30 min) preflight cache, - **allowCredentials** = not set (credentials/cookies **not** allowed). ### Common attributes - `origins` / `originPatterns` — which origins may call. - `methods` — override the allowed HTTP methods. - `allowedHeaders` — request headers the client may send. - `exposedHeaders` — response headers JS is allowed to read (beyond the CORS-safelisted ones). - `allowCredentials` — `"true"` to permit cookies/Authorization on cross-origin calls. - `maxAge` — preflight cache seconds. ### Example ```java @CrossOrigin(origins = "https://app.example.com") @RestController @RequestMapping("/api/items") class ItemController { @GetMapping("/{id}") Item get(@PathVariable long id) { ... } @CrossOrigin(origins = "https://admin.example.com") @DeleteMapping("/{id}") void delete(@PathVariable long id) { ... } } ``` ## Gotchas - **`origins` values are strings, not annotations of trust** — `"*"` means every site on the internet can read the response. Only widen it for truly public, unauthenticated endpoints. - **@CrossOrigin does not disable authentication.** It only adds CORS headers. If Spring Security sits in front, Security must also permit the CORS handling (see the Security-integration question). - **It's per-controller/method** — for app-wide rules use global `addCorsMappings` instead, so the policy lives in one place. ## When to use Use `@CrossOrigin` for quick, endpoint-scoped exceptions. For anything real, prefer centralized global config so the policy is auditable and consistent.
- If a bare @CrossOrigin is on a @GetMapping method, which HTTP methods does it allow cross-origin?Only the ones the mapping declares — GET here (plus HEAD/OPTIONS handling). The default does not open POST/PUT/DELETE; you'd need to add them via the methods attribute or a broader mapping.
- Does a CORS error mean the request never reached the server?Not necessarily. For a simple request the call reaches the server and executes; the browser only blocks JavaScript from reading the response. For a request that triggers a preflight, the actual request is withheld if the preflight is rejected.
saying these in an interview costs you the question
- Thinking CORS is a server-side firewall that blocks the request (it's browser-enforced, and non-browsers ignore it)
- Assuming @CrossOrigin with no attributes also allows credentials/cookies
- Believing @CrossOrigin authenticates or authorizes anything
- Assuming bare @CrossOrigin on @GetMapping also permits POST/DELETE