# spring-boot-sign **Repository Path**: Debugman/spring-boot-sign ## Basic Information - **Project Name**: spring-boot-sign - **Description**: SpringBoot请求响应加签、验签 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 10 - **Created**: 2022-11-30 - **Last Updated**: 2022-11-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 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.添加拦截注解 * 主要为了标识加签验签规则 ```java @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface SignProcess { /** * 请求参数是否验签 */ boolean verify() default true; /** * 响应结果是否加签 */ boolean sign() default true; } ``` ### 2.添加配置application.properties ```properties # 注意:添加MD5密钥 signKey=1234567890abcdef ``` ### 3.实现前置验签处理逻辑 ```java @ControllerAdvice public class MyRequestBodyAdvice implements RequestBodyAdvice { @Value("${signKey:}") private String secret; @Override public boolean supports(MethodParameter methodParameter, Type type, Class> 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> aClass) throws IOException { HttpHeaders headers = httpInputMessage.getHeaders(); //源请求参数 String bodyStr = StreamUtils.copyToString(httpInputMessage.getBody(), Charset.forName("utf-8")); //转换成TreeMap结构 TreeMap map = JsonUtil.parse(bodyStr, new TypeReference>() {}); //校验签名 SignUtil.verify(map, secret); Map out = new HashMap<>(); for (Map.Entry 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> aClass) { return o; } @Override public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class> 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.实现后置加签处理逻辑 ```java @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 map = new TreeMap<>(); Iterator> fields = json.fields(); while(fields.hasNext()){ Map.Entry entry = fields.next(); map.put(entry.getKey(), JsonUtil.toStr(entry.getValue())); } //加签 SignUtil.sign(map, secret); return Response.success(map); } } } } return o; } } ``` ### 5.加签验签工具类 ```java 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 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 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 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.测试代码 * 请求对象数据体 ```java @NoArgsConstructor @Data public class DemoReqDTO implements Serializable { private static final long serialVersionUID = 1019466745376831818L; private List k10; private K3Bean k3; private Integer k9; private String k2; private String k1; private List 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; } } ``` * 响应对象数据体 ```java @NoArgsConstructor @Data @Accessors(chain = true) public class DemoRespDTO implements Serializable { private static final long serialVersionUID = 1019466745376831818L; private Integer a; private BBean b; private List e; @NoArgsConstructor @Data @Accessors(chain = true) public static class BBean { private String c; private String d; } } ``` * 统一封装数据体 ```java @Data @NoArgsConstructor @AllArgsConstructor public class Response 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 Response success(R data) { return new Response<>(SUCCESS, "success", data); } public static Response error(String msg) { return new Response<>(ERROR, msg, null); } @JsonIgnore public boolean isOk() { return null != getCode() && SUCCESS == getCode(); } } ``` * 测试代码 ```java @RestController public class DemoController { /** * @param reqDTO * @return */ @SignProcess @PostMapping(value = "/test") public Response 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 map = new TreeMap<>(); Iterator> fields = JsonUtil.read(rawJsonStr).fields(); while(fields.hasNext()){ Map.Entry 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 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" }' ``` * 响应结果 ```json { "code": 200, "message": "success", "data": { "a": "1", "b": "{\"c\":\"ccc\",\"d\":\"ddd\"}", "e": "[\"e1\",\"e2\"]", "randStr": "0.5463553287284311", "sign": "4EEC0B7D25D39702FE0FC0933D9FDA63", "timeStamp": "1625563969" } } ```