Git Product home page Git Product logo

ajaxdemo's Introduction

ajaxdemo

给部门写的跨域访问相关知识培训资料。主要内容分为如下几点

我的免费课程《AJAX完全讲解》已经上线,欢迎观看/交流! 链接:慕课网链接

  • 搭建环境测试
  • 什么是跨域访问安全错误
  • 产生跨域错误的条件
  • 解决的几种思路
  • 带cookie的跨域请求
  • 带自定义header的跨域请求
  • 总结
  • 培训的问题列表
  • 简单请求定义

大纲

搭建环境测试

  1. 使用springboot搭建后台服务,编写get请求。
@RestController
public class TestController {
	@GetMapping("/get1")
	public ResultBean<String> get1() {
		System.out.println("\n-------TestController.get1()\n");
		return new ResultBean<String>("get1 ok");
	}
}
  1. 配置host,把a.com和b.com都指向本地

host配置

  1. 编写请求页面,使用jq发送get的ajax请求,请求地址中使用b.com的绝对地址
function get1() {
	$.get("http://b.com:8080/get1", function(data) {
		console.log("get1 Loaded: ", data);
	});
}
  1. 访问a.com,点击get的ajax请求,发生跨域错误

注意!划重点!后台的get请求是执行成功了的!虽然前台报跨域错误!

返回码是200。

后台代码正常执行。

  1. 编写无参数post请求post1
@PostMapping("/post1")
public ResultBean<String> post1() {
	System.out.println("\n--------TestController.post1()\n");
	return new ResultBean<String>("post1 ok");
}
  1. 编写前台调用代码
function post1() {
	$.ajax({
		type : "POST",
		url : "http://b.com:8080/post1",
		dataType : "json",
		success : function(data) {
			console.log("post1 Loaded: ", data);
		}
	});
}
  1. 执行post1,前台报跨域错误,后台同样执行成功!

  1. 编写带参数post请求post2,参数格式为form-urlencoded格式(就是a=1&b=2这种)
@PostMapping("/post2")
public ResultBean<String> post2(Param param) {
	System.out.println("\n--------TestController.post2, param=" + param);
	return new ResultBean<String>("post2 ok, param=" + param);
}
  1. 编写前台调用代码
function post2() {
	var data = {
		key1 : '以form-urlencoded格式发送参数',
		id1 : 12345
	}

	$.ajax({
		type : "POST",
		data : data,
		url : "http://b.com:8080/post2",
		dataType : "json",
		success : function(data) {
			console.log("post2 Loaded: ", data);
		}
	});
}
  1. 执行post2,前台报跨域错误,后台同样执行成功!

  2. 编写带参数post请求post3,参数格式为json格式

@PostMapping("/post3")
public ResultBean<String> post3(@RequestBody Param param) {
	System.out.println("\n--------TestController.post3, param=" + param);
	return new ResultBean<String>("post3 ok, param=" + param);
}
  1. 编写前台调用代码
function post3() {
	var data = {
		key1 : '以json格式发送',
		id1 : 12345
	}

	$.ajax({
		type : "POST",
		contentType : "application/json",
		data : JSON.stringify(data),
		url : "http://b.com:8080/post3",
		dataType : "json",
		success : function(data) {
			console.log("post3 Loaded: ", data);
		}
	});
}
  1. 执行post3,前台报跨域错误,后台没有执行post3代码 可以看出post2是post命令,而且返回码是200,就是服务器已经正确执行了。 而post3是options命令,返回的是403,服务器并没有执行命令! 原因后面会解析。

  2. 测试结束

什么是跨域访问安全错误

不照抄网上的语言了,用我的理解来说,就是浏览器出于安全考虑,在XMLHttpRequest请求其他域的url的时候,会判断服务器是否允许跨域,如果不允许就会抛出跨域错误。

产生跨域错误的条件

  1. 必须是浏览器上发出的请求

其实就是浏览器多管闲事,觉得【可能】有安全问题,所以不允许。非浏览器发生的请求没有这个问题,如你在java代码中掉任何域都不可能报这个问题。

