借一古老技術(shù)考察你對(duì)SpringBoot掌握程度
環(huán)境:Spring3.2.5
本篇文章將通過一個(gè)古老的技術(shù)JSONP來考察在座的對(duì)SpringBoot中某些技術(shù)的掌握程度。
1. 簡(jiǎn)介
JSONP(JSON with Padding)是一種非官方的協(xié)議,主要用于解決瀏覽器跨域數(shù)據(jù)訪問的問題。它利用HTML的<script>標(biāo)簽可以跨域加載資源的特性,通過服務(wù)器端生成包含JSON數(shù)據(jù)的JavaScript函數(shù)調(diào)用,并返回給客戶端執(zhí)行??蛻舳诵枰A(yù)先定義好回調(diào)函數(shù),以便在數(shù)據(jù)加載完畢后接收并處理數(shù)據(jù)。JSONP簡(jiǎn)單易用,但僅支持GET請(qǐng)求,且存在安全風(fēng)險(xiǎn),如XSS攻擊和CSRF攻擊。隨著技術(shù)的發(fā)展,CORS等更安全的跨域解決方案逐漸取代了JSONP。
關(guān)于JSONP的應(yīng)用示例
現(xiàn)有如下接口地址:http://localhost:9100/jsonps,返回?cái)?shù)據(jù)如下:
[{"id":1,"name":"張三"},{"id":2,"name":"李四"},{"id":3,"name":"王五"}]
JSONP需要我們傳遞一個(gè)類似回調(diào)的參數(shù),服務(wù)端拿到值后會(huì)將最終的響應(yīng)數(shù)據(jù)拼接成javascript函數(shù)調(diào)用的形式,如下:
<script src="http://localhost:9100/jsonps?callback=getUsers"></script>
通過<script>標(biāo)簽引用上面的即可地址,同時(shí)傳遞了callback參數(shù),當(dāng)請(qǐng)求到達(dá)服務(wù)端后會(huì)拿到callback參數(shù)對(duì)應(yīng)的getUsers值,與真正的數(shù)據(jù)做拼接,如下:
getUsers([{"id":1,"name":"張三"},{"id":2,"name":"李四"},{"id":3,"name":"王五"}]);
上面將是服務(wù)端響應(yīng)的最終結(jié)果。這就是javascript函數(shù)的調(diào)用,我們只要保證前端頁(yè)面中有g(shù)etUsers函數(shù)即可,它會(huì)自動(dòng)的執(zhí)行該函數(shù)。
以上就是JSONP實(shí)現(xiàn)的基本原理。
思考:我們的服務(wù)端又該實(shí)現(xiàn)呢?直接在對(duì)應(yīng)的接口中進(jìn)行修改嗎?如果直接修改接口,那么當(dāng)我又希望返回的是數(shù)據(jù)又該如何,重新再來一個(gè)接口嗎?
接下來我們通過HttpMessageConverter和ResponseBodyAdvice來實(shí)現(xiàn)即支持原始數(shù)據(jù)又支持JSONP格式的數(shù)據(jù)響應(yīng)。
2. 實(shí)戰(zhàn)案例
2.1 Rest接口定義
@RestController
@RequestMapping(("/jsonps"))
public class JsonpController {
static List<User> DATAS = List.of(new User(1L, "張三"), new User(2L, "李四"), new User(3L, "王五")) ;
@GetMapping("")
public List<User> queryUsers() {
return DATAS ;
}
}
接口非常簡(jiǎn)單直接返回List集合。
2.2 自定義JSON包裝器
public class JsonpMappingJacksonValue extends MappingJacksonValue {
private String jsonpFunction ;
public JsonpMappingJacksonValue(Object value) {
super(value);
}
// getters, setters
}
該類繼承了MappingJacksonValue,同時(shí)增加了jsonpFunction的屬性,后面會(huì)根據(jù)該屬性是否有值對(duì)結(jié)果進(jìn)行處理,如果沒有值則原始返回。而MappingJacksonValue類的作用就是一個(gè)POJO序列化到JSON時(shí)提供額外的序列號(hào)指令。
SpringBoot默認(rèn)響應(yīng)JSON數(shù)據(jù)是通過MappingJackson2HttpMessageConverter類,在該類中的writeInternal方法中會(huì)判斷當(dāng)前輸出的值是否是MappingJacksonValue,如果是最終也會(huì)獲取其中的Value進(jìn)行輸出客戶端的。
2.3 自定義ResponseBodyAdvice
@ControllerAdvice
public class JsonpControllerAdvice implements ResponseBodyAdvice<Object> {
// 參數(shù)值必須滿足該正則
private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");
// 參數(shù)名稱默認(rèn)callback,你也可以通過配置方式設(shè)置
private String jsonpQueryParamName = "callback" ;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 只要轉(zhuǎn)換器是jackson(json數(shù)據(jù)輸出)
// 當(dāng)然你也可以自定義實(shí)現(xiàn),比如:方法上有具體的某個(gè)注解等
return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
}
@Override
public Object beforeBodyWrite(
Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
// 創(chuàng)建MappingJacksonValue對(duì)象(包裝原始的數(shù)據(jù))
JsonpMappingJacksonValue container = this.getOrCreateContainer(body) ;
// 取得請(qǐng)求的callback參數(shù)值
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest() ;
String value = servletRequest.getParameter(jsonpQueryParamName) ;
// 如果不存在直接返回,不做任何處理
if (value != null) {
// 不滿足條件也直接返回
if (!CALLBACK_PARAM_PATTERN.matcher(value).matches()) {
return container ;
}
// 設(shè)置響應(yīng)頭為:application/javascript;charset=utf-8
MediaType contentTypeToUse = new MediaType("application", "javascript", StandardCharsets.UTF_8) ;
response.getHeaders().setContentType(contentTypeToUse) ;
// 設(shè)置jsonp函數(shù)名,后面就會(huì)根據(jù)該值判斷是否要進(jìn)行處理
container.setJsonpFunction(value) ;
}
return container ;
}
// ...
}
自定義ResponseBodyAdvice的作用是將返回客戶端的數(shù)據(jù)包裝為MappingJacksonValue對(duì)象,然后設(shè)置jsonp會(huì)調(diào)用函數(shù)名。
接下來就是最重要的,如何在寫入客戶端時(shí),將數(shù)據(jù)改造成JSONP所需要的格式。
2.4 重寫HttpMessageConverter
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() {
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
// 我們上面設(shè)置的值在這里用上了,關(guān)鍵就在該值是否有
// 只有有值的情況下我們才會(huì)進(jìn)行JSONP的處理
String jsonpFunction =
(object instanceof JsonpMappingJacksonValue ? ((JsonpMappingJacksonValue) object).getJsonpFunction() : null);
if (jsonpFunction != null) {
generator.writeRaw("/**/");
generator.writeRaw(jsonpFunction + "(");
}
}
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
String jsonpFunction =
(object instanceof JsonpMappingJacksonValue ? ((JsonpMappingJacksonValue) object).getJsonpFunction() : null);
if (jsonpFunction != null) {
generator.writeRaw(");") ;
}
}
} ;
return converter ;
}
在這里我們自定義了MappingJackson2HttpMessageConverter 的writePrefix和writeSuffix方法,這兩個(gè)方法都進(jìn)行判斷,如果期望輸出的是JSONP格式才會(huì)進(jìn)行數(shù)據(jù)處理。
到此就完成了所有處理過程,每一步你都懂嗎?
說明:本篇文章不是教你實(shí)現(xiàn)JSONP這個(gè)技術(shù)并使用它,JSONP本就是用來解決跨域的問題,我用CORS技術(shù)不比它簡(jiǎn)單,安全。這里只是借用這個(gè)JSONP來檢驗(yàn)?zāi)銓?duì)其它知識(shí)的掌握程度。
驗(yàn)證上面的代碼
不使用callback參數(shù)請(qǐng)求
圖片
使用callback參數(shù)請(qǐng)求
圖片
成功,當(dāng)你的頁(yè)面中有g(shù)etUsers方法時(shí),會(huì)自動(dòng)調(diào)用getUsers方法。
通過HTML頁(yè)面進(jìn)行測(cè)試
<html>
<head>
<script>
function getUsers(users) {
alert(JSON.stringify(users))
}
</script>
<script src="http://localhost:9100/jsonps?callback=getUsers"></script>
</head>
</html>
訪問上面的頁(yè)面,輸出結(jié)果: