前后端分离的项目中,前端和后端单独部署,使用不同的域名,前端代码在浏览器端访问后端的时候就会有跨域问题。
之前帮前端调试活动页面的时候,chrome调试工具上总是看到一个请求会重复发两次,后端加了锁,并做了数据校验,所以前端不管怎么搞都不会有问题,也没在意,一直以为是前端代码的问题。
后来研究跨域的时候,发现前端发的两个请求中,第一个是 options请求,第二个才是正常的 get/post请求。options请求是跨域试探,试探通过后才会发送真正的业务请求。
整个跨域过程主要是浏览器在处理,服务器端也要感知跨域,但是主要是告诉浏览器跨域的相关的配置,比如哪些域名被允许,哪些请求头被允许
(1) 发送 options请求的条件
当请求满足下述任一条件时,即应首先发送预检请求:
开发中一般使用的都是 get /post 请求,一般不满足第一个条件
但是 请求的 content-type 一般为 application / json,符合第二个条件
(2) Access-Control-Request-Headers
用 jquery 的 ajax请求的时候,第一次的options请求会加上下面的请求头
这个请求头的内容是正式请求时会发送的自定义请求,也就是说jquery的ajax在跨域的时候加了 x-csrf-token这个自定义请求头,然后浏览器监控到了x-csrf-token这个自定义请求头,就在options请求的 Acess-Control-Request-Headers的请求头内容中加入了 x-csrf-token
var formData = new FormData();
var xhr = new XMLHttpRequest();
xhr.timeout = 30000;
xhr.responseType = "json";
xhr.open('POST', 'http://localhost:8090/springProject/testController1/upLoad1', true);
xhr.setRequestHeader("content-type", "application/json");
xhr.setRequestHeader("userName", "admin");
xhr.setRequestHeader("passWord", "1111");
xhr.send();
上面的代码,我加了一个 userName: admin passWord: 1111 的请求头,浏览器发送的options请求的请求头参数如下
可以看到有一个 Access-Control-Request-Headers: username, password
Jquery的ajax由于加了自定义请求头,所以会先发送options请求
(3) 浏览器校验后端responseHeader返回的跨域配置
Access-Control-Allow-Origin: 允许哪些域被允许跨域,例如 http://qq.com 或 https://qq.com,或者设置为 * ,即允许所有域访问(通常见于 CDN )
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 提供的信息多长时间,单位秒,一般为10分钟。
Access-Control-Expose-Headers: 通过该字段指出哪些额外的 header 可以被支持。
后端可以返回上面这些跨域配置相关的字段,浏览器根据后端返回的配置进行校验
(1) debug准备
在 tomcat 8.5.34 Spring 5.1.4 的环境下
一开始项目下面没有 tomcat 的包,IDEA中debug到tomcat包里面的方法进不去,pom.xml加上 tomcat的依赖就好了,注意版本要和使用的tomcat版本一致,不然debug的时候代码行数对不上
org.apache.tomcat
tomcat-catalina
8.5.34
tomcat-catalina.8.5.34.jar里面已经有 servlet相关的包了,我开始还加了下面javax-servlet-api-3.1.0.jar这个包,debug的时候会跳到javax-servlet-api-3.1.0.jar里面了,代码行数对不上。去掉javax-servlet-api-3.1.0.jar 的依赖后,debug就会跳到
tomcat-catalina.8.5.34.jar的包里面
javax.servlet
javax.servlet-api
3.1.0
provided
(2) 源码分析
看源码的话,后端只是处理了options请求,处理options请求时会判断 responseHeader中是否有 Access-Control-Allow-Header,有的话基本就放过了,没有就拒绝请求,返回403
javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
将不同的http请求方法映射到对应的java方法
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
org.springframework.web.cors.DefaultCorsProcessor#processRequest
处理跨域的options请求主要是这个方法,没有跨域 或者 response中有 Access-Control-Allow-Origin,都直接返回true
CorsConfiguration是跨域配置,应该是可以利用这个对象配置跨域的规则
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);
}
如果要设置跨域配置,最简单的办法就是自己写一个过滤器,在前置方法中往responseHeader里面设置参数就好了
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, WG-App-Version, WG-Device-Id, WG-Network-Type, WG-Vendor, WG-OS-Type, WG-OS-Version, WG-Device-Model, WG-CPU, WG-Sid, WG-App-Id, WG-Token, x-csrf-token");
response.setHeader("Access-Control-Allow-Methods", "POST, GET");
response.setHeader("Access-Control-Allow-Credentials", "true");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}