乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

CrossOrigin

本章聚焦 Spring MVC 的跨域请求处理。现代 Web 应用普遍采用前后端分离架构,前端页面部署在一个域名(如 http://localhost:3000),后端 API 部署在另一个域名(如 http://api.feixiang.com)。浏览器的同源策略会阻止这种跨域 AJAX 请求,而 @CrossOrigin 注解正是 Spring MVC 提供的细粒度跨域解决方案。

本章与全局的关系:前面章节讲解了 WebMvcConfigurer 的全局跨域配置,本章讲解方法级和类级的细粒度跨域控制。两者互补,共同构成完整的 CORS 策略体系。


定义与作用

@CrossOrigin 是 Spring MVC 提供的跨域注解,标注在 Controller 类或方法上,允许指定来源(origin)的浏览器发起跨域 AJAX 请求。它通过设置 CORS(Cross-Origin Resource Sharing)响应头,告诉浏览器"我允许这个域名的页面访问我"。

核心原理:浏览器在发送跨域请求前,会先发送一个 Preflight 预检请求(OPTIONS 方法),询问服务器是否允许跨域。服务器返回的响应头中包含允许的域名、方法、头信息等。浏览器根据响应决定是否发送真正的请求。

生活类比:公司访客登记系统

想象飞翔科技的大楼门禁:

  • 正常情况下,只有本大楼员工(同源)能自由进出
  • 外公司人员(跨域)想进来办事,需要提前申请访客许可
  • @CrossOrigin 就像"访客许可证"——你可以指定"允许哪家公司的人来访"(origins)、"允许他们办什么事"(methods)、"允许他们带什么物品"(headers)、"许可证有效期多久"(maxAge)
  • 没有许可证?保安(浏览器)直接拦在门外

关键认知:CORS 是服务器授权 + 浏览器执行的安全机制。服务器通过响应头表示"我同意",浏览器负责"强制执行"。如果服务器说同意但浏览器不执行,或者浏览器执行但服务器不同意,跨域请求都会失败。


核心原理

跨域请求完整流程

简单请求 vs 预检请求:

类型条件流程
简单请求GET/HEAD/POST + 标准头 + 无自定义头直接发送,服务器返回 Access-Control-Allow-Origin
预检请求PUT/DELETE/PATCH + 自定义头 + 非标准 Content-Type先发送 OPTIONS 预检,通过后再发真实请求

@CrossOrigin 生效位置

合并规则:方法级 @CrossOrigin 的属性覆盖类级 @CrossOrigin 的同名属性。如果方法级未指定某属性,则继承类级的该属性。


适用位置与常用属性

@CrossOrigin 可以标注在类和方法上:

属性类型默认值说明
origins / valueString[]*(允许所有)允许的来源域名,如 "http://localhost:3000"
allowedHeadersString[]*(允许所有)允许的请求头,如 "X-Token", "Content-Type"
methodsRequestMethod[]同 @RequestMapping允许的 HTTP 方法,如 GET, POST
exposedHeadersString[]无允许客户端访问的响应头
allowCredentialsString""是否允许携带 Cookie,"true" 或 "false"
maxAgelong-1(不缓存)预检请求缓存时间(秒)

标注位置

位置作用范围示例
类上该类所有方法@CrossOrigin(origins = "http://localhost:3000")
方法上仅该方法@CrossOrigin(origins = "*", maxAge = 3600)
类 + 方法合并配置,方法优先类设 origins,方法设 maxAge

完整示例

场景

飞翔科技员工管理系统的前端由黄俪开发,部署在 http://localhost:3000。后端 API 部署在 http://localhost:8080。需要配置 CORS 允许前端跨域访问。

项目结构

employee-web/
├── src/main/java/
│   └── com/feixiang/web/
│       └── controller/
│           └── EmployeeController.java

Controller 实现

// EmployeeController.java
package com.feixiang.web.controller;

import com.feixiang.web.dto.EmployeeDTO;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/api/employees")
@CrossOrigin(origins = "http://localhost:3000", allowedHeaders = "*", allowCredentials = "true")
public class EmployeeController {

    @GetMapping
    public List<EmployeeDTO> list() {
        EmployeeDTO e1 = new EmployeeDTO();
        e1.setName("张三");
        e1.setDepartment("研发部");
        
        EmployeeDTO e2 = new EmployeeDTO();
        e2.setName("李四");
        e2.setDepartment("产品部");
        
        return Arrays.asList(e1, e2);
    }

    @PostMapping
    @CrossOrigin(origins = "*", maxAge = 3600)  // 方法级覆盖类级的 origins
    public EmployeeDTO add(@RequestBody EmployeeDTO dto) {
        dto.setName(dto.getName() + "-已保存");
        return dto;
    }

    @DeleteMapping("/{id}")
    @CrossOrigin(origins = "http://localhost:3000", methods = RequestMethod.DELETE)
    public String delete(@PathVariable Long id) {
        return "删除成功: " + id;
    }
}

HTTP 请求示例 1:简单跨域 GET 请求

前端 JavaScript:

fetch('http://localhost:8080/api/employees', {
    method: 'GET',
    credentials: 'include'
});

对应的 curl:

$ curl -X GET http://localhost:8080/api/employees \
  -H "Origin: http://localhost:3000" \
  -v

响应:

< HTTP/1.1 200
< Access-Control-Allow-Origin: http://localhost:3000
< Access-Control-Allow-Credentials: true
< Vary: Origin
< Content-Type: application/json
<
[{"name":"张三","department":"研发部"},{"name":"李四","department":"产品部"}]

流程解析:

  1. 浏览器发现请求是跨域的(Origin 与目标域名不同)
  2. 这是 GET 请求,属于简单请求,直接发送
  3. 服务器返回 Access-Control-Allow-Origin: http://localhost:3000
  4. 浏览器检查响应头,发现 Origin 被允许,把数据交给前端 JS

HTTP 请求示例 2:预检请求(PUT + 自定义头)

前端 JavaScript:

fetch('http://localhost:8080/api/employees', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-Token': 'abc123'
    },
    body: JSON.stringify({name: '王五', department: '测试部'}),
    credentials: 'include'
});

