从入门到熟悉跨域(CORS)
目录
跨域简介
什么是跨域
跨域(Cross-Origin Resource Sharing, CORS)是指浏览器出于安全考虑,阻止网页向不同源(协议、域名、端口)的服务器发起请求的一种安全机制。当网页试图访问不同源的资源时,浏览器会执行同源策略检查,如果不符合同源策略,则会阻止请求。
同源策略
同源策略要求:
协议相同: http/https
域名相同: example.com
端口相同: 80/443/8080等
跨域请求示例
跨域场景:
- 前端: http://localhost:3000
后端: http://localhost:8080 (端口不同)
- 前端: https://www.example.com
后端: http://api.example.com (协议不同)
- 前端: http://example.com
后端: http://sub.example.com (子域名不同)
跨域的影响
影响范围:
- AJAX请求被阻止
- Fetch API请求失败
- WebSocket连接失败
- 图片、字体等资源加载失败
- 表单提交可能被阻止
跨域流程简介图
简单跨域请求流程
预检请求(Preflight)流程
跨域请求详细流程
同源策略检查流程
跨域的类型
1. 简单跨域请求
简单请求的条件
简单请求要求:
- 请求方法: GET、POST、HEAD
- 请求头: 只能是简单头部
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
简单请求示例
// 简单跨域请求
fetch('http://api.example.com/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
// 简单表单提交
const formData = new FormData();
formData.append('name', 'John');
fetch('http://api.example.com/submit', {
method: 'POST',
body: formData
});
2. 复杂跨域请求
复杂请求的特征
复杂请求特征:
- 请求方法: PUT、DELETE、PATCH等
- 自定义请求头
- Content-Type不是简单类型
- 包含认证信息
- 需要预检请求
复杂请求示例
// 复杂跨域请求
fetch('http://api.example.com/users/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'Custom-Header': 'custom-value'
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
});
3. 预检请求(Preflight)
预检请求的触发条件
预检请求触发:
- 非简单请求方法
- 包含自定义请求头
- Content-Type不是简单类型
- 包含认证头
- 包含其他非简单头
预检请求示例
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization, Custom-Header
预检响应示例
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, Custom-Header
Access-Control-Max-Age: 86400
4. 凭证跨域请求
凭证请求的特征
凭证请求特征:
- 包含Cookie
- 包含Authorization头
- 需要服务器明确允许
- 不能使用通配符*
凭证请求示例
// 包含凭证的跨域请求
fetch('http://api.example.com/users', {
method: 'GET',
credentials: 'include', // 包含Cookie
headers: {
'Authorization': 'Bearer token123'
}
});
重难点分析
1. 安全性问题
CSRF攻击风险
@Component
public class CSRFProtectionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean validateCSRFToken(String token, String sessionId) {
// 验证CSRF Token
String key = "csrf:" + sessionId;
String storedToken = (String) redisTemplate.opsForValue().get(key);
if (storedToken == null || !storedToken.equals(token)) {
log.warn("CSRF Token验证失败: sessionId={}", sessionId);
return false;
}
return true;
}
public String generateCSRFToken(String sessionId) {
String token = UUID.randomUUID().toString();
String key = "csrf:" + sessionId;
// 设置Token过期时间
redisTemplate.opsForValue().set(key, token, Duration.ofHours(1));
return token;
}
}
同源策略绕过风险
@Component
public class OriginValidationService {
private final Set<String> allowedOrigins = new HashSet<>();
@PostConstruct
public void init() {
// 配置允许的源
allowedOrigins.add("http://localhost:3000");
allowedOrigins.add("https://www.example.com");
allowedOrigins.add("https://app.example.com");
}
public boolean isValidOrigin(String origin) {
if (origin == null) {
return false;
}
// 检查是否为允许的源
return allowedOrigins.contains(origin);
}
public boolean isSubdomainAllowed(String origin) {
// 检查子域名是否允许
return origin.endsWith(".example.com");
}
}
2. 性能问题
预检请求缓存
@Component
public class PreflightCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void cachePreflightResult(String origin, String method,
Set<String> headers, int maxAge) {
String key = generateCacheKey(origin, method, headers);
PreflightCache cache = new PreflightCache();
cache.setMaxAge(maxAge);
cache.setTimestamp(System.currentTimeMillis());
redisTemplate.opsForValue().set(key, cache, Duration.ofSeconds(maxAge));
}
public PreflightCache getPreflightCache(String origin, String method,
Set<String> headers) {
String key = generateCacheKey(origin, method, headers);
return (PreflightCache) redisTemplate.opsForValue().get(key);
}
private String generateCacheKey(String origin, String method, Set<String> headers) {
return String.format("preflight:%s:%s:%s", origin, method,
String.join(",", headers));
}
}
请求频率控制
@Component
public class CORSRequestRateLimiter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean isRateLimitExceeded(String origin, String ip) {
String key = String.format("cors:rate:%s:%s", origin, ip);
// 获取当前请求次数
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
// 设置过期时间
redisTemplate.expire(key, Duration.ofMinutes(1));
}
// 每分钟最多允许100次跨域请求
return count > 100;
}
public void recordCORSRequest(String origin, String ip, String method) {
String key = String.format("cors:stats:%s:%s", origin, method);
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, Duration.ofHours(24));
}
}
3. 兼容性问题
浏览器兼容性处理
@Component
public class BrowserCompatibilityService {
public CORSHeaders getCompatibleHeaders(String userAgent, String origin) {
CORSHeaders headers = new CORSHeaders();
if (isOldBrowser(userAgent)) {
// 旧版本浏览器兼容处理
headers.setAllowOrigin("*");
headers.setAllowMethods("GET, POST");
headers.setAllowHeaders("Content-Type");
} else {
// 现代浏览器标准处理
headers.setAllowOrigin(origin);
headers.setAllowMethods("GET, POST, PUT, DELETE, OPTIONS");
headers.setAllowHeaders("Content-Type, Authorization, X-Requested-With");
headers.setAllowCredentials(true);
}
return headers;
}
private boolean isOldBrowser(String userAgent) {
return userAgent.contains("MSIE") ||
userAgent.contains("Trident") ||
userAgent.contains("Edge/12");
}
}
处理方式
1. 服务器端CORS配置
全局CORS配置
@Configuration
public class GlobalCORSConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 允许的源
configuration.setAllowedOriginPatterns(Arrays.asList(
"http://localhost:*",
"https://*.example.com",
"https://app.example.com"
));
// 允许的HTTP方法
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"
));
// 允许的请求头
configuration.setAllowedHeaders(Arrays.asList(
"Origin", "Content-Type", "Accept", "Authorization",
"X-Requested-With", "Cache-Control"
));
// 允许的响应头
configuration.setExposedHeaders(Arrays.asList(
"Access-Control-Allow-Origin", "Access-Control-Allow-Credentials"
));
// 是否允许凭证
configuration.setAllowCredentials(true);
// 预检请求缓存时间
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
注解方式配置
@RestController
@RequestMapping("/api/users")
@CrossOrigin(
origins = {"http://localhost:3000", "https://app.example.com"},
allowedHeaders = {"Content-Type", "Authorization"},
methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE},
allowCredentials = "true",
maxAge = 3600
)
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// 实现获取用户逻辑
return ResponseEntity.ok(new User());
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
// 实现创建用户逻辑
return ResponseEntity.ok(user);
}
}
2. 过滤器方式处理
CORS过滤器
@Component
public class CORSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 获取请求源
String origin = httpRequest.getHeader("Origin");
// 检查是否为允许的源
if (isAllowedOrigin(origin)) {
httpResponse.setHeader("Access-Control-Allow-Origin", origin);
}
// 设置其他CORS头
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS");
httpResponse.setHeader("Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With");
httpResponse.setHeader("Access-Control-Max-Age", "3600");
// 处理预检请求
if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
httpResponse.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
private boolean isAllowedOrigin(String origin) {
if (origin == null) {
return false;
}
// 允许的源列表
Set<String> allowedOrigins = new HashSet<>();
allowedOrigins.add("http://localhost:3000");
allowedOrigins.add("https://app.example.com");
allowedOrigins.add("https://www.example.com");
return allowedOrigins.contains(origin);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化逻辑
}
@Override
public void destroy() {
// 清理逻辑
}
}
3. 中间件方式处理
Spring Security CORS配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and() // 启用CORS
.csrf().disable()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 配置允许的源
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
// 配置允许的方法
configuration.setAllowedMethods(Arrays.asList("*"));
// 配置允许的头
configuration.setAllowedHeaders(Arrays.asList("*"));
// 配置是否允许凭证
configuration.setAllowCredentials(true);
// 配置预检请求缓存时间
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Java中的处理方案
1. Spring Boot CORS处理
配置类方式
@Configuration
public class CORSConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}
控制器级别配置
@RestController
@RequestMapping("/api")
public class ApiController {
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("/data")
public ResponseEntity<Map<String, Object>> getData() {
Map<String, Object> data = new HashMap<>();
data.put("message", "Hello from API");
data.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(data);
}
@CrossOrigin(
origins = {"http://localhost:3000", "https://app.example.com"},
methods = {RequestMethod.POST, RequestMethod.PUT},
allowedHeaders = {"Content-Type", "Authorization"}
)
@PostMapping("/submit")
public ResponseEntity<String> submitData(@RequestBody String data) {
return ResponseEntity.ok("Data submitted successfully");
}
}
2. 自定义CORS处理器
自定义CORS处理器
@Component
public class CustomCORSHandler implements CorsProcessor {
@Override
public boolean processRequest(@Nullable CorsConfiguration config,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
// 获取请求源
String origin = request.getHeader("Origin");
// 如果没有配置CORS,直接返回
if (config == null) {
return true;
}
// 处理预检请求
if (isPreFlightRequest(request)) {
return handlePreFlightRequest(config, request, response);
}
// 处理实际请求
return handleActualRequest(config, request, response);
}
private boolean isPreFlightRequest(HttpServletRequest request) {
return "OPTIONS".equals(request.getMethod()) &&
request.getHeader("Access-Control-Request-Method") != null;
}
private boolean handlePreFlightRequest(CorsConfiguration config,
HttpServletRequest request,
HttpServletResponse response) {
// 设置预检响应头
response.setHeader("Access-Control-Allow-Origin",
config.getAllowedOrigins().iterator().next());
response.setHeader("Access-Control-Allow-Methods",
String.join(",", config.getAllowedMethods()));
response.setHeader("Access-Control-Allow-Headers",
String.join(",", config.getAllowedHeaders()));
response.setHeader("Access-Control-Max-Age",
String.valueOf(config.getMaxAge()));
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
response.setHeader("Access-Control-Allow-Credentials", "true");
}
response.setStatus(HttpServletResponse.SC_OK);
return false; // 不继续处理
}
private boolean handleActualRequest(CorsConfiguration config,
HttpServletRequest request,
HttpServletResponse response) {
// 设置实际请求的CORS头
String origin = request.getHeader("Origin");
if (config.getAllowedOrigins().contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
response.setHeader("Access-Control-Allow-Credentials", "true");
}
return true; // 继续处理
}
}
3. 动态CORS配置
动态CORS配置服务
@Service
public class DynamicCORSService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public CorsConfiguration getDynamicCORSConfig(String origin) {
CorsConfiguration config = new CorsConfiguration();
// 从数据库或缓存获取动态配置
CORSConfig dbConfig = getCORSConfigFromDB(origin);
if (dbConfig != null) {
config.setAllowedOrigins(Arrays.asList(dbConfig.getAllowedOrigins()));
config.setAllowedMethods(Arrays.asList(dbConfig.getAllowedMethods()));
config.setAllowedHeaders(Arrays.asList(dbConfig.getAllowedHeaders()));
config.setAllowCredentials(dbConfig.isAllowCredentials());
config.setMaxAge(dbConfig.getMaxAge());
} else {
// 使用默认配置
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(false);
config.setMaxAge(3600L);
}
return config;
}
private CORSConfig getCORSConfigFromDB(String origin) {
// 从数据库获取CORS配置
String key = "cors:config:" + origin;
return (CORSConfig) redisTemplate.opsForValue().get(key);
}
public void updateCORSConfig(String origin, CORSConfig config) {
// 更新CORS配置
String key = "cors:config:" + origin;
redisTemplate.opsForValue().set(key, config, Duration.ofHours(24));
// 清除相关缓存
clearCORSConfigCache(origin);
}
private void clearCORSConfigCache(String origin) {
// 清除CORS配置缓存
String cacheKey = "cors:cache:" + origin;
redisTemplate.delete(cacheKey);
}
}
4. 微服务CORS处理
网关CORS配置
@Configuration
public class GatewayCORSConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许所有源(生产环境应该限制)
config.addAllowedOriginPattern("*");
// 允许所有方法
config.addAllowedMethod("*");
// 允许所有头
config.addAllowedHeader("*");
// 允许凭证
config.setAllowCredentials(true);
// 预检请求缓存时间
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
微服务CORS配置
@Configuration
public class MicroserviceCORSConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 只允许网关访问
config.addAllowedOrigin("http://localhost:8080");
config.addAllowedOrigin("https://gateway.example.com");
// 允许的方法
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
// 允许的头
config.addAllowedHeader("Content-Type");
config.addAllowedHeader("Authorization");
config.addAllowedHeader("X-Requested-With");
// 不允许凭证(微服务间通信)
config.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
5. 错误处理和监控
CORS错误处理器
@ControllerAdvice
public class CORSErrorHandler {
@ExceptionHandler(CORSException.class)
public ResponseEntity<ErrorResponse> handleCORSError(CORSException e) {
ErrorResponse error = ErrorResponse.builder()
.error("CORS_ERROR")
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
@ExceptionHandler(InvalidOriginException.class)
public ResponseEntity<ErrorResponse> handleInvalidOrigin(InvalidOriginException e) {
ErrorResponse error = ErrorResponse.builder()
.error("INVALID_ORIGIN")
.message("请求源不被允许: " + e.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
}
CORS监控服务
@Component
public class CORSMonitoringService {
@Autowired
private MeterRegistry meterRegistry;
public void recordCORSRequest(String origin, String method, boolean allowed) {
if (allowed) {
meterRegistry.counter("cors.requests.allowed",
"origin", origin, "method", method).increment();
} else {
meterRegistry.counter("cors.requests.blocked",
"origin", origin, "method", method).increment();
}
}
public void recordPreflightRequest(String origin, boolean success) {
if (success) {
meterRegistry.counter("cors.preflight.success", "origin", origin).increment();
} else {
meterRegistry.counter("cors.preflight.failed", "origin", origin).increment();
}
}
@EventListener
public void handleCORSRequestEvent(CORSRequestEvent event) {
log.info("CORS请求事件: origin={}, method={}, allowed={}, timestamp={}",
event.getOrigin(), event.getMethod(), event.isAllowed(), event.getTimestamp());
}
}
最佳实践
1. 安全最佳实践
安全建议:
- 不要使用通配符*允许所有源
- 明确指定允许的HTTP方法
- 限制允许的请求头
- 谨慎使用allowCredentials
- 实现源验证逻辑
- 监控异常CORS请求
- 定期审查CORS配置
2. 性能最佳实践
性能建议:
- 合理设置预检请求缓存时间
- 使用Redis缓存CORS配置
- 实现请求频率限制
- 避免不必要的预检请求
- 使用CDN减少跨域请求
3. 配置最佳实践
配置建议:
- 开发环境允许本地源
- 生产环境限制特定域名
- 使用环境变量配置
- 实现配置热更新
- 记录CORS配置变更
4. 测试和验证
@SpringBootTest
class CORSConfigurationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testCORSHeaders() {
// 测试CORS头是否正确设置
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:8080/api/test", String.class);
HttpHeaders headers = response.getHeaders();
assertThat(headers.get("Access-Control-Allow-Origin")).isNotNull();
assertThat(headers.get("Access-Control-Allow-Methods")).isNotNull();
}
@Test
void testPreflightRequest() {
// 测试预检请求
HttpHeaders headers = new HttpHeaders();
headers.set("Origin", "http://localhost:3000");
headers.set("Access-Control-Request-Method", "POST");
headers.set("Access-Control-Request-Headers", "Content-Type");
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:8080/api/test",
HttpMethod.OPTIONS, request, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
总结
Java跨域(CORS)处理是企业级应用开发中的重要技术,通过本文章的学习,您应该能够:
- 理解跨域的基本概念和同源策略
- 掌握不同类型跨域请求的处理方式
- 分析跨域系统的重难点和解决方案
- 在Spring Boot中正确配置和处理CORS
- 实现安全、高效的跨域解决方案
- 遵循最佳实践,构建可靠的跨域处理系统
跨域处理需要综合考虑安全性、性能、兼容性等多个方面,建议在实际项目中根据具体需求选择合适的处理方案,并定期审查和优化配置。

3695

被折叠的 条评论
为什么被折叠?



