1.基础知识
1.1 跨域
跨域是浏览器的一种同源安全策略,是浏览器单方面限制的,所有仅在客户端运行在浏览器中才需要考虑这个问题。
跨域分为三种情况,协议跨域(http->https)、端口跨域、主机跨域。
常用的解决跨域的三种方式,JSONP(只能支持GET跨域),NGINX代理转发(一般前端同学爱这么用),CORS
1.2 CORS原理
因为cors是由浏览器控制的,实际就是在HEADER中新增一些字段,来进行跨域的授权访问。
比如
request部分:
Origin //浏览器自己设置的,表示请求从哪个域名下发出来的 Access-Control-Request-Method Access-Control-Request-Headers
response部分:
Access-Control-Allow-Origin //指定哪些客户端的域名允许访问这个资源 Access-Control-Allow-Credentials //服务器允许浏览器带cookie上来,不设这个服务器端就无法获得登录信息 Access-Control-Max-Age //告诉浏览器多久不需要再发出预检请求 Access-Control-Allow-Methods //服务器支持的方法 比如POST GET之类的 Access-Control-Allow-Headers //需要在正式请求中加入的header值,否则正式请求会被拒绝,也是预检请求的响应中带回去的 Access-Control-Expose-Headers //很少用,告诉客户端哪些header可以使用
1.3 CORS的三种场景
1.3.1 简单请求
浏览器来决定请求的种类,比如不带自定义请求头信息的GET请求、HEAD请求以及Content-type为application/x-www-form-urlencoded、multipart/form-data或者text/plain的post请求,都是简单请求。
简单请求时,浏览器的request只有一个origin字段,后端的返回值也很简单只要加一个Access-Control-Allow-Origin九可以了,浏览器拿到Access-Control-Allow-Origin的值后和自己的进行首部比对,通过就允许跨域了。
1.3.2 预检请求(preflight)
预检请求是浏览器发现不是简单请求后,封装一个OPTIONS请求到服务器,根据服务器的返回值来判断是否可以进行跨域,可以的话,后面还有一次真正带数据的请求。上面的response部分:部分就是预检请求后服务器端的响应
1.3.3 带凭据的请求
其实就是当预检请求的返回值中 Access-Control-Allow-Credentials 的值为TRUE,那么后续的正式请求就会携带凭据信息(cookie等)
2.springsecurit中cors的实现
springboot中是通过CorsFilter来实现的,具体干活的是DefaultCorsProcessor,一会看下代码。开启方式也很简单,就是security的配置中通过HttpSecurity中的.cors()方法开启。
DefaultCorsProcessor.java
//干活的代码 public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { //回写三个vary属性 response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN); response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); //如果不是跨域请求,直接返回true了 if (!CorsUtils.isCorsRequest(request)) { return true; } //response已经有Access-Control-Allow-Origin 就是已经处理过了,就退回,兼容其他的自定义filter if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) { logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\""); return true; } //判断是否是预检请求 boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); //缺少cors的配置 if (config == null) { //预检请求直接拒绝 if (preFlightRequest) { rejectRequest(new ServletServerHttpResponse(response)); return false; } //简单请求直接通过 else { return true; } } //具体处理 return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), 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(); //确保请求带了origin这个属性 if (allowOrigin == null) { logger.debug("Reject: '" + requestOrigin + "' origin is not allowed"); rejectRequest(response); return false; } //请求的method,复杂请求从Access-Control-Request-Method里面拿,简单请求直接就是request.getMethod() HttpMethod requestMethod = getMethodToUse(request, preFlightRequest); List<HttpMethod> allowMethods = checkMethods(config, requestMethod); //确保请求的method 确保在服务器端的配置中是允许的 if (allowMethods == null) { logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed"); rejectRequest(response); return false; } //和方法类似,校验下header 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; } //设置Access-Control-Allow-Origin responseHeaders.setAccessControlAllowOrigin(allowOrigin); //如果是预检请求,设置下Access-Control-Allow-Methods if (preFlightRequest) { responseHeaders.setAccessControlAllowMethods(allowMethods); } //如果是预检请求,并且allowHeaders不为空,设置下Access-Control-Allow-Headers if (preFlightRequest && !allowHeaders.isEmpty()) { responseHeaders.setAccessControlAllowHeaders(allowHeaders); } //设置Access-Control-Expose-Headers if (!CollectionUtils.isEmpty(config.getExposedHeaders())) { responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders()); } //设置Access-Control-Allow-Credentials if (Boolean.TRUE.equals(config.getAllowCredentials())) { responseHeaders.setAccessControlAllowCredentials(true); } //设置Access-Control-Max-Age if (preFlightRequest && config.getMaxAge() != null) { responseHeaders.setAccessControlMaxAge(config.getMaxAge()); } response.flush(); return true; }