1. 程式人生 > 程式設計 >SpringBoot實現jsonp跨域通訊

SpringBoot實現jsonp跨域通訊

實現jsonp跨域通訊

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

原理

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

  • cros配置
  • jsonp請求

cros為新規範,通過一個head請求詢問伺服器是否允許跨域,若不允許則被攔截
jsonp則為利用瀏覽器不限制js指令碼的同源性,通過動態建立script請求,伺服器傳遞迴一個js函式呼叫語法,瀏覽器端按照js函式正常呼叫回撥函式

實現思路

首先確定伺服器端應該如何返回資料

一次正確的jsonp請求,伺服器端應該返回如下格式資料


jQuery39948237({key:3})

複製程式碼

其中,jQuery39948237

為瀏覽器端要執行的函式名,該函式由ajax庫動態建立,並將函式名作為一個請求引數和該次請求的其餘引數一併傳送,伺服器端無需對此引數做過多處理

{key:3}為此次請求返回的資料,作為函式引數傳遞


其次,伺服器端如何處理?

為了相容jsonp和cros方案,伺服器端應該在請求帶有函式名引數時返回函式呼叫,否則正常返回json資料即可


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

實現

前端

前端本次使用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