SpringBoot實現jsonp跨域通訊
實現jsonp跨域通訊
實現基於jsonp的跨域通訊方案
原理
瀏覽器對非同源ajax請求有限制,不允許傳送跨域請求
目前跨域解決方案有兩種
- cros配置
- jsonp請求
cros為新規範,通過一個head請求詢問伺服器是否允許跨域,若不允許則被攔截
jsonp則為利用瀏覽器不限制js指令碼的同源性,通過動態建立script請求,伺服器傳遞迴一個js函式呼叫語法,瀏覽器端按照js函式正常呼叫回撥函式
實現思路
首先確定伺服器端應該如何返回資料
一次正確的jsonp請求,伺服器端應該返回如下格式資料
jQuery39948237({key:3})
複製程式碼
其中,jQuery39948237
{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