JAVA Web 项目跨域预检请求过多?CORS 优化与 OPTIONS 缓存策略

JAVA Web 项目跨域预检请求过多?CORS 优化与 OPTIONS 缓存策略

大家好,今天我们来聊聊Java Web项目中一个常见的性能问题:跨域预检请求过多,以及如何通过CORS优化和OPTIONS缓存策略来解决这个问题。

什么是跨域请求?

首先,我们需要理解什么是跨域请求。这涉及到浏览器的同源策略(Same-Origin Policy)。同源策略是一种重要的安全机制,用于限制一个源的文档或脚本如何与来自另一个源的资源进行交互。如果两个URL的协议、域名和端口都相同,则它们属于同源。

例如:

  • http://www.example.com/app1/index.html
  • http://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)则会。

简单请求的条件:

  • 请求方法是 GETHEADPOST
  • 如果使用了 POST 方法,Content-Type 只能是以下三种:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 请求中没有设置自定义请求头(除了浏览器自动添加的,如 User-AgentReferer 等)。

复杂请求:

只要不满足简单请求的条件,就是复杂请求。 典型的例子包括:

  • 使用了 PUTDELETE 等请求方法。
  • 使用了 application/json 作为 Content-Type。
  • 设置了自定义请求头。

当浏览器检测到复杂请求时,会先发送一个 OPTIONS 预检请求到服务器。这个 OPTIONS 请求用于询问服务器,目标资源是否允许来自发起请求的源的跨域请求,以及支持哪些HTTP方法和头部。 服务器必须响应这个 OPTIONS 请求,并在响应头中包含相应的 CORS 头部信息,例如 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-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.propertiesapplication.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 缓存,还可以考虑以下优化技巧:

  1. 避免复杂请求: 尽量使用简单请求,避免触发预检请求。 例如,可以使用 GETPOST 方法,并使用 application/x-www-form-urlencoded 作为 Content-Type。
  2. 合并 API 请求: 如果需要多次跨域请求数据,可以考虑将这些请求合并为一个请求,减少跨域请求的次数。
  3. 使用 JSONP (仅限 GET 请求): JSONP 是一种古老的跨域解决方案,它利用 <script> 标签的跨域特性。 但是,JSONP 只能用于 GET 请求,并且存在安全风险,因此不推荐使用。
  4. 反向代理: 可以使用反向代理服务器将跨域请求代理到同源的服务器,从而避免跨域问题。

代码示例:一个完整的 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应用的性能和用户体验。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注