使用 Jsonp 解決 Ajax 跨域問題

跨域

同源策略

同源策略是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,瀏覽器很容易受到 XSS、CSFR 等攻擊。同源策略會阻止一個域的 javascript 腳本和另外一個域的內容進行交互。所謂同源是指"協議+域名+端口"三者相同。

什麼是跨域

當一個請求 url 的協議、域名、端口三者之間任意一個與當前頁面 url 不同即爲跨域。

跨域有以下限制:

  • 無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB 等存儲型內容
  • 無法接觸非同源網頁的 DOM節點
  • 無法向非同源地址發送 AJAX 請求

解決策略

解決跨域的方法很多,大致有以下下幾種:

  • Jsonp(只支持 get 請求)
  • CORS
  • Iframe
  • Proxy

本文先介紹使用 Jsonp 解決 Ajax 跨域問題,後續還會有 CORS、Proxy 解決跨域的文章。

客戶端

$.ajax({
    type: 'get',
    url: url,
    dataType: 'jsonp',
    jsonp: 'callback',
    jsonpCallback: 'callback' 
}).done(function (data) {
    alert(data);
}).error(function (XMLHttpRequest,textStatus,errorThrown) {
    alert('fail');
})

type: jsonp 只支持 get 請求
dataType:設定爲 jsonp
jsonp: 若無顯示指定,默認爲 callback,傳遞給後臺,用以獲取 jsonp 的回調函數名
jsonpCallback:jsonp 的回調函數名,若無顯示指定,則自動生成類似 jQuery11100431856629965818_1585317491198 的隨機函數名

該請求會自動在 url 後追加 ?callback=callbackMethodName&_=1585317491199,其中 callback 即爲 jsonp 的屬性值,callbackMethodName 即爲 jsonpCallback 的屬性值。主要作用是告訴服務器我的本地回調函數叫做 callbackMethodName,請要把查詢結果傳入這個函數中,即 callbackMethodName({"name":"xiaoming","age":18}) 這種形式。

服務端

方法一

@GetMapping("/test")
@ResponseBody
public String test() {
    CommonResult commonResult = new CommonResult();
    String responseStr = JSONObject.toJSONString(commonResult);
    return "callback" + "(" + responseStr + ")";
}

“callback” 需要跟 Ajax 中 jsonpCallback 的屬性值一致

方法二

使用 FastJson 提供的 com.fasterxml.jackson.databind.util.JSONPObject

@GetMapping("/test")
@ResponseBody
public JSONPObject test() {
    CommonResult commonResult = new CommonResult();
    JSONPObject jo = new JSONPObject("callback", commonResult);
    return jo;
}

“callback” 需要跟 Ajax 中 jsonpCallback 的屬性值一致

方法三

FastJson 對 Jsonp 提供了支持

配置

本實驗環境爲 SpringBoot 2.1.5.RELEASE,FastJson 1.2.58,採用 Java Config 形式實現。其他版本以及其他形式配置請參考在 Spring 中集成 Fastjson

@Configuration
public class FastJsonConfigurer implements WebMvcConfigurer {
    
    /**
     * 設置 FastJson 作爲默認的 java 對象與 json 互相轉換的工具
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        // 創建配置類
        FastJsonConfig config = new FastJsonConfig();
        // 自定義格式化輸出
        config.setSerializerFeatures(
                SerializerFeature.DisableCircularReferenceDetect,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteNullStringAsEmpty,
                SerializerFeature.WriteNullBooleanAsFalse);
        // 處理中文亂碼問題
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        converter.setSupportedMediaTypes(fastMediaTypes);
        converter.setFastJsonConfig(config);
        converters.add(0, converter);
    }

    /**
     * 對 JSONP 支持
     */
    @Bean
    public JSONPResponseBodyAdvice jsonpResponseBodyAdvice() {
        return new JSONPResponseBodyAdvice();
    }
}

如果使用的 FastJson 版本小於 1.2.36 的話(強烈建議使用最新版本),在與 Spring MVC 4.X 版本集成時需使用 FastJsonHttpMessageConverter4。

SpringBoot 2.0.1 版本中加載 WebMvcConfigurer 的順序發生了變動,故需使用 converters.add(0, converter); 指定 FastJsonHttpMessageConverter 在 converters 內的順序,否則在 SpringBoot 2.0.1 及之後的版本中將優先使用 Jackson 處理。

使用

Fastjson 提供了 @ResponseJSONP 註解,該註解組合了 @ResponseBody,也就意味着使用了 @ResponseJSONP 就沒必要再添加 @ResponseBody 了。另外該註解有個 callback 成員變量(默認值爲 callback ),即爲 Ajax 中 jsonp 屬性值。

@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ResponseBody
public @interface ResponseJSONP {
    String callback() default "callback";
}

@ResponseJSONP 可以修飾類或方法,若修飾在類上則該類下所有方法都生效,方法上優先級大於類。

@ResponseJSONP // 類級別
@Controller
@RequestMapping("/jsonp")
public class JsonpController {

    @ResponseJSONP(callback = "callback") // 方法級別
    @GetMapping("/test")
    public Object test() {
       ...
    }
}

源碼

@Order(-2147483648)
@ControllerAdvice
public class JSONPResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    public final Log logger = LogFactory.getLog(this.getClass());

    public JSONPResponseBodyAdvice() {
    }
    // 當前消息轉換器爲 FastJsonHttpMessageConverter 同時類或方法上有 @ResponseJSONP 註解,纔會進行返回體封裝
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return FastJsonHttpMessageConverter.class.isAssignableFrom(converterType) && (returnType.getContainingClass().isAnnotationPresent(ResponseJSONP.class) || returnType.hasMethodAnnotation(ResponseJSONP.class));
    }

    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 先獲取方法上的註解
        ResponseJSONP responseJsonp = (ResponseJSONP)returnType.getMethodAnnotation(ResponseJSONP.class);
        if (responseJsonp == null) {
            // 若方法上不存在則獲取類上的註解
            responseJsonp = (ResponseJSONP)returnType.getContainingClass().getAnnotation(ResponseJSONP.class);
        }

        HttpServletRequest servletRequest = ((ServletServerHttpRequest)request).getServletRequest();

        // 根據設置的 callback 獲取請求中的 callbackMethodName 回調函數名
        String callbackMethodName = servletRequest.getParameter(responseJsonp.callback());
        if (!IOUtils.isValidJsonpQueryParam(callbackMethodName)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invalid jsonp parameter value:" + callbackMethodName);
            }

            callbackMethodName = null;
        }
        // 包裝成類似 callbackMethodName({"name":"xiaoming","age":18})
        JSONPObject jsonpObject = new JSONPObject(callbackMethodName);
        jsonpObject.addParameter(body);
        this.beforeBodyWriteInternal(jsonpObject, selectedContentType, returnType, request, response);
        return jsonpObject;
    }

    public void beforeBodyWriteInternal(JSONPObject jsonpObject, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
    }

    protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) {
        return FastJsonHttpMessageConverter.APPLICATION_JAVASCRIPT;
    }
}

方法四

從上面源碼可以看出返回體封裝必須要求當前消息轉換器爲 FastJsonHttpMessageConverter,也就是必須要將 FastJsonHttpMessageConverter 作爲默認的 json 與 java 對象的轉換器。但是有些公司內部禁止使用 FastJson,這個時候我們可以仿照上面的源碼自定義 JSONPResponseBodyAdvice 實現 ResponseBodyAdvice,重寫 supports 和 beforeBodyWrite 方法。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章