场景:
现有一组对外提供服务的http api,返回一个Result,序列化后json如下
{"code":200,"message":"成功","data":null}
其中data为业务数据,为了避免在返回给终端用户之前,被非法劫持篡改,需要在返回之前,通过一个约定的算法,生成一个签名,将该签名放入响应的header中。前端接受到返回后,需要比较body和签名是否匹配。
解决办法:
实现ResponseBodyAdvice接口。
实现思路:
我们查看ResponseBodyAdvice的源码。
/* * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.servlet.mvc.method.annotation; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.lang.Nullable; /** * Allows customizing the response after the execution of an {@code @ResponseBody} * or a {@code ResponseEntity} controller method but before the body is written * with an {@code HttpMessageConverter}. * * <p>Implementations may be registered directly with * {@code RequestMappingHandlerAdapter} and {@code ExceptionHandlerExceptionResolver} * or more likely annotated with {@code @ControllerAdvice} in which case they * will be auto-detected by both. * * @author Rossen Stoyanchev * @since 4.1 * @param <T> the body type */ public interface ResponseBodyAdvice<T> { /** * Whether this component supports the given controller method return type * and the selected {@code HttpMessageConverter} type. * @param returnType the return type * @param converterType the selected converter type * @return {@code true} if {@link #beforeBodyWrite} should be invoked; * {@code false} otherwise */ boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); /** * Invoked after an {@code HttpMessageConverter} is selected and just before * its write method is invoked. * @param body the body to be written * @param returnType the return type of the controller method * @param selectedContentType the content type selected through content negotiation * @param selectedConverterType the converter type selected to write to the response * @param request the current request * @param response the current response * @return the body that was passed in or a modified (possibly new) instance */ @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }
可以发现其有两个方法。
1.supports。此方法可以写我们自己的逻辑,判断哪些方法需求增强。
2.beforeBodyWrite。此方法是给客户端响应之前执行,我们就在其中添加一个名为sign的参数。
实现:
@ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice<Result> { /** * 控制器增强配置类 */ @Autowired private ControllerAdviceConfig config; @Override public boolean supports(MethodParameter methodParameter, Class aClass) { //读取配置,判断哪些方法不需要生成签名 String methodName=methodParameter.getMethod().getName(); List list = config.getIgnore(); return !list.contains(methodName); } private String generateSign(String body){ //根据body计算签名。。。 return body; } @Override public Result beforeBodyWrite(Result result, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (result == null) { return result; } String bodyStr = result.toString(); String sign = generateSign(bodyStr); response.getHeaders().add("sign", sign); return result; } }
需要注意的是Result的toString()方法已经重写,重写的规则需要与客户端约定好,最终生成一致的字符串,再对字符串用一致的哈希算法,计算签名。