SpringBoot實現jsonp跨域通訊

實現jsonp跨域通訊

實現基於jsonp的跨域通訊方案javascript

原理

瀏覽器對非同源ajax請求有限制,不容許發送跨域請求
目前跨域解決方案有兩種前端

  • cros配置
  • jsonp請求

cros爲新規範,經過一個head請求詢問服務器是否容許跨域,若不容許則被攔截
jsonp則爲利用瀏覽器不限制js腳本的同源性,經過動態建立script請求,服務器傳遞迴一個js函數調用語法,瀏覽器端按照js函數正常調用回調函數java

實現思路

首先肯定服務器端應該如何返回數據jquery

一次正確的jsonp請求,服務器端應該返回以下格式數據ios

jQuery39948237({key:3})

複製代碼

其中,jQuery39948237爲瀏覽器端要執行的函數名,該函數由ajax庫動態建立,並將函數名做爲一個請求參數和該次請求的其他參數一併發送,服務器端無需對此參數作過多處理git

{key:3}爲這次請求返回的數據,做爲函數參數傳遞github


其次,服務器端如何處理?web

爲了兼容jsonp和cros方案,服務器端應該在請求帶有函數名參數時返回函數調用,不然正常返回json數據便可ajax


最後,爲了減小代碼的侵入,不該該將上述流程放入一個Controller正常邏輯中,應該考慮使用aop實現spring

實現

前端

前端本次使用jquery庫~~(原本想用axios庫的,可是axios不支持jsonp)~~

代碼以下

$.ajax({
        url:'http://localhost:8999/boot/dto',
        dataType:"jsonp",
        success:(response)=>{
            this.messages.push(response);
        }
    })

複製代碼

Jquery默認jsonp函數名參數name爲callback

後端

本次採用aop實現

具體思路爲: 給Controller添加後切點,判斷request是否有函數名參數,若是有則修改返回的數據,沒有則不作處理

而aop又有兩種方案

  • 常規aop,本身定義切點
  • ResponseBodyAdvice,Spring提供的可直接用於數據返回的工具類

本次使用第二種方案


首先是Controller的接口實現

@RequestMapping("dto")
public Position dto() {
    return new Position(239, 43);
}
複製代碼

返回一個複雜類型,Spring會自動對其作json序列化操做


而後的ResponseBodyAdvice實現

該類全路徑爲:org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice

/** * 處理controller返回值,對於有callback值的使用jsonp格式,其他不處理 */
@RestControllerAdvice(basePackageClasses = IndexController.class)
public class JsonpAdvice implements ResponseBodyAdvice {

    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private ObjectMapper mapper;

    //jquery默認是callback,其他jsonp庫可能不同
    private final String callBackKey = "callback";

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        logger.debug("返回的class={}", aClass);
        return true;
    }

    /** * 在此處對返回值進行處理,須要特別注意若是是非String類型,會被Json序列化,從而添加了雙引號,解決辦法見 * * @param body 返回值 * @param methodParameter 方法參數 * @param mediaType 當前contentType,非String類型爲json * @param aClass convert的class * @param serverHttpRequest request,暫時支持是ServletServerHttpRequest類型,其他類型將會原樣返回 * @param serverHttpResponse response * @return 若是body是String類型,加上方法頭後返回,若是是其餘類型,序列化後返回 * @see com.inkbox.boot.demo.converter.Jackson2HttpMessageConverter */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

        if (body == null)
            return null;
        // 若是返回String類型,media是plain,不然是json,將會通過json序列化,在下方返回純字符串以後依然會被序列化,就會添上多餘的雙引號
        logger.debug("body={},request={},response={},media={}", body, serverHttpRequest, serverHttpResponse, mediaType.getSubtype());


        if (serverHttpRequest instanceof ServletServerHttpRequest) {
            HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();

            String callback = request.getParameter(callBackKey);

            if (!StringUtils.isEmpty(callback)) {
                //使用了jsonp
                if (body instanceof String) {
                    return callback + "(\"" + body + "\")";
                } else {
                    try {
                        String res = mapper.writeValueAsString(body);
                        logger.debug("轉化後的返回值={},{}", res, callback + "(" + res + ")");

                        return callback + "(" + res + ")";
                    } catch (JsonProcessingException e) {
                        logger.warn("【jsonp支持】數據body序列化失敗", e);
                        return body;
                    }
                }
            }
        } else {
            logger.warn("【jsonp支持】不支持的request class ={}", serverHttpRequest.getClass());
        }
        return body;
    }
}
複製代碼

使用@RestControllerAdvice指明切點

bug

通過此步驟,理論上便可實現jsonp調用了。

然而實際測試發現,因爲Spring json序列化策略的問題,若是返回jsonp字符串,json序列化以後,將會添上一對引號,以下

應該返回

Jquery332({"x":239,"y":43})
複製代碼

實際返回

"Jquery332({\"x\":239,\"y\":43})"

複製代碼

致使瀏覽器端沒法正常運行函數


經多方查找資料後得知

因爲在ResponseBodyAdvice中修改了實際的返回值類型爲String,而字符串類型通過Jackson序列化後就會加上引號

解決辦法爲:修改默認的json序列化MessageConverter處理邏輯,對於實際是String的不作處理

代碼以下

@Component
public class Jackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        if (object instanceof String) {
            //繞開實際上返回的String類型,不序列化
            Charset charset = this.getDefaultCharset();
            StreamUtils.copy((String) object, charset, outputMessage.getBody());
        } else {
            super.writeInternal(object, type, outputMessage);
        }
    }
}


@Configuration
public class MvcConfig implements WebMvcConfigurer {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private MappingJackson2HttpMessageConverter converter;

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// MappingJackson2HttpMessageConverter converter = mappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(new LinkedList<MediaType>() {{
            add(MediaType.TEXT_HTML);
            add(MediaType.APPLICATION_JSON_UTF8);
        }});
        converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
        converters.add(converter);
    }
}

複製代碼

todo

暫時不明白爲何須要兩個類搭配使用

代碼

具體實現可查閱github

相關文章
相關標籤/搜索