Java实现接口防篡改


一、定义

在客户端与服务端请求交互的过程中,请求的数据容易被拦截并篡改,比如在支付场景中,请求支付金额为 10 元,被拦截后篡改为 100 元,由于没有防篡改校验,导致多支付了金钱,造成了用户损失。因此我们在接口设计时必须考虑防篡改校验,加签、验签就是用来解决这个问题的。划重点,敲黑板:加签、验签是用来解决防篡改问题的。

签名主要包含摘要和非对称加密两部分内容,首先对需要签名的数据进行摘要计算得到摘要值,然后通过签名者的私钥对摘要值进行非对称加密即可得到签名结果。

验签主要包含摘要、非对称解密、摘要比对三部分内容,首页对接收到的数据进行摘要计算得到验签方摘要值,然后通过签名者的公钥对摘要值进行非对称解密得到签名方摘要值,将签名方摘要值与验签方摘要值进行比对,如果相等则验签成功,否则验签失败。

二、签名

1、参数排序
将需要签名的内容根据参数名称进行排序,排序规则按照第一个字符的ASCII码值递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的ASCII码递增排序,以此类推。将参数内容进行排序,可以保证签名、验签双方参数内容的一致性。

为什么会产生不一致?

签名方以 Json 格式将参数内容发送给验签方,验签方需要将 Json 格式的参数内容反序列化为对象,由于验签方可能使用不同的编程语言,不同的 Json 框架,所以会导致双方的参数顺序不一致。

2、参数拼接
将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待摘要字符串。

3、摘要计算
通过摘要算法求待摘要字符串的摘要值,常用的摘要算法如MD5、SHA、HMAC等。

4、非对称加密
使用非非对称加密算法,利用客户端的私钥对摘要值进行加密,生成内容我们称之为签名。

5、发送请求
将参数内容、字符编码、签名方法(非对称加密算法)、签名发送给验签方。

验签
验签方收到请求后进行验签。

三、相应工具类

1、SHA256Util加密算法工具类:

public class SHA256Util {

    /**
     * @param str 加密前的报文
     * @Author: Mr.ZJW
     * @Description: 用java原生的摘要实现SHA256加密
     * @Date: 2022/5/5 14:14
     **/
    public static String getSHA256String(String str) {
        String encodeStr = "";
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(str.getBytes("UTF-8"));
            encodeStr = byte2Hex(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return encodeStr;
    }

    /**
     * @param [bytes]
     * @Author: Mr.ZJW
     * @Description: byte[]转为16进制
     * @Date: 2022/5/5 14:15
     **/
    private static String byte2Hex(byte[] bytes) {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) {
            String temp = Integer.toHexString(bytes[i] & 0xFF);
            if (temp.length() == 1) {
                stringBuffer.append("0");
            }
            stringBuffer.append(temp);
        }
        return stringBuffer.toString();
    }
}

2、生产签名工具类

/**
 * @Author: Mr.ZJW
 * @Date: 2022-05-05 11:36
 * @Description: 生产签名工具类
 */
public class SignUtil {

    private static String secret = "e10adc3949ba59abbe56e057f20f883f";

    /**
     * @param [map]
     * @Author: Mr.ZJW
     * @Description: 根据Map生成签名
     * @Date: 2022/5/5 11:38
     **/
    public static String generatorSign(Map<String, Object> map) {
        map.remove("sign");
        //排序
        Map<String, Object> stringObjectMap = MapSortUtil.sortMapByKey(map);
        //转格式
        Set<Map.Entry<String, Object>> entries = stringObjectMap.entrySet();
        //存放StringBuilder
        StringBuilder sb = new StringBuilder();
        //遍历
        for (Map.Entry<String, Object> entry : entries) {
            sb.append(entry.getKey() + ":" + entry.getValue()).append("&");
        }
        //组装secret
        sb.append("secret").append(secret);
        //生产签名
        return SHA256Util.getSHA256String(sb.toString());
    }

    /**
     * @Author: Mr.ZJW
     * @Description: 校验签名
     * @param [map]
     * @Date: 2022/5/6 11:11
    **/
    public static Boolean checkSign(Map<String,Object> map){
        String sign = (String) map.get("sign");
        map.remove("sign");
        //生产Sign
        String signGenera = generatorSign(map);
        //校验Sign
        if (signGenera.equals(sign)){
            return true;
        }
        return false;
    }
}

3、Map排序工具类

/**
 * @Author: Mr.ZJW
 * @Date: 2022-05-05 10:59
 * @Description: Map排序工具类
 */
public class MapSortUtil {