2.必须是XMLHttpRequest请求

直接访问肯定是不会错误的。

  1. 跨域

就是协议,域名,端口任何一个不同就算跨域。

重点:跨域和异步请求是浏览器的概念,服务器没有跨域和异步请求的概念。

解决的几种思路

针对上面产生的3个条件,我们有对应的解决方法。

针对浏览器,指定参数让浏览器闭嘴,不检查。

以chrome为例,增加参数--disable-web-security --user-data-dir=C:\MyChromeDevUserData 启动chrome即可解决,由于实际意义不大,不单独演示,大家有兴趣本机自己尝试即可。

针对XMLHttpRequest请求,使用jsonp

jsonp是比较上古的方式了,现在很多老系统还能看到。jsonp其实就插入了一个script标签来【同步】加载代码。服务器由原来的返回json数据,变成返回了调用函数的【js脚本】给浏览器执行,这个函数就是jsonp里面的callback函数,函数名是需要前台传给后台的。

我们来测试一下,编写json调用代码。主要就是 dataType: "jsonp"

function getByJsonp(){
	$.ajax({
		  type: "GET",
		  url: "http://b.com:8080/get1",
		  dataType: "jsonp",
		  success: function(data){
			  console.log("jsonp Loaded: " , data);
		  }
		});
}

分别执行原来的ajax请求和新写的jsonp 请求,可以看到原来的get请求出现在XHR XMLHttpRequest缩写异步请求上,而jsonp请求没有出现在XHR上,只在所有请求上,可以看出jsonp不是XHR请求。(其实jsonp的实现机制的动态插入script标签实现的,script标签在非ie浏览器在H5下也支持异步。)

截图中可以看到,jsonp请求的时候,jq会自动增加call函数。

如果服务器没有支持jsonp,返回的仍然是json格式,浏览器会报错(把json当做js执行了)。

如果服务器要支持jsonp,springboot下可以增加以下配置 AbstractJsonpResponseBodyAdvice

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");
	}
}

增加后会自动判断是否是jsonp,如果是json就返回对应的js。

再次调用,已经能正确获取数据打印结果了。

发送的请求,带上了jq随机生成的函数名 jQuery111309350735532809726_1493608521320

http://b.com:8080/get1?callback=jQuery111309350735532809726_1493608521320&_=1493608521322

返回的结果,是调用 jQuery111309350735532809726_1493608521320 函数的js语句。

/**/jQuery111309350735532809726_1493608521320({"code":0,"msg":"success","data":"get1 ok"});

重要:就算你明白了jsonp的工作原理,也不要自己编码实现jsonp,主流框架都支持jsonp的配置,直接使用即可。

jsonp有明显的几点硬伤

  • 返回json变成返回js了,所以服务器是要改动支持的,不是调用方一厢情愿说用就能用的。
  • 由于是动态内嵌script标签,那么肯定是不支持post方法了

所以,jsonp使用越来越少,不推荐。

针对跨域,有2种解决方法

服务器返回支持跨域信息

由于是浏览器出于安全考虑才限制跨域访问,那么我们可以在服务器中返回允许跨域的信息,让浏览器知道这个服务器请求支持跨域,请求就可以正常执行。

最简单的方式是增加@CrossOrigin注解,该注解可以加在类上也可以加在方法上。默认允许所有域名跨域。

@CrossOrigin
@RestController
public class TestController 

再次调用所有的请求,全部成功!表明,已经可以支持跨域了。

返回允许跨域信息

对于post3,可以看出先发出了一个OPTIONS咨询命令 看是否可以跨域,服务器在响应头里面告诉浏览器可以跨域,然后post3请求才真正执行。

所以post3会有2条请求记录。

OPTIONS咨询命令 并不是每次都会发送,第一次查询的返回的头里面有一条 Access-Control-Max-Age:1800,是表示有效期,这个时间段不会再次发送 OPTIONS咨询命令 了。

使用反向代理解决

既然浏览器觉得其他域名可能有安全问题,那么我们只需要把其他域名的东西变成自己域名的东西,跨域就可以解决。

