天天看點

SpringBoot-12-之Ajax跨域通路全解析

一.什麼是跨域呢?

1.引入:
先講個故事:從前一個叫8080的大佬和一個8081的大佬各占一方天地,還有一個叫浏覽器的大佬和8080還有8081關系都不錯。浏覽器和8080做着一件事(8080端應用),浏覽器和8081做着另一件事(8081端應用),但8080和8081卻沒有什麼交集。有一天8081什麼話也沒說,就跑到8080的地盤拿東西(ajax傳回的資料),浏覽器手下的警衛員說:"這種珍貴的東西,無憑無據的,我們可不能給你"(跨域通路錯誤)。
8081氣憤離去,心想:"老子可是大佬,還要憑據,于是打電話給8080。"他們商量了一下,8080說:“警衛也是完成自己分内的事,那好吧,我把東西用保險箱(javascript)包裹起來(json轉化為jsonp),警衛就不認得了,你回去用我們約定的密碼(callback)打開就行了。”(使用jsonp實作跨域)。
這種方法确實可行,一段時間後,兩個大佬覺得挺麻煩的,8080說,給你個令牌(響應頭上增加相應字段)算了,那着令牌警衛就不會攔你了。果然,簡單了許多。
又過了一段時間,8081想:"我是大佬哎,讓我每天拿着令牌進進出出,這不損我形象嗎?"打電話給8080,說:"既然咱們都是大佬,還讓警衛操心幹嘛,以後我直接去找你,咱倆喝喝茶,聊聊天不是更好。"經過一段時間的接觸,8080和8081關系也不錯了,8080爽快地答應了。(隐藏跨域,大佬背後交接)

跨域錯誤.png

2.為什麼?
[1] 浏覽器出于安全的限制,而不是伺服器
[2] 跨域:協定/域名/端口必須一緻
[3] XHR請求(XMLHttpRequest)
           

二.解決思路

1: 浏覽器放方:8080大佬讓浏覽器警衛隊不要阻攔

浏覽器不校驗跨域.png

2: jsonp:需要後端修改資料格式,前端修改接受方式
普通ajax請求的Type是:xhr           傳回的是json字元串      
jsonp的ajax請求的Type是:script     傳回的是js腳本          url後有一段callback參數
           

json和jsonp.png

點選各種url檢視:
jsonp傳回的:動态建立是一段js腳本,用完再删除
/**/jQuery33103437422192155124_1532262222426({"data":"say Ok"});
json傳回的:隻是json
{"data":"say Ok"}
           
jsonp實作步驟
後端:AbstractJsonpResponseBodyAdvice方法過時,沒查到新的方法,但也能用
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
    public JsonpAdvice() {
        super("callback");
    }
}
           
8080端頁面中:
<script>
    var baseUrl = 'http://localhost:8080/ajax';//
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;//每個測試用例耗時時間
        //單元測試用例
        it("jsonp", function (done) {
            var result;//儲存傳回結果
            $.ajax({
                url: baseUrl,
                dataType: "jsonp",//jsonp格式
                // cache: true,//結果可被緩存
                jsonp:'callback',//預設參數,與後端的callback字元串相對于
                success: function (json) {
                    result = json;
                }
            });

            $.getJSON(baseUrl, function (data) {
                result = data;
            });

            setTimeout(function () {
                expect(result).toEqual({
                    "data": "say Ok"
                });
                done()//完成校驗
            }, 100);
        })
    });
</script>
           

jsonp.png

弊端
[1]伺服器需要改動,若後端非己主宰,則無能為力
[2]隻支援GET
[3]發的不是XHR請求
           
3.令牌模式:
被調用方(服務端):響應頭上增加相應字段告訴浏覽器允許
8081跨域的請求頭有:Origin:http://localhost:8081
           

服務端打造令牌:Filter

com.toly1994.ajaxser.AjaxserApplication

@SpringBootApplication
public class AjaxserApplication {

    public static void main(String[] args) {
        SpringApplication.run(AjaxserApplication.class, args);
    }

    @Bean
    public FilterRegistrationBean registerFilter() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.addUrlPatterns("/*");//所有請求都經過這個Filter
        bean.setFilter(new CrosFilter());//設定過濾器
        return bean;
    }
}
           
com.toly1994.ajaxser.CrosFilter
package com.toly1994.ajaxser;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 作者:張風捷特烈
 * 時間:2018/7/22:21:44
 * 郵箱:[email protected]
 * 說明:CrosFilter
 */
