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