对应的 curl(模拟预检):

$ curl -X OPTIONS http://localhost:8080/api/employees \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: X-Token" \
  -v

预检响应:

< HTTP/1.1 204
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET,HEAD,POST
< Access-Control-Allow-Headers: X-Token
< Access-Control-Max-Age: 3600

真实请求:

$ curl -X POST http://localhost:8080/api/employees \
  -H "Origin: http://localhost:3000" \
  -H "Content-Type: application/json" \
  -H "X-Token: abc123" \
  -d '{"name":"王五","department":"测试部"}'

响应:

{"name":"王五-已保存","department":"测试部"}

流程解析:

  1. 浏览器发现 POST + 自定义头 X-Token,触发预检请求
  2. 发送 OPTIONS 询问服务器是否允许
  3. 服务器根据 @CrossOrigin 配置返回允许的头和方法
  4. 浏览器缓存预检结果(3600 秒内不再重复预检)
  5. 发送真实的 POST 请求

@CrossOrigin vs CorsRegistry 全局配置对比

对比维度@CrossOrigin(注解)CorsRegistry(全局配置)
配置位置Controller 类/方法上WebMvcConfigurer.addCorsMappings
粒度细粒度,按接口控制粗粒度,按路径模式控制
灵活性与业务代码耦合与业务代码解耦
维护性分散在各 Controller集中在一处
适用场景个别接口特殊跨域需求整个项目统一跨域策略
优先级方法级 > 类级与 @CrossOrigin 共存时,注解优先

推荐实践

架构师白歌在飞翔科技制定了以下 CORS 策略:

// WebConfig.java —— 全局兜底配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}
// AdminController.java —— 特殊接口额外限制
@RestController
@RequestMapping("/api/admin")
@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")
public class AdminController {
    
    @DeleteMapping("/users/{id}")
    @CrossOrigin(origins = "http://localhost:3000", methods = RequestMethod.DELETE, maxAge = 7200)
    public String deleteUser(@PathVariable Long id) {
        return "删除用户: " + id;
    }
}

策略说明:

  • 全局配置覆盖 90% 的接口,维护简单
  • 敏感接口(如删除操作)用 @CrossOrigin 额外限制方法和缓存时间
  • 两者共存时,@CrossOrigin 的属性覆盖全局配置的同名属性

易错场景与面试考点

误区一:origins = "*" 与 allowCredentials = "true" 同时使用

错误代码:

@CrossOrigin(origins = "*", allowCredentials = "true")

错误现象:浏览器报错:The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

纠正:当需要携带 Cookie(allowCredentials = "true")时,origins 不能为 *,必须显式指定具体的域名:

@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")

这是浏览器的安全限制,不是 Spring 的限制。

误区二:CORS 配置正确但请求仍失败

排查清单:

  1. 检查是否是简单请求:如果是非简单请求,确认 OPTIONS 预检是否通过
  2. 检查响应头是否包含:Access-Control-Allow-Origin 必须存在且值正确
  3. 检查是否被拦截器拦截:如果预检请求被 LoginInterceptor 拦截返回 401,浏览器会报 CORS 错误(实际是先被拦截器拦了)
  4. 检查是否被 Filter 修改:某些安全 Filter 可能会在响应头之后追加头,导致 CORS 头丢失

关键经验:看到 CORS 错误时,先 curl 测试服务器是否返回了正确的 CORS 头。如果 curl 有头但浏览器报错,检查是否被拦截器/Filter 干扰。

误区三:maxAge 配置不生效

错误认知:"我设置了 maxAge = 3600,但浏览器每次还是发 OPTIONS。"

纠正:maxAge 只控制预检请求的缓存。简单请求(GET/POST)不需要预检,所以不涉及 maxAge。另外,浏览器对 maxAge 有上限(如 Chrome 最大 7200 秒),超过上限会被截断。

面试高频:CORS 和 JSONP 的区别

对比维度CORSJSONP
原理服务器响应头授权利用 <script> 标签不受同源限制
支持方法所有 HTTP 方法仅 GET
安全性高(服务器控制)低(XSS 风险)
现代推荐✅ 首选❌ 已淘汰

标准回答:CORS 是 W3C 标准,通过服务器响应头实现跨域,支持所有 HTTP 方法,安全性高。JSONP 是历史 hack 方案,利用 script 标签绕过同源策略,只支持 GET,存在 XSS 风险,现代项目应使用 CORS。

面试高频:Spring MVC 如何处理 OPTIONS 预检请求

标准回答:Spring MVC 的 DispatcherServlet 接收到 OPTIONS 请求后,会查找匹配的 HandlerMapping。如果目标 Controller/方法上有 @CrossOrigin,AbstractHandlerMethodMapping 会构造一个特殊的 Handler(PreFlightHandler),直接返回 CORS 响应头,不执行实际的 Controller 方法。


小结

@CrossOrigin 是 Spring MVC 提供的细粒度跨域注解,通过设置 CORS 响应头,允许指定来源的浏览器发起跨域 AJAX 请求。它可以标注在 Controller 类上(影响所有方法)或方法上(仅影响该方法),方法级配置覆盖类级配置。与 WebMvcConfigurer 的全局 CORS 配置相比,@CrossOrigin 更适合个别接口的特殊跨域需求。

本章与全局的关系:本章讲解了方法级跨域控制。下一章"文件上传与异常处理"将讲解如何处理 multipart 文件上传请求,以及如何在 Controller 层面处理异常。

上一页
WebMvcConfigurer
下一页
登录验证实战