SpringBoot跨域&预请求

1,909 阅读4分钟

SpringBoot跨域&预请求

浏览器最基本的安全规范-同源策略。所谓同源是指域名、协议、端口相同。不同源的浏览器脚本(javascript、canvas)在没有明确授权的情况下,不能读写对方的资源

SpringBoot请求处理流程

Filter -> DispatcherServlet -> Intercepter -> Controller

我们可以在Filter中配置跨域也可以在Intercepter中配置跨域

我更倾向于在Filter中配置跨域,可以第一时间配置跨域,也可以第一时间处理预请求("OPTIONS")

 @Component
 @Order(Ordered.HIGHEST_PRECEDENCE)
 public class CorsFilter implements Filter {
 ​
     private static final String OPTIONS = "OPTIONS";
 ​
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
         HttpServletResponse res = (HttpServletResponse) response;
         res.addHeader("Access-Control-Allow-Credentials", "true");
         res.addHeader("Access-Control-Allow-Origin", "*");
         res.addHeader("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE,PUT");
         res.addHeader("Access-Control-Allow-Headers", "*");
         res.addHeader("Access-Control-Max-Age", "1800");
 ​
         // 如果是OPTIONS则结束请求
         if (OPTIONS.equals(((HttpServletRequest) request).getMethod())) {
             response.getWriter().println("ok");
             return;
         }
 ​
         chain.doFilter(request, response);
     }
 }

OPTIONS预请求

当前端使用脚本请求一个跨域资源时,如果是非简单请求(下文会解释),浏览器会自动帮你先发出一个OPTIONS查询请求,称为预检(cors-preflight-request),作用是询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用那些HTTP动词和头信息字段。 只有得到肯定答复,浏览器才会发生正式的XHR请求。

响应的header可以包含以下字段:

  • Access-Control-Allow-Origin: 允许哪些域被允许跨域,例如 baidu.comqq.com,或者设置为 * 则允许所有域访问
  • Access-Control-Allow-Credentials: 是否携带票据访问(对应fetch方法中credentials),当该值为true时,Access-Control-Allow-Origin 不允许设置为*
  • Access-Control-Allow-Methods: 标识该资源支持哪些方法,例如:POST, GET, PUT, DELETE
  • Access-Control-Allow-Headers: 标识允许哪些额外的自定义 header 字段和非简单值的字段
  • Access-Control-Max-Age: 表示可以缓存Access-Control-Allow-Methods和Access-Control-Allow-Headers提供的信息多长时间,单位秒,由服务端和浏览器默认值共同决定。
  • Access-Control-Expose-Headers: 通过该字段指出哪些额外的 header 可以被支持。

resp.addHeader("Access-Control-Max-Age", "0"),表示每次异步请求都发起预检请求,也就是说,发送两次请求。

resp.addHeader("Access-Control-Max-Age", "1800"),表示隔30分钟才发起预检请求。也就是说,发送两次请求

由此可见,当触发预检时,一次AJAX请求会消耗掉两个TTL,严重影响性能

那么如何节省掉 OPTIONS 请求来提升性能呢?如下:

发出简单请求

只要同时满足一下两个条件,就属于简单请求 (1)使用下列方法之一:

  • head
  • get
  • post

(2)请求的Header是

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

不同时满足上面的两个条件,就属于非简单请求。 很明显,我们常见的Post请求、媒体类型Content-Type=application/json也属于非简单请求,也会触发预检请求。

解决方案

服务器端在Response Headers中设置Access-Control-Max-Age字段

当第一次请求该URL时会发出OPTIONS请求,浏览器会根据返回的Access-Control-Max-Age字段缓存该请求的OPTIONS预检请求的响应结果。

在缓存有效期内,该资源的请求(URL和header字段都相同的情况下)不会再触发预检。

注意:chrome 打开控制台可以看到,当服务器响应Access-Control-Max-Age 时只有第一次请求会有预检,后面不会了。注意要开启缓存,在Chrome的Network中去掉Disable cache的勾选,否则不被缓存,Access-Control-Max-Age不生效。

问题

若配置完跨域后发现

请求会请求失败但使用PostMan等工具测试没有问题

则仔细检查配置的CORS 查看其中的Response Header是否得当

比如Access-Control-Allow-Headers配置使请求失败

 res.addHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,X-CAF-Authorization-Token,sessionToken,X-TOKEN");
 ​
 ​
 Provisional headers are shown Learn more
 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryxVYIRbbh7unBTUgp
 Referer: http://localhost:3000/
 sec-ch-ua: "Google Chrome";v="95", "Chromium";v="95", ";Not A Brand";v="99"
 sec-ch-ua-mobile: ?0
 sec-ch-ua-platform: "Windows"
 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
 X-Requested-With: XMLHttpRequest

\