环境:Spring3.2.5
本篇文章将通过一个古老的技术JSONP来考察在座的对SpringBoot中某些技术的掌握程度。
1. 简介
JSONP(JSON with Padding)是一种非官方的协议,主要用于解决浏览器跨域数据访问的问题。它利用HTML的<script>标签可以跨域加载资源的特性,通过服务器端生成包含JSON数据的JavaScript函数调用,并返回给客户端执行。客户端需要预先定义好回调函数,以便在数据加载完毕后接收并处理数据。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>
访问上面的页面,输出结果: