Ajax跨域問題詳解
1.什麼是Ajax跨域問題
客戶端Client通過Ajax方式向伺服器Server發送Ajax請求,想要得到響應數據,但是由於客戶端和伺服器不在同一個域(協議,域名或埠不一致),瀏覽器出於安全方面的考慮,會在Ajax請求的時候作校驗,校驗不通過時瀏覽器會在控制台會拋出一個類似於SEC7120: [CORS] 原點「http://localhost:8080」未在「http://localhost:8081/ajaxserver/hello」的 cross-origin資源的 Access-Control-Allow-Origin response header 中找到「http://localhost:8080」的跨域安全問題
2.為什麼會產生Ajax跨域問題
*瀏覽器限制:通俗一點講就是瀏覽器多管閑事,當發現客戶端和伺服器不在同一個域中時會對Ajax請求做校驗,校驗不通過就會產生跨域安全問題,並非伺服器不允許客戶端訪問(以下示例可以驗證)。
*跨域:當客戶端和伺服器的協議,域名,埠有一樣不一致時,瀏覽器就會認為是跨域。
*XMLHttpRequest請求(Ajax請求):如果發出的不是XMLHttpRequest請求,瀏覽器也不會報跨域問題(以下示例可以驗證)。
3.Ajax跨域問題示例(基於SpringBoot)
創建伺服器端:
[java] view plain copy
import
org.springframework.web.bind.annotation.RequestMapping;import
org.springframework.web.bind.annotation.RestController;- @RestController
- @RequestMapping("/ajaxserver")
public
class
AjaxServerController {- @RequestMapping("hello")
public
String getString(){- System.out.println("************");
return
"Hello world!";
- }
- }
添加配置:
[html] view plain copy
- #配置埠號
- server.port=8081
- #熱部署生效
- spring.devtools.restart.enabled=true
驗證伺服器端(瀏覽器訪問http://localhost:8081/ajaxserver/hello):
創建客戶端:
[java] view plain copy
import
org.springframework.stereotype.Controller;import
org.springframework.web.bind.annotation.RequestMapping;- @Controller
- @RequestMapping("/ajaxclient")
public
class
AjaxClientController {
- @RequestMapping("/index")
public
String getIndex(){return
"index";- }
- }
創建靜態頁面index.html
[html] view plain copy
- <!DOCTYPE html
>
<html
xmlns:th="http://www.thymeleaf.org">
<head>
<meta
charset="UTF-8">
<title>
Ajax跨域請求</title>
- <!-- <script type="text/javascript" src="js/jquery-3.2.1.min.js"></script> -->
- <!-- 引入靜態資源文件-->
<script
th:src="@{/static/js/jquery-3.2.1.min.js}" type="text/javascript"></script>
</head>
<body>
<a
href="#" onclick="getRequest();">
Ajax跨域請求</a>
<script
type="text/javascript"
>
- function getRequest(){
- console.log("getRequest");
- $.ajax({
- url:"http://localhost:8081/ajaxserver/hello",
- dataType:"JSON",
- type:"POST",
- async:true,
- success:function(data){
- console.log(data);
- }
- });
- }
</script>
</body>
</html>
添加配置:
[html] view plain copy
- ############################################################
- #
- # thymeleaf相關配置
- #
- ############################################################
- spring.thymeleaf.prefix=classpath:/templates/
- spring.thymeleaf.suffix=.html
- spring.thymeleaf.mode=HTML5
- spring.thymeleaf.encoding=UTF-8
- spring.thymeleaf.content-type=text/html
- #載入靜態資源文件
- spring.mvc.static-path-pattern=/static/**
引入依賴:
[html] view plain copy
- <!-- 引入thymeleaf依賴 -->
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-thymeleaf</artifactId>
</dependency>
啟動客戶端,訪問http://localhost:8080/ajaxclient/index
點擊Ajax跨域問題鏈接,可以看到如下信息:
瀏覽器控制台拋出如下錯誤信息:SEC7120: [CORS] 原點「http://localhost:8080」未在「http://localhost:8081/ajaxserver/hello」的 cross-origin 資源的 Access-Control-Allow-Origin response header 中找到「http://localhost:8080」。
此時,清空編輯器控制台信息,來驗證跨域問題並非伺服器不允許客戶端訪問。再一次請求伺服器,可以看到,編輯器控制台列印信息如下:
此時,F12進入瀏覽器調試模式查看網路,伺服器沒有響應數據
在index.html中添加如下鏈接來驗證如果發出的不是XMLHttpRequest請求,瀏覽器也不會報跨域問題
[html] view plain copy
<a
href="http://localhost:8081/ajaxserver/hello">
非Ajax請求</a>
訪問http://localhost:8080/ajaxclient/index可以看到如下信息:
點擊非Ajax請求鏈接,可以看到如下信息:
控制台沒有拋出Ajax跨域安全問題,並且伺服器返回了響應數據
4.Ajax跨域問題解決思路
Ajax跨域問題產生的原因是瀏覽器限制,Ajax請求以及跨域,當三者同時滿足時才會產生Ajax跨域問題。基於這種情況,解決思路如下:
1>不讓瀏覽器做跨域校驗:可以通過一些參數設置禁止瀏覽器做限制,但是這需要客戶端都要做改動,因此不推薦。
2>發出不是XMLHttpRequest請求:JSONP可以動態創建一個script來發出跨域請求,但是瀏覽器不認為這是一個XMLHttpRequest請求。
3>支持跨域/隱藏跨域:當被調用方可以做一些修改時,被調用方可以設置參數來支持跨域(例如A域名調用B域名時,在返回的數據裡面加入一些欄位允許A域名調用);當被調用方不可以做一些修改時(例如需要請求www.baidu.com響應數據,而百度並非你的合作公司),此時需要調用方來做修改,通過一個代理,從瀏覽器發出的都是A域名的請求,在代理裡面把指定的URL轉到B域名里,此時瀏覽器認為就是同一個域名,就不會產生跨域問題。
5.Ajax跨域問題解決方法
基於Ajax跨域問題解決思路,整理了如下解決辦法:
1>不讓瀏覽器做跨域校驗
可以通過命令行的方式進行設置,具體操作參考:https://www.cnblogs.com/zhongxia/p/5416024.html
2>JSONP的方式
JSONP(JSON with Padding)是 JSON 的一種「使用模式」,可用於解決主流瀏覽器的跨域數據訪問的問題。通俗地說,JSONP是非官方協議,是一種約定,它約定了如果請求的參數里包含了指定的參數(默認是callback)就是一個JSONP請求,伺服器發現該請求是JSONP請求時就會把響應數據由原來的JSON對象改為JS代碼(JS代碼是函數調用的形式,函數名是callback參數的值,函數參數是原來要返回的JSON對象)。
將index.html中的Ajax請求的dataType改為JSONP,並修改伺服器代碼,添加如下類:
[java] view plain copy
import
org.springframework.web.bind.annotation.ControllerAdvice;import
org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;- @SuppressWarnings("deprecation")
- @ControllerAdvice
public
class
JSONPAdviceextends
AbstractJsonpResponseBodyAdvice{public
JSONPAdvice(){super
("callback");- }
- }
然後重新訪問客戶端,可以看到:
錯誤信息變為SCRIPT1004: SCRIPT1004: Expected ";"查看伺服器響應數據如下:
伺服器已經正常響應,但是瀏覽器控制台依然報錯,這是為什麼呢?於是百度了這個錯誤,得到如下結果:http://www.codes51.com/itwd/2116389.html
由此得知,介面不支持JSONP。JSONP弊端:(1)伺服器需要改動代碼支持(2)SJSONP只支持GET請求(可以驗證)將Ajax請求添加參數type:"POST",然後重新訪問客戶端可以看到如下信息(請求的方法依然是GET):
(3)發送的不是XMLHttpRequest請求:這既是JSONP能解決跨域問題的原因,也是其弊端。因為XMLHttpRequest請求有很多新特性(例如非同步,各種事件),而在JSONP中無法使用。
3>支持跨域/隱藏跨域
(1)常見的J2EE架構圖
客戶端Client向伺服器發送請求,先經過Apache/Nginx(http伺服器),當Apache/Nginx發現請求是靜態請求(js文件,css文件,圖片等)時,會直接將資源文件返回給客戶端Client而不會再轉發到Tomcat;當
[java] view plain copy
import
java.io.IOException;import
javax.servlet.Filter;import
javax.servlet.FilterChain;import
javax.servlet.FilterConfig;import
javax.servlet.ServletException;import
javax.servlet.ServletRequest;import
javax.servlet.ServletResponse;import
javax.servlet.annotation.WebFilter;import
javax.servlet.http.HttpServletResponse;- @WebFilter(urlPatterns="/*")
public
class
CrossFilterimplements
Filter {- @Override
public
void
init(FilterConfig filterConfig)throws
ServletException {- // TODO Auto-generated method stub
- }
- @Override
public
void
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws
IOException, ServletException {- System.out.println("CrossFilter");
- HttpServletResponse res=(HttpServletResponse) response;
- //res.addHeader("Access-Control-Allow-Origin", "*");
- res.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
- //res.addHeader("Access-Control-Allow-Methods", "*");
- res.addHeader("Access-Control-Allow-Methods", "POST");
- chain.doFilter(request, response);
- }
- @Override
public
void
destroy() {- // TODO Auto-generated method stub
- }
- }
此過濾器的作用是將響應頭添加兩個欄位Access-Control-Allow-Origin和Access-Control-Allow-Methods,前者表示支持跨域的域,後者表示支持跨域的方法。
修改控制層代碼(注意:控制層要返回JSON格式的數據,這是因為在Ajax請求時要求響應數據必須時JSON格式, 所以在返回數據時都需要轉為JSON格式)
JSONResult為LZ自己封裝的工具類,源碼地址https://github.com/Jasper2s/Study_Imooc/tree/master/SpringBoot/src/main/java/com/springboot/until
[java] view plain copy
import
org.springframework.web.bind.annotation.RequestMapping;import
org.springframework.web.bind.annotation.RestController;import
com.ajaxserver.utils.JSONResult;- @RestController
- @RequestMapping("/ajaxserver")
public
class
AjaxServerController {- /**
- * 注意:由於在Ajax請求時要求響應數據必須時JSON格式
- * 所以在返回數據時都需要轉為JSON格式
- * @return
- */
- @RequestMapping("hello")
public
JSONResult getString(){- System.out.println("AjaxServerController-->getString");
return
JSONResult.ok("Hello world!");- }
- }<span stylex="background-color:transparent;color:rgb(79,79,79);float:none;font-size:16px;font-style:normal;font-variant:normal;font-weight:400;letter-spacing:normal;line-height:26px;text-align:justify;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;word-spacing:0px;">
- </span>
然後,在項目的啟動程序AjaxServcerApplication上要添加@ServletComponentScan來掃描組件(否則,過濾器不會起作用)
最後,進入瀏覽器訪問http://localhost:8080/ajaxclient/index,查看控制台,可以看到:
控制台的響應表頭多了兩個欄位Access-Control-Allow-Origin和Access-Control-Allow-Methods,此時我們查看數據是否列印出來了:
由此說明,此次Ajax跨域訪問成功響應,並將數據成功返回給客戶端!
b.Apache配置
可參考https://www.imooc.com/video/16592
c.Nginx配置
可參考https://www.imooc.com/video/16591/0
d.Spring框架解決方案
可參考https://www.imooc.com/video/16593
(3)調用方解決跨域:Apache/Nginx(http伺服器)將客戶端Client所有的請求轉發到伺服器,此時瀏覽器發現所有的請求都是同一個域就不會產生跨域問題。a.Nginx配置可參考https://www.imooc.com/video/16594/0b.Apache配置可參考https://www.imooc.com/video/16595
6.請求分類
(1)簡單請求:瀏覽器對於簡單請求往往先執行後判斷,例如:瀏覽器控制台報跨域安全問題時,請求依然有響應數據,這說明瀏覽器將該請求視為簡單請求,先執行然後再判斷是否存在跨域安全問題。
(2)非簡單請求:瀏覽器對於非簡單請求往往先判斷後執行,舉例說明:
發送JSON格式Ajax請求示例如下:
修改index.html(添加如下代碼)
[html] view plain copy
<a
href="#" onclick="getRequest2();">
非簡單請求</a>
- function getRequest2(){
- console.log("getRequest2");
- $.ajax({
- url:"http://localhost:8081/ajaxserver/getuser",
- dataType:"JSON",
- type:"POST",
- data:{"name":"Jasper","age":20,"sex":"man"},//請求的數據為JSON對象
- //cache:true,//表示請求可以被緩存
- async:true,
- success:function(data_response){
- console.log(data_response);
- },
- error:function(){
- alert("error");
- }
- });
- }
Controller層添加如下代碼:
[java] view plain copy
- @RequestMapping("getuser")
public
JSONResult getUser(UserVO userVO){- System.out.println("AjaxServerController-->getUser");
- System.out.println(userVO.getName());
return
JSONResult.ok(userVO);- }
添加UserVO類:
[java] view plain copy
public
class
UserVO {private
String name;private
Integer age;private
String sex;public
String getName() {return
name;- }
public
void
setName(String name) {this
.name = name;- }
public
Integer getAge() {return
age;- }
public
void
setAge(Integer age) {this
.age = age;- }
public
String getSex() {return
sex;- }
public
void
setSex(String sex) {this
.sex = sex;- }
- }
瀏覽器訪問http://localhost:8080/ajaxclient/index,點擊非簡單請求鏈接可以看到如下信息:
可以看到瀏覽器發起了兩次getUser請求,第一次getUser請求是預檢命令,當預檢命令通過後,再發第二次請求然後伺服器進行響應。而簡單請求只有一次請求,如下圖:
那麼問題又出現了,如果每次發送非簡單請求,瀏覽器都要發起兩次請求豈不很影響速度?如何只讓瀏覽器在第一次發起的非簡單請求中請求兩次,之後只請求一次呢(因為第一次預檢命令通過之後,就沒有必要在之後的每次請求都發送一次預檢命令)?可以通過設置緩存!Win10瀏覽器已經自動將預檢命令加入到緩存(從上圖可知預檢命令執行時間為0秒,且來自緩存)。當然,也可以通過res.addHeader("Access-Control-Max-Age", "3600");告訴瀏覽器在一個小時內可以緩存設置的頭部信息。
至此,Ajax跨域請求相關問題已全部介紹完畢,歡迎大家指出不足之處!
※springmvc+jsp轉spring boot結構 前後端分離
※一個製作Web圖案的組件css-doodle
TAG:程序員小新人學習 |