SpringBoot请求响应参数防篡改

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();
    }

    /**
     * @param httpInputMessage
     * @param methodParameter
     * @param type
     * @param aClass
     * @return
     * @throws IOException
     */
    @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结构
        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;
    }

    /**
     * 自定义消息体,因为org.springframework.http.HttpInputMessage#getBody()只能调一次,所以要重新封装一个可重复读的消息体
     */
    @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();
    }

    /**
     * @param o
     * @param methodParameter
     * @param mediaType
     * @param aClass
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @return
     */
    @Override
    public 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
    @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;

    //状态码,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);
    }

    @JsonIgnore
    public 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项目: https://gitee.com/hweiyu/spring-boot-sign.git
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值