JAVA Web 项目跨域预检请求过多?CORS 优化与 OPTIONS 缓存策略
大家好,今天我们来聊聊Java Web项目中一个常见的性能问题:跨域预检请求过多,以及如何通过CORS优化和OPTIONS缓存策略来解决这个问题。
什么是跨域请求?
首先,我们需要理解什么是跨域请求。这涉及到浏览器的同源策略(Same-Origin Policy)。同源策略是一种重要的安全机制,用于限制一个源的文档或脚本如何与来自另一个源的资源进行交互。如果两个URL的协议、域名和端口都相同,则它们属于同源。
例如:
http://www.example.com/app1/index.htmlhttp://www.example.com/app2/index.html同源 (路径不同没关系)http://www.example.com:8080/app1/index.html不同源 (端口不同)https://www.example.com/app1/index.html不同源 (协议不同)http://api.example.com/app1/index.html不同源 (域名不同)
如果JavaScript代码尝试从与加载该代码的页面不同的源请求资源,则会发生跨域请求。
为什么需要预检请求(OPTIONS)?
并非所有跨域请求都会触发预检请求。简单请求(Simple Request)不会触发,而复杂请求(Preflighted Request)则会。
简单请求的条件:
- 请求方法是 
GET、HEAD或POST。 - 如果使用了 
POST方法,Content-Type 只能是以下三种:text/plainmultipart/form-dataapplication/x-www-form-urlencoded
 - 请求中没有设置自定义请求头(除了浏览器自动添加的,如 
User-Agent、Referer等)。 
复杂请求:
只要不满足简单请求的条件,就是复杂请求。 典型的例子包括:
- 使用了 
PUT、DELETE等请求方法。 - 使用了 
application/json作为 Content-Type。 - 设置了自定义请求头。
 
当浏览器检测到复杂请求时,会先发送一个 OPTIONS 预检请求到服务器。这个 OPTIONS 请求用于询问服务器,目标资源是否允许来自发起请求的源的跨域请求,以及支持哪些HTTP方法和头部。 服务器必须响应这个 OPTIONS 请求,并在响应头中包含相应的 CORS 头部信息,例如 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers。
如果服务器没有正确响应 OPTIONS 请求,或者响应头中的 CORS 信息不满足浏览器的要求,浏览器会阻止实际的跨域请求发送。
跨域预检请求过多带来的问题
频繁的 OPTIONS 请求会带来以下问题:
- 性能损耗: 每次 
OPTIONS请求都需要一次完整的HTTP往返,增加延迟,降低用户体验。 - 服务器压力:  大量的 
OPTIONS请求会增加服务器的负载,尤其是在高并发场景下。 - 带宽消耗:  虽然 
OPTIONS请求的 body 通常为空,但其头部信息仍然会消耗一定的带宽。 
CORS 配置:解决跨域问题的基础
CORS (Cross-Origin Resource Sharing) 是一种W3C标准,它允许服务器声明哪些来源的Web页面可以访问其资源。通过正确配置 CORS,我们可以允许合法的跨域请求,并避免不必要的预检请求。
以下是在Java Web项目中配置CORS的几种常见方法:
1. 使用Servlet Filter:
这是最灵活的方式,你可以完全控制CORS响应头的设置。
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CorsFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // Initialization code (if needed)
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        // 允许所有来源的跨域请求(生产环境请谨慎使用)
        httpResponse.setHeader("Access-Control-Allow-Origin", "*");
        // 允许的请求方法
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        // 允许的请求头
        httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
        // 允许携带凭证(例如 cookies)
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        // 预检请求的缓存时间(秒) - 稍后会详细讲解
        httpResponse.setHeader("Access-Control-Max-Age", "3600");
        // 处理预检请求
        if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
            httpResponse.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(request, response);
        }
    }
    @Override
    public void destroy() {
        // Cleanup code (if needed)
    }
}
配置web.xml(或使用注解):
在 web.xml 中配置 Filter:
<filter>
    <filter-name>CorsFilter</filter-name>
    <filter-class>com.example.CorsFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
或者,使用 Spring Boot 的 @WebFilter 注解:
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, content-type");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }
    @Override
    public void init(FilterConfig filterConfig) {}
    @Override
    public void destroy() {}
}
2. 使用 Spring MVC 的 @CrossOrigin 注解:
Spring MVC 提供了 @CrossOrigin 注解,可以更方便地配置 CORS。
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.RestController;
@RestController
@CrossOrigin(origins = "http://localhost:3000", allowedHeaders = {"Content-Type", "Authorization"}) // 允许来自 http://localhost:3000 的跨域请求,并指定允许的请求头
public class MyController {
    @GetMapping("/data")
    public String getData() {
        return "Hello from the server!";
    }
    @PostMapping("/submit")
    public String submitData(String data) {
        return "Data received: " + data;
    }
}
@CrossOrigin 注解可以应用于整个Controller,也可以应用于单个方法。
3. 使用 Spring Boot 全局 CORS 配置:
Spring Boot 提供了全局 CORS 配置,可以在 application.properties 或 application.yml 中进行设置。
spring:
  mvc:
    cors:
      allowed-origins: http://localhost:3000, http://example.com # 允许的来源
      allowed-methods: GET, POST, PUT, DELETE, OPTIONS # 允许的请求方法
      allowed-headers: Content-Type, Authorization # 允许的请求头
      allow-credentials: true # 允许携带凭证
      max-age: 3600 # 预检请求的缓存时间(秒)
