跨域
同源策略
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 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 方法。