跨域
同源策略
同源策略是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,瀏覽器很容易受到 XSS、CSFR 等攻擊。同源策略會阻止一個域的 javascript 腳本和另外一個域的內容進行交互。所謂同源是指"協議+域名+端口"三者相同。
什麼是跨域
當一個請求 url 的協議、域名、端口三者之間任意一個與當前頁面 url 不同即爲跨域。
跨域有以下限制:
- 無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB 等存儲型內容
- 無法接觸非同源網頁的 DOM節點
- 無法向非同源地址發送 AJAX 請求
解決策略
解決跨域的方法很多,大致有以下下幾種:
- Jsonp(只支持 get 請求)
- CORS
- Iframe
- Proxy
本文先介紹使用 Jsonp 解決 Ajax 跨域問題,後續還會有 CORS、Proxy 解決跨域的文章。
客戶端
$.ajax({
type: 'get',
url: url,
dataType: 'jsonp',
jsonp: 'callback',
jsonpCallback: 'callback'
}).done(function (data) {
alert(data);
}).error(function (XMLHttpRequest,textStatus,errorThrown) {
alert('fail');
})
type: jsonp 只支持 get 請求
dataType:設定爲jsonp
jsonp: 若無顯示指定,默認爲callback
,傳遞給後臺,用以獲取 jsonp 的回調函數名
jsonpCallback:jsonp 的回調函數名,若無顯示指定,則自動生成類似jQuery11100431856629965818_1585317491198
的隨機函數名
該請求會自動在 url 後追加 ?callback=callbackMethodName&_=1585317491199
,其中 callback
即爲 jsonp
的屬性值,callbackMethodName
即爲 jsonpCallback
的屬性值。主要作用是告訴服務器我的本地回調函數叫做 callbackMethodName,請要把查詢結果傳入這個函數中,即 callbackMethodName({"name":"xiaoming","age":18})
這種形式。
服務端
方法一
@GetMapping("/test")
@ResponseBody
public String test() {
CommonResult commonResult = new CommonResult();
String responseStr = JSONObject.toJSONString(commonResult);
return "callback" + "(" + responseStr + ")";
}
“callback” 需要跟 Ajax 中 jsonpCallback 的屬性值一致
方法二
使用 FastJson 提供的 com.fasterxml.jackson.databind.util.JSONPObject
@GetMapping("/test")
@ResponseBody
public JSONPObject test() {
CommonResult commonResult = new CommonResult();
JSONPObject jo = new JSONPObject("callback", commonResult);
return jo;
}
“callback” 需要跟 Ajax 中 jsonpCallback 的屬性值一致
方法三
FastJson 對 Jsonp 提供了支持
配置
本實驗環境爲 SpringBoot 2.1.5.RELEASE,FastJson 1.2.58,採用 Java Config 形式實現。其他版本以及其他形式配置請參考在 Spring 中集成 Fastjson。
@Configuration
public class FastJsonConfigurer implements WebMvcConfigurer {
/**
* 設置 FastJson 作爲默認的 java 對象與 json 互相轉換的工具
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
// 創建配置類
FastJsonConfig config = new FastJsonConfig();
// 自定義格式化輸出
config.setSerializerFeatures(
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullBooleanAsFalse);
// 處理中文亂碼問題
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
converter.setSupportedMediaTypes(fastMediaTypes);
converter.setFastJsonConfig(config);
converters.add(0, converter);
}
/**
* 對 JSONP 支持
*/
@Bean
public JSONPResponseBodyAdvice jsonpResponseBodyAdvice() {
return new JSONPResponseBodyAdvice();
}
}
如果使用的 FastJson 版本小於 1.2.36 的話(強烈建議使用最新版本),在與 Spring MVC 4.X 版本集成時需使用 FastJsonHttpMessageConverter4。
SpringBoot 2.0.1 版本中加載 WebMvcConfigurer 的順序發生了變動,故需使用
converters.add(0, converter);
指定 FastJsonHttpMessageConverter 在 converters 內的順序,否則在 SpringBoot 2.0.1 及之後的版本中將優先使用 Jackson 處理。
使用
Fastjson 提供了 @ResponseJSONP
註解,該註解組合了 @ResponseBody
,也就意味着使用了 @ResponseJSONP
就沒必要再添加 @ResponseBody
了。另外該註解有個 callback
成員變量(默認值爲 callback
),即爲 Ajax 中 jsonp
屬性值。
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ResponseBody
public @interface ResponseJSONP {
String callback() default "callback";
}
@ResponseJSONP 可以修飾類或方法,若修飾在類上則該類下所有方法都生效,方法上優先級大於類。
@ResponseJSONP // 類級別
@Controller
@RequestMapping("/jsonp")
public class JsonpController {
@ResponseJSONP(callback = "callback") // 方法級別
@GetMapping("/test")
public Object test() {
...
}
}
源碼
@Order(-2147483648)
@ControllerAdvice
public class JSONPResponseBodyAdvice implements ResponseBodyAdvice<Object> {
public final Log logger = LogFactory.getLog(this.getClass());
public JSONPResponseBodyAdvice() {
}
// 當前消息轉換器爲 FastJsonHttpMessageConverter 同時類或方法上有 @ResponseJSONP 註解,纔會進行返回體封裝
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return FastJsonHttpMessageConverter.class.isAssignableFrom(converterType) && (returnType.getContainingClass().isAnnotationPresent(ResponseJSONP.class) || returnType.hasMethodAnnotation(ResponseJSONP.class));
}
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 先獲取方法上的註解
ResponseJSONP responseJsonp = (ResponseJSONP)returnType.getMethodAnnotation(ResponseJSONP.class);
if (responseJsonp == null) {
// 若方法上不存在則獲取類上的註解
responseJsonp = (ResponseJSONP)returnType.getContainingClass().getAnnotation(ResponseJSONP.class);
}
HttpServletRequest servletRequest = ((ServletServerHttpRequest)request).getServletRequest();
// 根據設置的 callback 獲取請求中的 callbackMethodName 回調函數名
String callbackMethodName = servletRequest.getParameter(responseJsonp.callback());
if (!IOUtils.isValidJsonpQueryParam(callbackMethodName)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid jsonp parameter value:" + callbackMethodName);
}
callbackMethodName = null;
}
// 包裝成類似 callbackMethodName({"name":"xiaoming","age":18})
JSONPObject jsonpObject = new JSONPObject(callbackMethodName);
jsonpObject.addParameter(body);
this.beforeBodyWriteInternal(jsonpObject, selectedContentType, returnType, request, response);
return jsonpObject;
}
public void beforeBodyWriteInternal(JSONPObject jsonpObject, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
}
protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) {
return FastJsonHttpMessageConverter.APPLICATION_JAVASCRIPT;
}
}
方法四
從上面源碼可以看出返回體封裝必須要求當前消息轉換器爲 FastJsonHttpMessageConverter,也就是必須要將 FastJsonHttpMessageConverter 作爲默認的 json 與 java 對象的轉換器。但是有些公司內部禁止使用 FastJson,這個時候我們可以仿照上面的源碼自定義 JSONPResponseBodyAdvice 實現 ResponseBodyAdvice,重寫 supports 和 beforeBodyWrite 方法。