ajax跨域问题

什么是Ajax跨域问题

这里通过一个示例来说明。
我们这里准备了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跨域问题。

Ajax跨域的原因

产生跨域是由于浏览器的安全策略,JavaScript只能访问和操作自己域下的资源,不能访问和操作其他域下的资源。跨域问题是针对JS和ajax的,html本身没有跨域问题,比如a标签、script标签、甚至form标签(可以直接跨域发送数据并接收数据)等。所谓的同源,指的是域名、协议、端口均相等。

解决Ajax跨域的方式

1.jsonp

我们对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代码。

2.被调用方修改以支持跨域

我们这里使用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");

带cookie的跨域

我们在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(数字秒)来缓存预检命令的结果,这样在指定的时间内浏览器不会再次发送预检命令。

3.服务器代理

使用nginx解决跨域

我们使用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

你可能感兴趣的:(Security)