CORS 配置的关键点:
Access-Control-Allow-Origin: 指定允许跨域请求的来源。可以使用具体的域名,也可以使用*表示允许所有来源(生产环境不推荐)。Access-Control-Allow-Methods: 指定允许的HTTP方法。Access-Control-Allow-Headers: 指定允许的请求头。Access-Control-Allow-Credentials: 指定是否允许携带凭证(例如 cookies)。 如果设置为 "true",Access-Control-Allow-Origin不能设置为*, 必须设置为具体的 origin。Access-Control-Max-Age: 指定预检请求的缓存时间(秒)。 这是一个非常重要的配置,它可以显著减少OPTIONS请求的数量。
OPTIONS 缓存策略:减少预检请求的核心
Access-Control-Max-Age 是 CORS 中一个关键的响应头,它指示浏览器可以缓存 OPTIONS 预检请求结果的时间(以秒为单位)。
工作原理:
当浏览器发送一个 OPTIONS 请求并收到带有 Access-Control-Max-Age 响应头的响应时,它会将该响应缓存一段时间。 在这段时间内,如果浏览器再次需要发送相同的跨域请求(即,请求方法、请求头和来源都相同),它将直接使用缓存的响应,而不会再次发送 OPTIONS 请求。
如何设置 Access-Control-Max-Age:
在上面的 CORS 配置示例中,我们已经看到了如何设置 Access-Control-Max-Age:
- Servlet Filter:  使用 
httpResponse.setHeader("Access-Control-Max-Age", "3600");设置。 - Spring MVC 
@CrossOrigin:@CrossOrigin(maxAge = 3600)。 - Spring Boot 全局配置:  
spring.mvc.cors.max-age: 3600。 
Access-Control-Max-Age 的最佳实践:
- 选择合适的值:  
Access-Control-Max-Age的值越高,浏览器缓存OPTIONS响应的时间就越长,OPTIONS请求的次数就越少。 但是,如果你的 CORS 配置发生更改,浏览器可能需要很长时间才能反映这些更改。 因此,需要根据实际情况选择一个合适的值。 通常,几小时(例如 3600 秒)到几天(例如 86400 秒)是一个不错的选择。 - 考虑浏览器兼容性:  不同的浏览器对 
Access-Control-Max-Age的支持程度可能有所不同。 一些旧版本的浏览器可能不支持该响应头,或者对其支持存在一些限制。 
其他优化技巧
除了 CORS 配置和 OPTIONS 缓存,还可以考虑以下优化技巧:
- 避免复杂请求:  尽量使用简单请求,避免触发预检请求。  例如,可以使用 
GET或POST方法,并使用application/x-www-form-urlencoded作为 Content-Type。 - 合并 API 请求: 如果需要多次跨域请求数据,可以考虑将这些请求合并为一个请求,减少跨域请求的次数。
 - 使用 JSONP (仅限 GET 请求):  JSONP 是一种古老的跨域解决方案,它利用 
<script>标签的跨域特性。 但是,JSONP 只能用于GET请求,并且存在安全风险,因此不推荐使用。 - 反向代理: 可以使用反向代理服务器将跨域请求代理到同源的服务器,从而避免跨域问题。
 
代码示例:一个完整的 Spring Boot CORS 优化示例
// Controller
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000", allowedHeaders = {"Content-Type", "Authorization"}, maxAge = 3600)
public class ApiController {
    @GetMapping("/data")
    public String getData() {
        return "This is some data from the server!";
    }
    @PostMapping("/submit")
    public String submitData(@RequestBody String data) {
        return "Received data: " + data;
    }
}
// Spring Boot Application Class
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CorsDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CorsDemoApplication.class, args);
    }
}
application.properties:
server.port=8080
前端 (例如 React):
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState('');
  useEffect(() => {
    axios.get('http://localhost:8080/api/data')
      .then(response => {
        setData(response.data);
      })
      .catch(error => {
        console.error('Error fetching data:', error);
      });
  }, []);
  const handleSubmit = () => {
    axios.post('http://localhost:8080/api/submit', 'Some data', {
      headers: {
        'Content-Type': 'application/json'
      }
    })
      .then(response => {
        console.log('Response:', response.data);
      })
      .catch(error => {
        console.error('Error submitting data:', error);
      });
  };
  return (
    <div>
      <h1>Data from Server: {data}</h1>
      <button onClick={handleSubmit}>Submit Data</button>
    </div>
  );
}
export default App;
在这个例子中,我们使用了 Spring MVC 的 @CrossOrigin 注解来配置 CORS,并设置了 Access-Control-Max-Age 为 3600 秒。 前端使用 axios 发送跨域请求。 请注意,由于我们使用了 application/json 作为 Content-Type,因此这是一个复杂请求,会触发预检请求。 但是,由于我们设置了 Access-Control-Max-Age,浏览器会在一段时间内缓存 OPTIONS 响应,从而减少 OPTIONS 请求的次数。
总结
通过理解跨域请求、预检请求的机制,并正确配置 CORS,结合 Access-Control-Max-Age 缓存策略,我们可以显著减少 Java Web 项目中的跨域预检请求,提升性能,降低服务器压力。  记住,选择合适的 Access-Control-Max-Age 值,并在必要时进行其他优化,可以进一步提升用户体验。理解并应用这些策略,能有效改善Web应用的性能和用户体验。