public class CrosFilter implements javax.servlet.Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse rep = (HttpServletResponse) servletResponse;
        //允許8081通路:"http://localhost:8081"換為*表示允許所有
        rep.addHeader("Access-Control-Allow-Origin", "http://localhost:8081");
        //允許通路方法GET:GET"換為*表示允許所有
        rep.addHeader("Access-Control-Allow-Methods", "GET");
        filterChain.doFilter(servletRequest, rep);
    }

    @Override
    public void destroy() {

    }
}

           

跨域方式.png

4.非簡單請求:post json
4-1:簡單請求:先執行,後判斷
方法:GET HEAD POST
header:無自定義頭 
        Content-Type:rext/plain||multipart/form-data||application/x-www-form-urlencoded
           
4-2:非簡單請求:後判斷,先執行
put delete 方法的ajax
發送帶有json格式的ajax請求
帶自定義頭的ajax
           
4-3:Post請求傳Json

8080服務端暴露接口:com.toly1994.ajaxser.controller.AjaxController

@PostMapping(value = "/postJson")
    public ResultBean postJson(@RequestBody User user) {
        System.out.println(user);
        return new ResultBean("post:"+user.getName());
    }
           

8081服務端調用接口:

it("postJson", function (done) {
            var result;//儲存傳回結果
                $.ajax({
                    type:"post",//請求類型
                    url: baseUrl+"/postJson",
                    contentType:"application/json;charset=UTF-8",//請求内容類型
                    data:JSON.stringify({name: "toly"}),//資料JOSN.stringify
                    // cache: true,//結果可被緩存
                    success: function (json) {
                        result = json;
                    }
                });

            setTimeout(function () {
                expect(result).toEqual({
                    "data": "post:toly"//預期結果
                });
                done()//完成校驗
            }, 100);
        });
    });
           

post發送帶有json格式的ajax請求.png

4-4:既然是Header原因,那就放行呗:com.toly1994.ajaxser.CrosFilter#doFilter
rep.addHeader("Access-Control-Allow-Headers", "Content-Type");
           

請求成功.png

可以看到有兩個請求,其中一個是OPTIONS的預檢請求,下面一句對這個請求做緩存
  • com.toly1994.ajaxser.CrosFilter#doFilter
rep.addHeader("Access-Control-Max-Age","3600");//一小時内緩存預檢請求
           
5.帶Cookie的跨域
@GetMapping("/getCookie")
    private ResultBean getCookie(@CookieValue(value = "cookie") String cookie) {
        System.out.println("//////////////////////");
        return new ResultBean("getCookie:"+cookie);
    }
           
//單元測試用例
        it("getCookie", function (done) {
            var result;//儲存傳回結果
            $.ajax({
                type:"get",//請求類型
                url: baseUrl+"/getCookie",
                xhrFields:{
                    withCredentials: true//發送ajax請求時加cookie
                },
                success: function (json) {
                    result = json;
                }
            });

            setTimeout(function () {
                expect(result).toEqual({
                    "data": "getCookie:toly"//預期結果
                });
                done()//完成校驗
            }, 100);
        });
    });
           

8080種cookie.png

cookie.png

既然是Credentials原因,那就放行呗:com.toly1994.ajaxser.CrosFilter#doFilter
rep.addHeader("Access-Control-Allow-Credentials","true");//允許cookie
           
這隻解決了8081的跨域,怎麼能實作其他的呢?可以擷取請求頭中的Origin,動态設定。
HttpServletRequest req = (HttpServletRequest) servletRequest;
        String origin = req.getHeader("Origin");
        if (!StringUtils.isEmpty(origin)) {
            rep.addHeader("Access-Control-Allow-Origin", origin);
        }
           

6.跨域帶自定義頭

@GetMapping("/getHeader")//
    private ResultBean getHeader(@RequestHeader("x-header1") String header1,
    @RequestHeader("x-header2") String header2) {
        System.out.println(header1 + " " + header2);
        return new ResultBean(header1 + " " + header2);
    }
           
it("getHeaders", function (done) {
            var result;//儲存傳回結果
            $.ajax({
                type:"get",//請求類型
                url: baseUrl+"/getHeader",
                headers:{
                    "x-header1": "AAA"
                },
                beforeSend: function (xhr) {
                    xhr.setRequestHeader("x-header2","BBB")
                },
                success: function (json) {
                    result = json;
                }
            });

            setTimeout(function () {
                expect(result).toEqual({
                    "data": "AAA BBB"//預期結果
                });
                done()//完成校驗
            }, 100);
        });
           

自定義頭錯誤.png

解決方案:添加頭

//動态添加自定義頭
        String headers = req.getHeader("Access-Control-Request-Headers");
        if (!StringUtils.isEmpty(headers)) {
            System.out.println(headers);
            rep.addHeader("Access-Control-Allow-Headers", headers);
        }
           
7.調用方:隐藏跨域--越過浏覽器
暫略