现在应用开发都朝着前后端分离、服务化的架构发展,所以会经常遇到跨域问题,就是当前端调用服务端接口时发送的请求不是同一个域的就会产生跨域问题。
一、同源策略(跨域原因)
同源策略:为了保证浏览器安全,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。
如果请求地址中的协议、域名、端口都相同,则为同源。
同源策略是浏览器安全的基石。
综上,当前端请求(XMLHttpRequest请求)后台接口时,浏览器会去校验该请求是否属于同源和是否有明确的的授权。
二、解决方案
1、取消浏览器检验
2、不使用XHR(XMLHttpRequest)请求类型(JSONP)
3、被调用方在请求头里添加相关参数(CORS)支持跨域
4、调用方使用反向代理,将请求在调用方的http服务器发送。或者在应用服务器中发生和接受并处理返回。
三、具体操作
- 1、取消浏览器检验
使用cmd命令行启动浏览器,切换到浏览器的exe文件所在的目录下:
- 2、使用JSONP
1)JSONP的原理:通过动态创建script标签(src属性)来发送请求,将后台返回的内容当做JavaScript解析和执行,完成后移除标签。例如:
可以看到JSONP请求发出去的时候会加一个callback参数。JSONP不是一种官方协议,但也是一种协议,即约定,前后端约定当请求中带有callback参数的请求为JSONP请求,后台则返回的是javascript代码(callback参数值为函数名,请求结果为参数的函数调用代码)
所以使用JSONP需要改动后端服务器代码,springboot2.x之前的版本可以使用AbstractJsonpResponseBodyAdvice类实现对JSONP请求的支持,因为spring5.x的版本已经将该类删除了,因为JSONP存在安全问题,不建议使用JSONP的方式解决跨域问题。不过我们可以利用ResponseBodyAdvice类拦截响应结果,简单写个切面模拟JSONP的实现原理。
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice{
public JsonpAdvice(){
super("callback");
}
}
import com.alibaba.druid.support.json.JSONUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
@ControllerAdvice
public class JsonpAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//如果为false则不会进入该切面
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response){
System.out.print("JsonpAdvice -->进入JsonpAdvice切面");
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse();
String callback = servletRequest.getParameter("callback");
if(callback != null&& !"".equals(callback)){
servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("application/javascript");
//用回调函数名称包裹返回数据,这样返回数据就作为回调函数的参数传回去
String result = callback + "(" + JSONUtils.toJSONString(body) + ")";
OutputStream out = null;
try {
out = servletResponse.getOutputStream();
out.write(result.getBytes());
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return body;
}
}
JSONP的弊端
1)服务器需要改动
2)只支持GET方法
3)发送的不是XHR请求
- 3、被调用方在请求头里添加相关参数(CORS)支持跨域
CORS(Cross-Origin Resource Sharing):为了解决跨域问题W3C提出了跨域资源共享(CORS)方案,通过后端服务器实现CORS接口,来实现跨域通信。
CORS将请求分为简单请求和非简单请求。
简单请求满足:
1)请求方法为GET、HEAD、POST其中一种;
2)请求头中无自定义头;
3)Content-Type仅限于text/plain、multipart/form-data、application/x-www-form-urlencoded
非简单请求有
1)求方式为put、delete的ajax请求;
2)json格式的ajax请求
3)带自定头的ajax请求
执行过程:
简单请求:先执行后判断(简单的跨域请求,判断跨域的域名和请求方法是否响应头中 Access-Control-Allow-Origin字段和 Access-Control-Allow-Method字段中)
非简单请求:先发送一次OPTION请求(预检请求)包含真实的请求信息(请求方法、自定义头字段、源信息),询问服务器是否允许这样操作(校验)。通过后才将真实请求发送到服务器。
filter解决方案:
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
String origin = httpServletRequest.getHeader("Origin");
String headers = httpServletRequest.getHeader("Headers");
if(origin != null && !"".equals(origin)){
//Access-Control-Allow-Origin为* 不支持带cookie的请求,必须全匹配,cookies为被调用放的cookie,所以动态获取
httpServletResponse.addHeader("Access-Control-Allow-Origin",origin);
}
if(headers != null && !"".equals(headers)){
//支持带自定义头的请求
httpServletResponse.addHeader("Access-Control-Allow-Headers",headers);
}
httpServletResponse.addHeader("Access-Control-Allow-Methods","*");
httpServletResponse.addHeader("Access-Control-Max-Age","3600");
httpServletResponse.addHeader("Access-Control-Allow-Credentials","true");
chain.doFilter(request,response);
}
@Override
public void destroy(){}
}
SpringBoot 中的解决方案:增加配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //允许跨域访问的路径
.allowedHeaders("*") //允许的请求头设置的字段
.allowedOrigins("*") //允许跨域的域名
.allowedMethods("POST","GET","PUT","OPTIONS","DELETE")//允许的请求方法
.maxAge(36000) //预检请求的缓存时间
.allowCredentials(true); //是否带cookie
}
}
- 4、调用方使用反向代理,将请求在调用方的http服务器(nginx)发送。或者在应用服务器中发生和接受并处理返回。
使用nginx做请求转发,但代码里面的请求路径为nginx的配置的虚拟路径。