这里通过一个示例来说明。
我们这里准备了2个Springboot工程。
crossdomain-server:
端口:8080
对外提供的接口如下:
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/get")
public ResultBean get() {
System.out.println("TestController.get().");
return new ResultBean("hello,justin");
}
}
通过浏览器请求http://localhost:8080/test/get得到如下结果:
crossdomain-client:
端口:8081
提供了一个简单的页面,用于Ajax请求crossdomain-server的接口。
<html>
<head>
<meta charset="UTF-8">
<title>title>
<script type="text/javascript" src="/jquery.js">script>
head>
<body>
<a href="#" onclick="get1();">get请求a>
<script type="text/javascript">
function get1() {
$.getJSON("http://localhost:8080/test/get",function(json) {
alert(json);
});
}
script>
body>
html>
但是点击“get请求”后,发现控制台报错了。
如下:
这个就是Ajax跨域问题。
产生跨域是由于浏览器的安全策略,JavaScript只能访问和操作自己域下的资源,不能访问和操作其他域下的资源。跨域问题是针对JS和ajax的,html本身没有跨域问题,比如a标签、script标签、甚至form标签(可以直接跨域发送数据并接收数据)等。所谓的同源,指的是域名、协议、端口均相等。
我们对crossdomain-server做些修改:
a.增加ControllerAdvice
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback");
}
}
b.页面Ajax请求方式改为jsonp
// 每个测试用例的超时时间
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
var base = "http://localhost:8080/test";
// 测试模块
describe("ajax跨域",function() {
it("jsonp请求",function(done) {
var result;
$.ajax({
url:base+"/get",
dataType:"jsonp",
success:function(callback) {
result = callback;
expect(result).toEqual({
"data": "hello,justin"
})
// 校验完成,通知jasmine框架
done();
}
});
});
});
浏览器输入http://localhost:8081可以看到测试通过,
看下jsonp请求:
这里使用了jasmine测试框架,具体使用方法可以执行百度。jasmine的github地址为:https://jasmine.github.io,可以在release中下载。使用可以参考:https://jasmine.github.io/2.3/introduction.html。
jsonp虽然可以解决跨域问题,但jsonp只支持get请求,而且还需要修改前后台代码。
jsonp为什么只支持get,不支持post?
jsonp不是使用xhr发送的,是使用动态插入script标签实现的,当前无法指定请求的method,只能是get。
调用的地方看着一样,实际上和普通的ajax有2点明显差异:1. 不是使用xhr 2.服务器返回的不是json数据,而是js代码。
我们这里使用Filter,在响应中增加Access-Control-Allow-Origin。
@SpringBootApplication
public class CrossdoaminServerApplication {
public static void main(String[] args) {
SpringApplication.run(CrossdoaminServerApplication.class, args);
}
@Bean
public FilterRegistrationBean crossFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.addUrlPatterns("/*");
bean.setFilter(new CrossFilter());
return bean;
}
}
public class CrossFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 允许http://localhost:8081域访问
resp.addHeader("Access-Control-Allow-Origin", "http://localhost:8081");
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
crossdomain-client前端测试点:
// 测试模块
describe("ajax跨域",function() {
it("get请求",function(done) {
var result;
$.getJSON(base+"/get",function(json){
result = json;
expect(result).toEqual({
"data": "hello,justin"
})
// 校验完成,通知jasmine框架
done();
});
});
it("jsonp请求",function(done) {
var result;
$.ajax({
method:"post",
url:base+"/get",
dataType:"jsonp",
success:function(callback) {
result = callback;
expect(result).toEqual({
"data": "hello,justin"
})
// 校验完成,通知jasmine框架
done();
}
});
});
});
测试结果:
可以将Access-Control-Allow-Origin
设置为*,这样任何域都可以访问。同时可以通过Access-Control-Allow-Methods
指定允许访问的方法。
如允许GET请求:
// 同样可以将Access-Control-Allow-Methods设置为*,表示允许所有方法。
resp.addHeader("Access-Control-Allow-Methods", "GET");
我们在crossdomain-server增加一个测试方法:
@GetMapping("/getCookie")
public ResultBean getCookie(@CookieValue(name="cookie1") String cookie1) {
System.out.println("TestController.getCookie().cookie1=" + cookie1);
return new ResultBean("cookie:" + cookie1);
}
然后浏览器访问crossdomain-server的任意一个请求,使用document.cookie="cookie1=justin"
来增加一个名为cookie1,值为justin的cookie。
crossdomain-client增加一个测试用例:
it("getCookie请求",function(done) {
var result;
$.ajax({
type:"get",
url:base+"/getCookie",
xhrFields: {
// 默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。如果服务器接收带凭据的请求,会用下面的HTTP头部来响应。
withCredentials: true
},
success:function(json) {
result = json;
expect(result).toEqual({
"data": "cookie:justin"
})
// 校验完成,通知jasmine框架
done();
}
});
});
我们访问http://localhsot:8081,
可以看到,请求是成功的(statuscode=200),请求也带上了cookie。但jasmine提示失败。
我们看下浏览器控制台:
提示信息很明确了:提示我们响应头需要设置Access-Control-Allow-Credentials为true。
我们到CrossFilter设置一下:
resp.addHeader("Access-Control-Allow-Credentials", "true");
ok,加上以后再次请求就成功了。
注意:这里不能设置Access-Control-Allow-Origin为*,否则会报下面的错误:
但是,我们不可能只有一个跨域的站,怎么处理?
我们观察浏览器的请求,可以发现,如果是跨域请求,会有Origin请求头,我们后台根据这个请求头设置即可。
String url = req.getHeader("Origin");
if (!StringUtils.isEmpty(url)) {
resp.addHeader("Access-Control-Allow-Origin", url);
resp.addHeader("Access-Control-Allow-Credentials", "true");
}
错误:Missing cookie ‘cookie1’ for method parameter of type String
最后发现是jquery版本太低,这里使用了jquery1.11.3后ok了。
在crossdomain-server增加一个请求方法:
@GetMapping("/customHeader")
public ResultBean getCustomHeader(@RequestHeader("X-My-Header") String myHeader) {
System.out.println("TestController.getCustomHeader().myHeader=" + myHeader);
return new ResultBean("header:" + myHeader);
}
在crossdomain-client增加一个测试用例:
it("getCustomeHeader请求",function(done) {
var result;
$.ajax({
type:"get",
url:base+"/customHeader",
headers:{
'X-My-Header':'justin'
},
success:function(json) {
result = json;
expect(result).toEqual({
"data": "header:justin"
})
// 校验完成,通知jasmine框架
done();
}
});
});
测试发现报错了
意思我们的Access-Control-Allow-Headers响应头没有包含这个自定义的请求头,所以我们在CrossFilter加上
resp.addHeader("Access-Control-Allow-Headers", "Content-Type,X-My-Header");
再次请求,成功。
与Access-Control-Allow-Origin
一样,我们也可以对Access-Control-Allow-Headers
进行动态设置。
我们观察customHeader的预检命令的请求头中有Access-Control-Request-Headers:x-my-header
。
String headers = req.getHeader("Access-Control-Request-Headers");
if (!StringUtils.isEmpty(headers)) {
resp.addHeader("Access-Control-Allow-Headers", headers);
}
我们在crossdomain-server增加一个postJson方法:
@PostMapping("/postJson")
public ResultBean postJson(@RequestBody User user) {
System.out.println("TestController.postJson()");
return new ResultBean("hello," + user.getName());
}
在crossdomain-client增加一个测试用例:
it("postJson请求",function(done) {
var result;
$.ajax({
type:"post",
url:base+"/postJson",
contentType:"application/json;charset=utf-8",
data:JSON.stringify({name:"justin"}),
success:function(json) {
result = json;
expect(result).toEqual({
"data": "hello,justin"
})
// 校验完成,通知jasmine框架
done();
}
});
});
浏览器访问发现postJson失败了,如图:
而且,我要请求的是一个post请求的/postJson请求,但实际浏览器是发出了一个OPTIONS请求,这个就是预检命令。
看下浏览器的控制台:
意思是我们的响应头Access-Control-Allow-Headers
中没有找到请求头Content-Type
。
所以,我们修改一下代码,增加Content-Type。
我们在crossdomain-server的CrossFilter中增加:
resp.addHeader("Access-Control-Allow-Headers", "Content-Type");
这次请求成功了
可以看到postJson实际发送了2个请求,第一个是OPTIONS,它返回200后,浏览器再次发送了我们要请求的。
简单请求:
请求方法为GET,POST,HEAD。
且请求header中无自定义请求头,且Content-type为下面几种:text/plain,multipart/form-data,application/x-www-form-urlencoded.
非简单请求:
put,delete方法的Ajax请求
发送json格式的Ajax请求
带自定义请求头的Ajax请求
比如一个post的json请求,实际浏览器先发出一个OPTIONS预检命令,然后才发送的POST请求。可以在Filter中增加请求头Access-Control-Max-Age:3600(数字秒)来缓存预检命令的结果,这样在指定的时间内浏览器不会再次发送预检命令。
我们使用nginx帮我们对请求做了转发,将b.com的请求转发到http://localhost:8080,同时设置了相关的响应头。
1.修改本机hosts文件,将b.com映射到127.0.0.1;
127.0.0.1 b.com
2.在nginx.conf文件最后(最后一个}上面)增加:
include vhost/*.conf;
3.在nginx.conf同级目录增加vhost目录,并在下面创建b.com.conf文件。
b.com.conf文件内容如下:
server{
# 监听80端口
listen 80;
# 监控的域名
server_name b.com;
# 拦截所有请求
location /{
# 将请求转发给http://localhost:8080/
proxy_pass http://localhost:8080/;
# 允许访问所有的方法
add_header Access-Control-Allow-Methods *;
# 设置预检命令的有效期
add_header Access-Control-Max-Age 3600;
# 允许凭据
add_header Access-Control-Allow-Credentials true;
# 使用$http_orgin获取请求头orgin的值
add_header Access-Control-Allow-Origin $http_origin;
# 使用$http_access_control_request_headers获取请求头Access-Control-Request-Headers的值
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
# 如果是预检命令,直接返回200OK
if ($request_method = OPTIONS){
return 200;
}
}
}
4.crossdomain-server修改
注释掉CrossFilter的使用代码。
// @Bean
// public FilterRegistrationBean crossFilter() {
// FilterRegistrationBean bean = new FilterRegistrationBean();
// bean.addUrlPatterns("/*");
// bean.setFilter(new CrossFilter());
// return bean;
// }
5.crossdomain-client修改
将http://localhost:8080改成http://b.com
var base = "http://b.com/test";
6.测试
cmd切换到ningx所在目录,使用nginx -t先测试一下配置是否正确,没问题执行start nginx启动nginx服务。
所有请求都访问ok。
Ajax跨域的解决方法有很多,使用时要根据实际的情况选择合适的解决方法。
可以参考慕课网的课程https://www.imooc.com/video/16571讲的很详细。
代码:https://gitee.com/qincd/crossdomain-demo