    /**
     * @Author: Mr.ZJW
     * @Description: Map排序工具类
     * @Date: 2022/5/5 11:01
     **/
    public static Map<String, Object> sortMapByKey(Map<String, Object> map) {
        //判断是否为空
        if (ObjectUtils.isEmpty(map)) {
            throw new RuntimeException("输入参数为空");
        }
        //排序
        Map<String, Object> sortMap = new TreeMap<>(new MyMapComparator());
        sortMap.putAll(map);
        return sortMap;
    }

    static class MyMapComparator implements Comparator<String> {
        @Override
        public int compare(String o1, String o2) {
            return o1.compareTo(o2);
        }
    }
}

四、测试get请求,参数写url上

1、测试内容,这里简单测试两个appId以及name

    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("appId", 1);
        map.put("name", "jowell");
        String s = generatorSign(map);
        System.out.println("s = " + s);
    }

在这里插入图片描述
2、controller代码

    /**
     * @Author: Mr.ZJW
     * @Description: get请求,参数写url上
     * @param [sign, request]
     * @Date: 2022/5/5 17:22
    **/
    @GetMapping("/getTest")
    public String getTest(String sign,HttpServletRequest request){
        HashMap<String, Object> map = new HashMap<>();
        // 获取get中的参数
        Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()){
            //获取name
            String parametename = parameterNames.nextElement();
            // 获取值
            String parameterValue = request.getParameter(parametename);
            map.put(parametename,parameterValue);
        }
        //排序
        Map<String, Object> map1 = MapSortUtil.sortMapByKey(map);
        //生产签名
        String sign1 = SignUtil.generatorSign(map1);
        //判断签名
        if (sign.equals(sign1)){
            return "success";
        }
        return "error";
    }

3、启动项目测试效果
如下图测试成功:
在这里插入图片描述
我们把appId内容改为2,可以可以看到请求接口失败,无论是改了内容还是改了签名,都请求不成功,这样就防止了第三方而已者篡改接口内容
在这里插入图片描述

五、post请求,参数放入body中

测试内容同上

1、把请求参数封装为实体类

@Data
public class SignDTO {

    private String appId;

    private String name;

    private String sign;
}

2、controller

    /**
     * @Author: Mr.ZJW
     * @Description: post请求,参数放入body中
     * @param [signDTO]
     * @Date: 2022/5/5 17:09
    **/
    @PostMapping("/postTest")
    public String postTest(@RequestBody SignDTO signDTO) {
        //JSON转对象
        JSONObject jsonObject = JSONUtil.parseObj(signDTO);
        //转Map
        Map<String, Object> map = Convert.toMap(String.class, Object.class, jsonObject);
        //排序
        Map<String, Object> map1 = MapSortUtil.sortMapByKey(map);
        System.out.println("map1 = " + map1);
        //生成
        String sign = SignUtil.generatorSign(map1);
        //判断签名
        if (sign.equals(signDTO.getSign())){
            return "校验通过";
        }
        return "校验失败";
    }

3、启动项目测试
在这里插入图片描述
在这里插入图片描述

但通过上面代码可以看到,代码非常冗余,每次写一次controller都得写签名校验,下面把签名验证改为统一过滤器。

六、使用过滤器配置接口防篡改

一、相关工具类

1、Sign过滤器类

@Slf4j
@Component
public class SignAuthFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
		//ServletRequest转HttpServletRequest
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        //获取请求路径
        final String uri = req.getRequestURI().startsWith("/") ? req.getRequestURI().substring(1) : req.getRequestURI();
        //对以下请求放行
        if(uri.contains("user/getCaptcha")){
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }


        //转HttpServletRequest以及HttpServletResponse
        HttpServletRequest request = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) servletRequest);
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //获取请求参数工具类,包括是get或post
        SortedMap<String, Object> allParams = HttpParamUtil.getAllParams(request);
        log.info("所有请求参数:{}", allParams);
        //校验签名
        Boolean flag = SignUtil.checkSign(allParams);
        if (flag){
            filterChain.doFilter(request, response);
        }else {
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("msg","签名不正确");
            jsonObject.put("code",-1);
            writer.println(jsonObject);
        }
    }

    @Override
    public void destroy() {
    }

}

2、保存过滤器里面的流工具类