我们使用反向代理,代理非本域名的请求,在外面看来就是同一个系统的请求,自然不用担心跨域问题。

以nginx配置为例,配置非常简单,配置如下:

 server {
        listen       80;
        server_name  a.com;

	location / {
	    proxy_pass http://a.com:8080/;
        }

        location /bcom/ {
	    proxy_pass http://b.com:8080/;
        }
}

表示 /bcom 开头的请求都转发到 http://b.com:8080/

然后我们把上面页面另存一份nginx.html,里面的请求地址由绝对地址 http://b.com:8080/xxx 改成相对地址 /bcom/xxx

重新测试,全部成功!

可以看到所有请求都是 http://a.com/bcom/ 开头的。

带cookie的跨域请求

默认跨域都是不带cookie的。

但我们很多时候需要发送cookie(如会话等),这种情况发送XMLHttpRequest请求的时候,客户端需要设置 withCredentials 为true,然后服务端需要返回支持cookie配置,需要返回 Access-Control-Allow-Credentials : trueAccess-Control-Allow-Origin : 对应的域名 ,注意:此处不能用*,必须是具体的域名

编写js代码

function getWithCookie() {
	$.ajax({
		type : "GET",
		url : "http://b.com:8080/getWithCookie",
		xhrFields : {
			withCredentials : true
		},
		success : function(data) {
			console.log("getWithCookie Loaded: ", data);
		}
	})
}

编写java代码,后台使用spring的@CookieValue注解获取cookie值。

@GetMapping("/getWithCookie")
public ResultBean<String> getWithCookie(@CookieValue(required=false) String cookie1) {
	System.out.println("\n-------TestController.getWithCookie(), cookie1="+cookie1);
	return new ResultBean<String>("getWithCookie ok, cookie1="+cookie1);
}

然后,在b.com上添加对应的cookie(使用工具或者document.cookie上增加),从a.com上发送getWithCookie请求到b.com,成功。

如果发送的请求里面没有对应的cookie,会报错

{"timestamp":1493552031929,"status":400,"error":"Bad Request","exception":"org.springframework.web.bind.ServletRequestBindingException","message":"Missing cookie 'cookie1' for method parameter of type String","path":"/getWithCookie"}

服务器设置 @CookieValue(required=false) 即可

带自定义header的跨域请求

很多时候,我们需要发送自定义的header,这个时候首先先要在服务器配置能接受哪些header。并使用 @RequestHeader 得到头字段。

@GetMapping("/getWithHeader")
@CrossOrigin(allowedHeaders = { "X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header4" })
public ResultBean<String> getWithHeader(
		@RequestHeader(required = false, name = "X-Custom-Header1") String header1) {
	System.out.println("\n-------TestController.getWithHeader(), header1=" + header1);
	return new ResultBean<String>("getWithHeader ok, header1=" + header1);
}

注意,@CrossOrigin(allowedHeaders = { "X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header4" })需要配置在方法上,不要配在类上面的 @CrossOrigin 注解上,否则会导致一些问题。

编写js代码,JQ里面增加自定义头有2种方法。headersbeforeSend事件 加。

function getWithHeader() {
	$.ajax({
		type : "GET",
		url : "http://b.com:8080/getWithHeader",
		headers : {
			"X-Custom-Header1" : "can not include zhongwen1111"
		},
		beforeSend : function(xhr) {
			xhr.setRequestHeader("X-Custom-Header2", "can not include zhongwen2222");
			xhr.setRequestHeader("X-Custom-Header3", "can not include zhongwen3333");
		},
		success : function(data) {
			console.log("getWithHeader Loaded: ", data);
		},
		error:function(data) {
			console.log("getWithHeader error: ", data);
		},
	})
}

注意1:一开始用 jquery.1.6.1 上面代码怎么样都发送不成功,后面换了 1.11.3 版本成功了。

注意2:header里面的值不能直接放中文,中文必须自己编码。

注意3:自定义头都使用 X- 开头,养成好习惯。

