跨域与预检请求 PreFlightRequest,springSecurity解决跨域问题
什么是预检请求
当浏览器发送post请求时,一般请求体都比较大,但如果是跨域的话,服务器会拒绝该请求,传输的数据被丢弃。
为了不浪费流量,浏览器在发送post前先发送一个小的options请求,将接下来的请求方式等设置到请求头中,
服务器检查请求头,如果服务器允许这样的请求
则返回200并在相应头中设置以下消息给客户端,如允许什么样的请求头,请求方式,是否允许带cookie等,
否则就拒绝请求。
这样的话,浏览器就不会再发送接下来的post请求了,从而节省资源。
- 其中两个请求头就是
Access-Control-Request-Method:POST
,表示稍后的请求使用什么method进行请求,这里是 port,
Access-Control-Request-Headers:content-type
,表示稍后的请求中自定义请求头是什么这里我是 content-type
查看spring的CorsFilter源码
在servlet环境下,请求都要经过filter,spring为我们提供了CorsFilter来处理这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (CorsUtils.isCorsRequest(request)) { CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request); if (corsConfiguration != null) { boolean isValid = this.processor.processRequest(corsConfiguration, request, response); if (!isValid || CorsUtils.isPreFlightRequest(request)) { return; } } }
filterChain.doFilter(request, response); }
|
上面源码可以看出,拦截器对请求进行检查,对是预检请求的request进行处理
检查没通过则isValid为false,直接返回,此时response被设置了401状态码,前台看到的是401
如果检查通过,但是是预检请求也直接返回,此时前台收到的是无内容的200响应
如何判断是预检请求的逻辑
1 2 3 4 5 6 7 8 9
| public static boolean isPreFlightRequest(HttpServletRequest request) { return (isCorsRequest(request) && HttpMethod.OPTIONS.matches(request.getMethod()) &&request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null); }
|
上面源码isPreFlightRequest中三个`&&`连接的判断可以看出,满足预检请求的条件是
1.是cors请求,即带有origin头的,如get方式的请求就不带有origin头,所以就不是cors请求
2.必须是options的请求
3.带有Access-Control-Request-Method请求头,表示接下来的请求方式
满足上面三个条件就是预检请求了
this.processor.processRequest(corsConfiguration, request, response)这里面做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { if (!CorsUtils.isCorsRequest(request)) { return true; } ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response); if (responseHasCors(serverResponse)) { logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\""); return true; } ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request); if (WebUtils.isSameOrigin(serverRequest)) { logger.trace("Skip: request is from same origin"); return true; } boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); if (config == null) { if (preFlightRequest) { rejectRequest(serverResponse); return false; } else { return true; } } return handleInternal(serverRequest, serverResponse, config, preFlightRequest); }
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, CorsConfiguration config, boolean preFlightRequest) throws IOException { String requestOrigin = request.getHeaders().getOrigin(); String allowOrigin = checkOrigin(config, requestOrigin); HttpHeaders responseHeaders = response.getHeaders();
responseHeaders.addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS)); if (allowOrigin == null) { logger.debug("Reject: '" + requestOrigin + "' origin is not allowed"); rejectRequest(response); return false; } HttpMethod requestMethod = getMethodToUse(request, preFlightRequest); List<HttpMethod> allowMethods = checkMethods(config, requestMethod); if (allowMethods == null) { logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed"); rejectRequest(response); return false; } List<String> requestHeaders = getHeadersToUse(request, preFlightRequest); List<String> allowHeaders = checkHeaders(config, requestHeaders); if (preFlightRequest && allowHeaders == null) { logger.debug("Reject: headers '" + requestHeaders + "' are not allowed"); rejectRequest(response); return false; } responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) { responseHeaders.setAccessControlAllowMethods(allowMethods); }
if (preFlightRequest && !allowHeaders.isEmpty()) { responseHeaders.setAccessControlAllowHeaders(allowHeaders); }
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) { responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders()); }
if (Boolean.TRUE.equals(config.getAllowCredentials())) { responseHeaders.setAccessControlAllowCredentials(true); }
if (preFlightRequest && config.getMaxAge() != null) { responseHeaders.setAccessControlMaxAge(config.getMaxAge()); }
response.flush(); return true; }
|
总结,什么时候允许request向下进入servlet
非cors请求,即头部不带origin的:有效
response上已经设置过Access-Control-Allow-Origin:有效
同源请求:有效
缺少cors配置,是预检请求:拒绝
缺少cors配置,不是预检请求:有效
如果头部中的origin和配置的AllowedOrigins不匹配:拒绝
如果头部中的Access-Control-Request-Method和配置的AllowedMethods不匹配:拒绝
如果头部中的Access-Control-Request-Headers与配置中AllowedHeader不匹配且是预检请求:拒绝
在springSecurity 的config中配置cors的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| http.cors().configurationSource(request -> { CorsConfiguration c = new CorsConfiguration();
c.setAllowedOrigins(Arrays.asList("http://192.168.31.114", "*"));
c.setAllowedMethods(Arrays.asList("POST", "GET", "OPTIONS", "PUT"));
c.addAllowedHeader("content-type");
c.setAllowCredentials(true); c.setMaxAge(1800L); return c; });
|