/**
 * @Author: Mr.ZJW
 * @Description: 保存过滤器里面的流
 * @Date: 2022/5/6 15:03
**/
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
 
    private final byte[] body;
 
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        String sessionStream = getBodyString(request);
        body = sessionStream.getBytes(Charset.forName("UTF-8"));
    }
 
    /**
     * 获取请求Body
     *
     * @param request
     * @return
     */
    public String getBodyString(final ServletRequest request) {
 
        StringBuilder sb = new StringBuilder();
        try (
            InputStream inputStream = cloneInputStream(request.getInputStream());
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")))
        ) {
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }
 
    /**
     * Description: 复制输入流</br>
     */
    public InputStream cloneInputStream(ServletInputStream inputStream) {
 
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = inputStream.read(buffer)) > -1) {
                byteArrayOutputStream.write(buffer, 0, len);
            }
            byteArrayOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    }
 
    @Override
    public BufferedReader getReader() {
 
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
 
    @Override
    public ServletInputStream getInputStream() {
 
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
 
            @Override
            public int read() {
 
                return bais.read();
            }
 
            @Override
            public boolean isFinished() {
 
                return false;
            }
 
            @Override
            public boolean isReady() {
 
                return false;
            }
 
            @Override
            public void setReadListener(ReadListener readListener) {
 
            }
        };
    }
}

3、获取请求参数工具类,不管是get或post

/**
 * @Author: Mr.ZJW
 * @Date: 2022-05-06 9:30
 * @Description: 获取请求参数工具类,不管是get或post
 */
public class HttpParamUtil {

    /**
     * @param [request]
     * @Author: Mr.ZJW
     * @Description: 获取请求中的所以参数,包括get或post
     * @Date: 2022/5/6 10:25
     **/
    public static SortedMap<String, Object> getAllParams(HttpServletRequest request) throws IOException {
        //总的参数map
        SortedMap<String, Object> allMap = new TreeMap<>();
        //获取URL上的参数
        if (StringUtils.isNotEmpty(request.getQueryString())) {
            Map<String, Object> urlParams = getUrlParams(request);
            //遍历URL上的参数
            for (Map.Entry entry : urlParams.entrySet()) {
                allMap.put((String) entry.getKey(), entry.getValue());
            }
        }
        //获取Body上的参数
        Map<String, String> bodyParams = getBodyParams(request);
        if (ObjectUtils.isNotEmpty(bodyParams)) {
            //遍历Body上的参数
            for (Map.Entry entry : bodyParams.entrySet()) {
                allMap.put((String) entry.getKey(), entry.getValue());
            }
        }
        return allMap;
    }

    /**
     * @param [request]
     * @Author: Mr.ZJW
     * @Description: 获取Body上的参数
     * @Date: 2022/5/6 9:52
     **/
    private static Map<String, String> getBodyParams(HttpServletRequest request) throws IOException {
        //读取Body中的参数
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()));
        StringBuilder sb = new StringBuilder();
        String s = "";
        while (null != (s = bufferedReader.readLine())) {
            sb.append(s);
        }
        //转Map
        return JSONObject.parseObject(sb.toString(), Map.class);
    }

    /**
     * @param [request]
     * @Author: Mr.ZJW
     * @Description: 获取URL上的参数
     * @Date: 2022/5/6 9:52
     **/
    private static Map<String, Object> getUrlParams(HttpServletRequest request) {
        String queryParam = "";
        try {
            //查询URL上的请求参数
            queryParam = URLDecoder.decode(request.getQueryString(), "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        HashMap<String, Object> result = new HashMap<>();
        //分隔:如//hello?a=1&b=2  分隔&
        String[] split = queryParam.split("&");
        //遍历
        for (String s : split) {
            int i = s.indexOf("=");
            result.put(s.substring(0, i), s.substring(i + 1));
        }
        return result;
    }

}

二、测试

1、controller代码,如下可以看到代码清晰了很多,只关注业务代码即可

    /**
     * @param [sign, request]
     * @Author: Mr.ZJW
     * @Description: get请求,参数写url上
     * @Date: 2022/5/5 17:22
     **/
    @GetMapping("/getTest")
    public String getTest(String sign, HttpServletRequest request) {
        System.out.println("进入get请求,参数写url上方法");
        return "getTest";
    }
    
    /**
     * @param [signDTO]
     * @Author: Mr.ZJW
     * @Description: post请求,参数放入body中
     * @Date: 2022/5/5 17:09
     **/
    @PostMapping("/postTest")
    public String postTest(@RequestBody SignDTO signDTO) {
        System.out.println("进入post请求,参数放入body中方法");
        return "postTest";
    }

2、测试

在这里插入图片描述
post请求测试

在这里插入图片描述
如下可以看到,只要我修改了内容就验证不通过,就判定接口是被第三方恶意篡改过的
在这里插入图片描述
get请求测试同上,自行测试

在这里插入图片描述

  • 11
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喝汽水的猫^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值