SpringBoot请求响应参数防篡改
概述
- 有时候,为了接口安全,防止接口数据被篡改,我们需要对请求,响应参数进行加签、验签。
- 支持复杂请求参数验签。
- 定义签名规则如下:
必填参数:
timeStamp:时间戳,用于校验请求是否过期
randStr:随机串
sign:签名值,用于校验请求参数是否被篡改
规则:
1. 加入时间戳和随机字符串参数
2. 所有请求参数key按字典序排序
3. 如果value是非基本数据类型,是对象或数组时,转换成json字符串
4. 过滤掉所有value为空的字段
5. 将排序后的key和value进行拼接,最后加上密钥key,规则:key1=value1&key2=value2 ... &key=xxx
6. 将第5步得到的字符串进行MD5加密,然后转换成大写字母,最终生成即为sign的值
实现流程
1.添加拦截注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SignProcess {
boolean verify() default true;
boolean sign() default true;
}
2.添加配置application.properties
# 注意:添加MD5密钥
signKey=1234567890abcdef
3.实现前置验签处理逻辑
@ControllerAdvice
public class MyRequestBodyAdvice implements RequestBodyAdvice {
@Value("${signKey:}")
private String secret;
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
SignProcess process = methodParameter.getMethodAnnotation(SignProcess.class);
return null != process && process.verify();
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
HttpHeaders headers = httpInputMessage.getHeaders();
String bodyStr = StreamUtils.copyToString(httpInputMessage.getBody(), Charset.forName("utf-8"));
TreeMap<String, String> map = JsonUtil.parse(bodyStr, new TypeReference<TreeMap<String, String>>() {});
SignUtil.verify(map, secret);
Map<String, Object> out = new HashMap<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
out.put(entry.getKey(), JsonUtil.read(entry.getValue()));
}
String outStr = JsonUtil.toStr(out);
return new MyHttpInputMessage(headers, outStr.getBytes(Charset.forName("utf-8")));
}
@Override
public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return o;
}
@Override
public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return o;
}
@AllArgsConstructor
public static class MyHttpInputMessage implements HttpInputMessage {
private HttpHeaders headers;
private byte[] body;
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(body);
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
}
4.实现后置加签处理逻辑
@ControllerAdvice
@Slf4j
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
@Value("${signKey:}")
private String secret;
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
SignProcess process = methodParameter.getMethodAnnotation(SignProcess.class);
return null != process && process.sign();
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (o instanceof Response) {
Response res = (Response) o;
if (res.isOk()) {
Object data = res.getData();
if (null != data) {
JsonNode json = JsonUtil.beanToNode(data);
if (json.isObject()) {
TreeMap<String, String> map = new TreeMap<>();
Iterator<Map.Entry<String, JsonNode>> fields = json.fields();
while(fields.hasNext()){
Map.Entry<String, JsonNode> entry = fields.next();
map.put(entry.getKey(), JsonUtil.toStr(entry.getValue()));
}
SignUtil.sign(map, secret);
return Response.success(map);
}
}
}
}
return o;
}
}
5.加签验签工具类
public class SignUtil {
private static final String TIMESTAMP_KEY = "timeStamp";
private static final String RAND_KEY = "randStr";
private static final String SIGN_KEY = "sign";
private static final Long EXPIRE_TIME = 15 * 60L;
public static String sign(TreeMap<String, String> map, String key) {
if (!map.containsKey(TIMESTAMP_KEY)) {
map.put(TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis() / 1000));
}
if (!map.containsKey(RAND_KEY)) {
map.put(RAND_KEY, String.valueOf(new Random().nextDouble()));
}
StringBuilder buf = new StringBuilder();
for (Map.Entry<String, String> entry : map.entrySet()) {
if (!SIGN_KEY.equals(entry.getKey()) && StrUtil.isNotBlank(entry.getValue())) {
buf.append("&").append(entry.getKey()).append("=").append(entry.getValue());
}
}
String preSign = buf.substring(1) + "&key=" + key;
String sign = MD5.create().digestHex(preSign).toUpperCase();
if (!map.containsKey(SIGN_KEY)) {
map.put(SIGN_KEY, sign);
}
return sign;
}
public static void verify(TreeMap<String, String> map, String key) {
if (StrUtil.isBlank(map.get(TIMESTAMP_KEY))
|| StrUtil.isBlank(map.get(RAND_KEY))
|| StrUtil.isBlank(map.get(SIGN_KEY))) {
throw new MyException("必填参数为空");
}
long timeStamp = Long.valueOf(map.get(TIMESTAMP_KEY));
long expireTime = timeStamp + EXPIRE_TIME;
if (System.currentTimeMillis() / 1000 > expireTime) {
throw new MyException("请求已过期");
}
String sign = sign(map, key);
if (!Objects.equals(sign, map.get(SIGN_KEY))) {
throw new MyException("签名错误");
}
}
}
6.测试代码
@NoArgsConstructor
@Data
public class DemoReqDTO implements Serializable {
private static final long serialVersionUID = 1019466745376831818L;
private List<Integer> k10;
private K3Bean k3;
private Integer k9;
private String k2;
private String k1;
private List<K6Bean> k6;
@NoArgsConstructor
@Data
public static class K3Bean {
private String k4;
private String k5;
}
@NoArgsConstructor
@Data
public static class K6Bean {
private String k7;
private Integer k8;
}
}
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class DemoRespDTO implements Serializable {
private static final long serialVersionUID = 1019466745376831818L;
private Integer a;
private BBean b;
private List<String> e;
@NoArgsConstructor
@Data
@Accessors(chain = true)
public static class BBean {
private String c;
private String d;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> implements Serializable {
private static final long serialVersionUID = 4921114729569667431L;
private Integer code;
private String message;
private T data;
public static final int SUCCESS = 200;
public static final int ERROR = 1000;
public static <R> Response<R> success(R data) {
return new Response<>(SUCCESS, "success", data);
}
public static <R> Response<R> error(String msg) {
return new Response<>(ERROR, msg, null);
}
@JsonIgnore
public boolean isOk() {
return null != getCode() && SUCCESS == getCode();
}
}
@RestController
public class DemoController {
@SignProcess
@PostMapping(value = "/test")
public Response<DemoRespDTO> test(@RequestBody DemoReqDTO reqDTO) {
DemoRespDTO respDTO = new DemoRespDTO();
respDTO.setA(1);
respDTO.setB(new DemoRespDTO.BBean().setC("ccc").setD("ddd"));
respDTO.setE(Arrays.asList("e1", "e2"));
return Response.success(respDTO);
}
public static void main(String[] args) {
String rawJsonStr = "{\"k10\":[1,2],\"k3\":{\"k4\":\"v4\",\"k5\":\"v5\"},\"k6\":[{\"k7\":\"v7\",\"k8\":8}],\"k9\":9,\"k2\":\"v2\",\"k1\":\"v1\"}";
TreeMap<String, String> map = new TreeMap<>();
Iterator<Map.Entry<String, JsonNode>> fields = JsonUtil.read(rawJsonStr).fields();
while(fields.hasNext()){
Map.Entry<String, JsonNode> entry = fields.next();
map.put(entry.getKey(), JsonUtil.toStr(entry.getValue()));
}
SignUtil.sign(map, "1234567890abcdef");
System.out.println("原始json串:" + rawJsonStr);
System.out.println("实际请求参数:" + JsonUtil.toStr(map));
}
}
7.测试效果
curl -X POST \
http://localhost:8080/test \
-H 'Content-Type: application/json' \
-d '{
"k1": "v1",
"k10": "[1,2]",
"k2": "v2",
"k3": "{\"k4\":\"v4\",\"k5\":\"v5\"}",
"k6": "[{\"k7\":\"v7\",\"k8\":8}]",
"k9": 9,
"randStr": "0.32433307072478823",
"sign": "D387A0E49F217D60444A9AF1E90579B6",
"timeStamp": "1625563875"
}'
{
"code": 200,
"message": "success",
"data": {
"a": "1",
"b": "{\"c\":\"ccc\",\"d\":\"ddd\"}",
"e": "[\"e1\",\"e2\"]",
"randStr": "0.5463553287284311",
"sign": "4EEC0B7D25D39702FE0FC0933D9FDA63",
"timeStamp": "1625563969"
}
}
- git项目: https://gitee.com/hweiyu/spring-boot-sign.git