发送请求前,先发送 OPTIONS咨询命令 看看服务器是否允许发送这些自定义头。

发送请求的头会包含此次所有的自定义头列表,使用Spring@CrossOrigin 注解支持跨域的时候,服务器返回服务器支持的头和你请求的头的交集。服务器并没有告诉你所有支持的头。

Options命令会返回200(成功),里面会包含允许的列表,浏览器自己判断不一样,就会报错。

XMLHttpRequest cannot load http://b.com:8080/getWithHeader. Request header field X-Custom-Header3 is not allowed by Access-Control-Allow-Headers in preflight response.

我们修改上面js代码,去掉服务器没有配置的 X-Custom-Header3 , 重新测试,取值成功。

总结

本着工匠精神就写细一点,发现东西还是比较多的,本来觉得写4个小时应该就能写完了,结果周末花了快2天才写完,最后总结一下,对工作中用得上的知识点。

  • 发生跨域访问的三个条件:浏览器端,跨域,异步。
  • 针对异步的解决方法jsonp有很多硬伤,并不推荐。
  • 浏览器发送跨域请求之前会区分简单请求还是非简单请求,简单请求是直接请求,请求完再根据响应头信息判断(如果不支持跨域,尽管服务器成功执行返回200,但浏览器还是报错),非简单请求会先发送 OPTIONS咨询命令(如果不支持跨域,返回403禁止访问错误,支持则返回200,但并不一定就代表该请求能发出去,某些情况服务器还需要额外判断)。
  • 工作中遇到比较常见的非简单请求就是发送json数据的和带自定义头的。(带cookie的是简单请求)
  • 使用Spring的 @CrossOrigin 能很方便的解决跨域访问问题,几乎只需要一行代码。
  • 使用反向代理也是比较好的解决方法,公司内部配置也比较简单,反向代理能封装很多细节,增加很多其他特性。
  • 学会注解 @RequestHeader@CookieValue 的使用,不要自己去request对象上获取这些信息。

培训的问题列表

培训中问的问题列表,单独列出来供大家参考。

jsonp为什么只支持get,不支持post?

jsonp不是使用xhr发送的,是使用动态插入script标签实现的,当前无法指定请求的method,只能是get。调用的地方看着一样,实际上和普通的ajax有2点明显差异:1. 不是使用xhr 2.服务器返回的不是json数据,而是js代码。

在a上跨域访问b的时候,发送cookie的时候发送的是a.com 的还是 b.com 的cookie?

这个很明显是发送b.com的。cookie都是发送请求的url的域名上的。b.com上面不可能访问到a.com的cookie的。

那为什么实践中,调用其他公司内的子系统,会有a.com上的一些cookie?那是因为公司的sso单点登录的时候,把cookie设置到一级域名 .huawei.com ,而且 hostOnly 为false,所以二级域名都能访问到对应的cookie。所以同一个大域名下公司做单点登录太简单了。

另外说一点:cookie不区分端口

简单请求定义

定义请查看这篇文章

公司里面,最近别人问的最多的就是跨域问题,从来没有人跑过来问我数据结构的问题。。。

** GET不一定就是普通请求。POST不一定就是非简单请求。**

Simple requests

Some requests don’t trigger a CORS preflight. Those are called “simple requests” in this article, though the Fetch spec (which defines CORS) doesn’t use that term. A request that doesn’t trigger a CORS preflight—a so-called “simple request”—is one that meets all the following conditions:

The only allowed methods are:

  • GET
  • HEAD
  • POST

Apart from the headers set automatically by the user agent (for example, Connection, User-Agent, or any of the other headers with names defined in the Fetch spec as a “forbidden header name”), the only headers which are allowed to be manually set are those which the Fetch spec defines as being a “CORS-safelisted request-header”, which are:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (but note the additional requirements below)
  • Last-Event-ID
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width

The only allowed values for the Content-Type header are:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

No event listeners are registered on any XMLHttpRequestUpload object used in the request.

No ReadableStream object is used in the request.

ajaxdemo's People

Contributors

xwjie avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.