springmvc 自定义消息转换器完整例子
问题描述:
最近在项目中对接第三方接口,采用http协议,post方法,协议类型:Content-Type: application/json;charset=utf-8,将用户名和密码等信息放在header中,用于验证请求。将业务数据放到body体中,并使用3DES加密。
- 请求报文样例如下:
POST /api/GetParkingPaymentInfo HTTP/1.1
Content-Type: application/json;charset=utf-8
user: 123453
pwd: qwerew
Host: 220.160.112.124:9096
Content-Length: 43
Expect: 100-continue
{"data":"DkTwRsUUza33A8/TvrocXI3r+Az1T7bt"}
每个接口都是此加密验证方式,但是我不想再每个controller方法中都校验解密一次,故而想到使用springmvc 的自定义消息转换器,在消息转换器中先解密,然后将报文转换为对应的java对象,controller入参直接是java对象,这样校验用户名密码和解密就可以单独处理了。
验证用户名和密码,使用拦截器实现
因为用户名和密码放到了header中,可以在拦截器中获取请求头,判断用户名和密码是否正确。
- 创建拦截器
@Component
public class KeyTopInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
private static final String MIME_JSON = "application/json;charset=UTF-8";
@Value("${keytop.user}")
private String ktuser;
@Value("${keytop.pwd}")
private String ktpwd;
//在请求进入controller前进行拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String user = request.getHeader("user");
String pwd = request.getHeader("pwd");
String host = request.getHeader("Host");
log.info("===校验科托请求头中的用户名和密码,url={},user={},pwd={},host={}",request.getRequestURI(),user,pwd,host);
if(ktuser.equals(user) && ktpwd.equals(pwd)){
return true;
}else{
log.info("===校验科托失败,配置的用户名和密码与传递的不一致,配置的ktuser={},ktpwd={}",ktuser,ktpwd);
//根据接口要求返回错误信息
PrintWriter writer = response.getWriter();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", MIME_JSON);
response.setContentType(MIME_JSON);
BaseKeyTopRes<?> baseKeyTopRes = new BaseKeyTopRes<>();
baseKeyTopRes.setFaileInfo("user or pwd incorrectness");
response.setStatus(HttpStatus.OK.value());
writer.write(JSONObject.toJSON(baseKeyTopRes).toString());
writer.close();
return false;
}
}
}
- 配置拦截器
@Configuration
@EnableWebMvc
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
@Autowired
private KeyTopInterceptor keyTopInterceptor;
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加科托拦截器
registry.addInterceptor(keyTopInterceptor)
.addPathPatterns("/keytop/**");
}
}
body体解密,转换为java对象
例如有个接口的data字段为:{“data”:{"platno":"A1234"}},获取到的参数为加密之后的:{"data":"DkTwRsUUza33A8/TvrocXI3r+Az1T7bt"}。
- 创建消息转换器
为了使创建的消息转换器只转换本次业务新增的接口,创建一个请求基类bean对象,没有任何字段,只是实现Serializable接口,作为其他业务的父类,例如:BaseKeyTopReq
public class BaseKeyTopReq implements Serializable{
}
创建消息转换器如下:
public class KeyTopMsgConverter extends AbstractHttpMessageConverter<BaseKeyTopReq> {
private static final Logger logger = LoggerFactory.getLogger(KeyTopMsgConverter.class);
//科托3DES加解密需要的key
private String ktkey;
//科托3DES加解密需要的偏移量
private String ktiv;
public KeyTopMsgConverter(MediaType supportedMediaType,String ktkey,String ktiv) {
super(supportedMediaType);
this.ktiv=ktiv;
this.ktkey=ktkey;
}
/**
* 如果支持 true支持
* 会调用 readInternal 将http消息 转换成方法中被@RequestBody注解的参数
* 会调用writeInternal 将被@ResponseBody注解的返回对象转换成数据字节响应给浏览器
*/
@Override
protected boolean supports(Class<?> clazz) {
//判断父类是否为BaseKeyTopReq,如果是则使用该转换器
if(clazz.getSuperclass() == BaseKeyTopReq.class){
return true;
}
return false;
}
/**
*解析请求的参数
*/
@Override
protected BaseKeyTopReq readInternal(Class<? extends BaseKeyTopReq> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
//获取body信息
InputStream is=inputMessage.getBody();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
StringBuilder stringBuilder = new StringBuilder();
br.lines().forEach(item->stringBuilder.append(item));
logger.info("科托解密之前数据:"+stringBuilder.toString());
JSONObject jsonObject = JSON.parseObject(stringBuilder.toString());
String data = jsonObject.getString("data");
//解密
try {
String desString = ThreeDESUtil.getDesString(data,ktkey,ktiv);
logger.info("科托解密之后数据:"+desString);
//将解密出来的信息转换为java对象,注意该对象必须继承BaseKeyTopReq
return JSONObject.parseObject(desString,clazz);
} catch (Exception e) {
logger.error("科托解密失败",e);
throw new BizException(ErrorType.DECODE_ERROR);
}
}
/**
* 响应给对象的参数
* 将方法被@ResponseBody注解的返回对象转换成数据字节响应给浏览器
*/
@Override
protected void writeInternal(BaseKeyTopReq baseKeyTopReq, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
}
}
- 配置转换器
@Configuration
@EnableWebMvc
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
@Value("${keytop.key}")
private String ktkey;
@Value("${keytop.iv}")
private String ktiv;
/**
* 扩展消息转换器
* 注意不能使用configureMessageConverters方法,使用configureMessageConverters方法,则只包含你新增的,springmvc默认的消息转换器没有了。
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//增加科托消息转换器
KeyTopMsgConverter converter = new KeyTopMsgConverter(MediaType.APPLICATION_JSON,ktkey,ktiv);
converters.add(0,converter);//将自定义的设置为优先级最高
}
}
使用测试
例如有个接口PostFreeParkingSpace,data字段信息为:plateNo,json格式。
则可以创建一个PostFreeParkingSpaceReq对象,继承BaseKeyTopReq。
//响应接口对应的对象
public class PostFreeParkingSpaceReq extends BaseKeyTopReq {
private String plateNo;
public String getPlateNo() {
return plateNo;
}
public void setPlateNo(String plateNo) {
this.plateNo = plateNo;
}
}
则接口调用方,发送的body体数据为:{"data":"DkTwRsUUza33A8/TvrocXI3r+Az1T7bt"},经过消息转换器解密(只解密data内容)之后为:{"plateNo":"A12345"},然后将该json字符串转换为java对象。
则在controller中入参对象里面就有值了。
- controller
@RestController
@RequestMapping("/keytop")
public class KeyTopController {
private static final Logger logger = LoggerFactory.getLogger(PubParkingController.class);
@RequestMapping(value = "/PostFreeParkingSpace", method = RequestMethod.POST)
public String PostFreeParkingSpace(@RequestBody PostFreeParkingSpaceReq spaceReq) {
logger.info("科托空闲车位上报:" + JSON.toJSONString(spaceReq));
/**此时从入参对象中获取的plateNo值则为A12345,已经解密完且转换成了对应的实体对象*/
return spaceReq.getPlateNo();
}
}