SpringBoot请求响应参数防篡改

编程入门 行业动态 更新时间:2024-10-10 21:33:31

SpringBoot请求响应<a href=https://www.elefans.com/category/jswz/34/1771441.html style=参数防篡改"/>

SpringBoot请求响应参数防篡改

SpringBoot请求响应参数防篡改

概述

  • 有时候,为了接口安全,防止接口数据被篡改,我们需要对请求,响应参数进行加签、验签。
  • 支持复杂请求参数验签。
  • 定义签名规则如下:
必填参数: timeStamp:时间戳,用于校验请求是否过期randStr:随机串sign:签名值,用于校验请求参数是否被篡改规则:1. 加入时间戳和随机字符串参数2. 所有请求参数key按字典序排序3. 如果value是非基本数据类型,是对象或数组时,转换成json字符串4. 过滤掉所有value为空的字段5. 将排序后的key和value进行拼接,最后加上密钥key,规则:key1=value1&key2=value2 ... &key=xxx6. 将第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;@Overridepublic boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {SignProcess process = methodParameter.getMethodAnnotation(SignProcess.class);//如果带有注解且标记为验签,测进行验签操作return null != process && process.verify();}/*** @param httpInputMessage* @param methodParameter* @param type* @param aClass* @return* @throws IOException*/@Overridepublic 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结构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")));}@Overridepublic Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {return o;}@Overridepublic Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {return o;}/*** 自定义消息体,因为org.springframework.http.HttpInputMessage#getBody()只能调一次,所以要重新封装一个可重复读的消息体*/@AllArgsConstructorpublic static class MyHttpInputMessage implements HttpInputMessage {private HttpHeaders headers;private byte[] body;@Overridepublic InputStream getBody() throws IOException {return new ByteArrayInputStream(body);}@Overridepublic HttpHeaders getHeaders() {return headers;}}}

4.实现后置加签处理逻辑

@ControllerAdvice
@Slf4j
public class MyResponseBodyAdvice implements ResponseBodyAdvice {@Value("${signKey:}")private String secret;@Overridepublic boolean supports(MethodParameter methodParameter, Class aClass) {SignProcess process = methodParameter.getMethodAnnotation(SignProcess.class);//如果带有注解且标记为加签,测进行加签操作return null != process && process.sign();}/*** @param o* @param methodParameter* @param mediaType* @param aClass* @param serverHttpRequest* @param serverHttpResponse* @return*/@Overridepublic Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {//如果是rest接口统一封装返回对象if (o instanceof Response) {Response res = (Response) o;//如果返回成功if (res.isOk()) {Object data = res.getData();if (null != data) {JsonNode json = JsonUtil.beanToNode(data);//仅处理object类型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";//过期时间,15分钟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@Datapublic static class K3Bean {private String k4;private String k5;}@NoArgsConstructor@Datapublic 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;//状态码,200为成功,其它为失败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);}@JsonIgnorepublic boolean isOk() {return null != getCode() && SUCCESS == getCode();}}
  • 测试代码
@RestController
public class DemoController {/*** @param reqDTO* @return*/@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) {//创建测试使用的json串//原始json串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");//原始json串System.out.println("原始json串:" + rawJsonStr);//测试请求json串,value值为对象或数组的情况,都转换为json串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项目: .git

更多推荐

SpringBoot请求响应参数防篡改

本文发布于:2024-03-10 09:49:41,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1727639.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:参数   SpringBoot

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!