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.com 或 qq.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
\