Java对外接口签名(Signature)实现方案

为什么要加密验签? 防止报文明文传输

        数据在网络传输过程中,容易被抓包。如果使用的是HTTP协议的请求/响应(Request OR Response),它是明文传输的,都是可以被截获、篡改、重放(重发)的。所以需要进行数据的加密验签,所以需要考虑以下几点。

  1. 防伪装攻击(案例:在公共网络环境中,第三方 有意或恶意 的调用我们的接口)
  2. 防篡改攻击(案例:在公共网络环境中,请求头/查询字符串/内容 在传输过程被修改)
  3. 防重放攻击(案例:在公共网络环境中,请求被截获,稍后被重放或多次重放)
  4. 防数据信息泄漏(案例:截获用户登录请求,截获到账号、密码等)

实现方式

        常见的方式,就是对关键字段加密。比如查询订单接口,就可以对订单号进行加密。一般常用的加密算法对称加密算法(如:AES),或者哈希算法处理(如:MD5)

对称加密:加密和解密使用相同秘钥的加密算法

采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。

非对称加密:非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥和私钥是成对存在的,如果用公钥对数据加密,只有对应的私钥才能解密。 (非对称加密是更安全的做法,加密是算法RSASM2

非对称加密算法需要两个密钥来进行加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。

加签验签:使用Hash算法(如 MD5或者SHA-256)把原始请求参数生成报文摘要,然后用私钥对这个摘要进行加密,得到报文对应的sign

加签:用Hash函数把原始报文生成报文摘要,然后用私钥对这个摘要进行加密,就得到这个报文对应的数字签名。通常来说呢,请求方会把「数字签名和报文原文」一并发送给接收方。

验签:接收方拿到原始报文和数字签名后,用「同一个Hash函数」从报文中生成摘要A。另外,用对方提供的公钥对数字签名进行解密,得到摘要B,对比A和B是否相同,就可以得知报文有没有被篡改过。


客户端操作

请求参数

字段

类型

必传

说明

sign

String

接口签名,用户接口验证

app_id

String

开放平台的APP_ID,例如:1234

date_time

String

当前时间戳

key

String

开发平台的APP_KEY,例如:XA12#Da

name

String

业务参数

age

Integer

业务参数

业务参数消息体数据格式:Content-Type 指定为 application/json

1.将请求参数中除sign外的多个键值对,根据键按照字典序排序,并按照"key1=value1&key2=value2..."的格式拼成一个字符串

String sortStr=" age=11&app_id=1234&date_time=1656926899731&name=xxx"

2.将key拼接在第一步中排序后的字符串后面得到待签名字符串 

String sortStr ="age=11&app_id=1234&date_time=1656926899731&name=xxxkey=XA12#Da"

 3.使用md5算法加密待加密字符串并转为大写即为sign

String sign ="57A132B7585F77B1948812275BE945B8"

4.将sign添加到请求参数中 

https://www.baidu.com/test/get?age=11&app_id=1234&date_time=1656926899731&name=xxx&sign=57A132B7585F77B1948812275BE945B8

需要注意以下重要规则

◆ 请求参数中有中文时,中文需要经过url编码,但计算签名时不需要;

◆ 请求参数的值为空则不参与签名;

◆ 参数名区分大小写;

◆ sign参数不参与签名;

服务端操作

  1. 接收到请求参数,转JSON格式
  2. 验签
    1. 拿出用户签名
    2. 根据APP_ID 拿去数据库中的KEY,使用该KEY进行重签参数
    3. 如果重签结果和用户签名一致则通过,否则返回签名错误
    4. 校验参数中的时间戳,如果时间戳 超过当前时间5分钟则签名失效
  3. 如果c、d都通过则正常请求业务

实现 

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>3.0.6</version>
</dependency>
<!--json-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.78</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

服务端拦截器:

package com.cykj.card.filter;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.groovy.util.concurrent.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import org.apache.groovy.util.concurrent.concurrentlinkedhashmap.Weighers;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;

/**
 * @author 小影
 * @create 2022-07-04 10:59
 * @describe:
 */
@Slf4j
@Order(1)
@WebFilter
@Component
public class ReqFilter implements Filter {
    // 热点缓存
    public static ConcurrentLinkedHashMap<String, String> cache = new ConcurrentLinkedHashMap.Builder<String, String>()
            .maximumWeightedCapacity(30).weigher(Weighers.singleton()).build();

    /**
     * 初始化
     *
     * @param filterConfig
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // key=APPID ,value=密钥  在数据库中加载出来,这里为了演示写死
        cache.put("1234", "XA12#Da");
    }

    /**
     * 签名验证时间(TIMES =分钟 * 秒 * 毫秒)
     * 当前设置为:5分钟有效期
     */
    protected static final Integer TIMES = 5 * 60 * 1000;

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        // 判断请求方式
        String method = request.getMethod();
        if ("POST".equals(method)) {
            log.info("POST请求进入...");
            // 获取请求Body参数,需要使用 BodyReaderHttpServletRequestWrapper进行处理
            // 否则会出现异常:I/O error while reading input message; nested exception is java.io.IOException: Stream closed
            // 原因就是在拦截器已经读取了请求体中的内容,这时候Request请求的流中已经没有了数据
            // 解决流只能读取一次的问题:先读取流,然后在将流重新写进去就行了
            ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
            String body = HttpHelper.getBodyString(requestWrapper);
            String bodyString = URLDecoder.decode(body, "utf-8");
            if (StrUtil.isEmpty(bodyString)) {
                Map<String, Object> map = new HashMap<>();
                response.setCharacterEncoding("utf-8");
                PrintWriter writer = response.getWriter();
                response.setHeader("Content-type", "application/json;charset=UTF-8");
                response.setCharacterEncoding("utf-8");
                map.put("code", 7000);
                map.put("msg", "请求参数不能为空");
                writer.print(JSONObject.toJSONString(map));
                writer.close();
                return;
            }
            // 解析参数转JSON格式
            // bodyString = "{" + bodyString.replace("&", "',").replace("=", ":'") + "'}";
            JSONObject jsonObject = JSONObject.parseObject(bodyString);
            // 验签
            boolean validation = validation(jsonObject, response);
            if (!validation) {
                return;
            }
            log.info("POST请求验签通过...");
            // 放行
            chain.doFilter(request, response);
        }
        if ("GET".equals(method)) {
            log.info("GET请求进入...");
            // 获取请求参数
            Map<String, String> allRequestParam = getAllRequestParam(request);
            Set<Map.Entry<String, String>> entries = allRequestParam.entrySet();
            // 参数转JSON格式
            JSONObject jsonObject = new JSONObject();
            entries.forEach(key -> {
                jsonObject.put(key.getKey(), key.getValue());
            });
            // 验签
            boolean validation = validation(jsonObject, response);
            if (!validation) {
                return;
            }
            log.info("GET请求验签通过...");
            // 放行
            chain.doFilter(request, response);

        }
    }
    
    /**
     * 验签
     *
     * @param body     请求参数
     * @param response
     * @return
     * @throws IOException
     */
    private boolean validation(JSONObject body, HttpServletResponse response) throws IOException {
        // 拿出请求签名
        String sign = body.getString("sign");
        body.remove("sign");
        // 根据APPID查询的密钥进行重签
        String sign1 = getSign(body);


        Map<String, Object> map = new HashMap<>();
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
        // 校验签名
        if (!StringUtils.equals(sign1, sign)) {// APPID查询的密钥进行签名 和 用户签名进行比对
            map.put("code", 10000);
            map.put("msg", "签名错误");
            writer.print(JSONObject.toJSONString(map));
            return false;
        }
        // 校验签名是否失效
        long thisTime = System.currentTimeMillis() - body.getLong("date_time");
        if (thisTime > TIMES) {// 比对时间是否失效
            map.put("code", 10000);
            map.put("msg", "签名失效");
            writer.print(JSONObject.toJSONString(map));
            return false;
        }
        return true;
    }

    /**
     * 计算签名
     *
     * @param params
     * @return
     */
    public static String getSign(JSONObject params) {
        // 从缓存中获取密钥
        String key = cache.get(params.getString("app_id"));
        if (StringUtils.isBlank(key)) {
            key = "XA12#Da";// 如果为nulll密钥就从DB中查询,这里演示就写死
            cache.put(params.getString("app_id"), key);// 放入缓存
        }
        // 参数进行字典排序
        String sortStr = getFormatParams(params);
        // 将密钥key拼接在字典排序后的参数字符串中,得到待签名字符串。
        sortStr += "key=" + key;
        // sortStr += "key=xxxxx";
        // 使用md5算法加密待加密字符串并转为大写即为sign
        String sign = SecureUtil.md5(sortStr).toUpperCase();
        return sign;
    }

    /**
     * 参数字典排序
     *
     * @param params
     * @return
     */
    private static String getFormatParams(Map<String, Object> params) {
        List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(params.entrySet());
        Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {
            public int compare(Map.Entry<String, Object> arg0, Map.Entry<String, Object> arg1) {
                return (arg0.getKey()).compareTo(arg1.getKey());
            }
        });
        String ret = "";
        for (Map.Entry<String, Object> entry : infoIds) {
            ret += entry.getKey();
            ret += "=";
            ret += entry.getValue();
            ret += "&";
        }
        return ret;
    }

    /**
     * 获取客户端GET请求中所有的请求参数
     *
     * @param request
     * @return
     */
    private Map<String, String> getAllRequestParam(final HttpServletRequest request) {
        Map<String, String> res = new HashMap<String, String>();
        Enumeration<?> temp = request.getParameterNames();
        if (null != temp) {
            while (temp.hasMoreElements()) {
                String en = (String) temp.nextElement();
                String value = request.getParameter(en);
                res.put(en, value);
                //如果字段的值为空,判断若值为空,则删除这个字段>
                if (null == res.get(en) || "".equals(res.get(en))) {
                    res.remove(en);
                }
            }
        }
        return res;
    }
}

防止流丢失:

package com.cykj.card.filter;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Enumeration;

/**
 * @author 小影
 * @create 2022-07-04 10:59
 * @describe:
 */
public class BodyReaderHttpServletRequestWrapper extends  
        HttpServletRequestWrapper {  
      
    private final byte[] body;  
  
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {  
        super(request);  
        System.out.println("-------------------------------------------------");    
        Enumeration e = request.getHeaderNames()   ;    
         while(e.hasMoreElements()){    
             String name = (String) e.nextElement();    
             String value = request.getHeader(name);    
             System.out.println(name+" = "+value);    
                 
         }    
        body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));  
    }  
  
    @Override  
    public BufferedReader getReader() throws IOException {  
        return new BufferedReader(new InputStreamReader(getInputStream()));  
    }  
  
    @Override  
    public ServletInputStream getInputStream() throws IOException {  
  
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);  
  
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {  
                return bais.read();  
            }  
        };  
    }  
  
    @Override  
    public String getHeader(String name) {  
        return super.getHeader(name);  
    }  
  
    @Override  
    public Enumeration<String> getHeaderNames() {  
        return super.getHeaderNames();  
    }  
  
    @Override  
    public Enumeration<String> getHeaders(String name) {  
        return super.getHeaders(name);  
    }  
      
}

获取POST请求的Body中的参数:

package com.cykj.card.filter;

import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
/**
 * @author 小影
 * @create 2022-07-04 10:59
 * @describe:
 */
public class HttpHelper {  
    /** 
     * 获取请求Body 
     * 
     * @param request 
     * @return 
     */  
    public static String getBodyString(ServletRequest request) {  
        StringBuilder sb = new StringBuilder();  
        InputStream inputStream = null;  
        BufferedReader reader = null;  
        try {  
            inputStream = request.getInputStream();  
            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();  
        } finally {  
            if (inputStream != null) {  
                try {  
                    inputStream.close();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                }  
            }  
            if (reader != null) {  
                try {  
                    reader.close();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                }  
            }  
        }
        System.out.println("sb = " + sb);
        return sb.toString();  
    }  
}

客户端请求模拟:

/**
 * @author 小影
 * @create 2022-07-04 17:01
 * @describe:
 */
@SpringBootTest
public class TestHttpReq {

    public static String secretKey = "XA12#Da";
    @Test
    public void testPost() {
        JSONObject data = new JSONObject();
        data.put("name", "xxx");
        data.put("age", "11");
        data.put("app_id", "1234");
        long dateTime = new Date().getTime();
        data.put("date_time", "1656926899731");
        String sign = getSign(data);
        //修改密钥为数据加密后加密串
        data.put("sign", sign);
        HttpResponse response = HttpRequest.post("localhost:8083/card/ss").form(data).contentType("application/json").execute();
        Object jsonObject = JSONObject.parse(response.body().trim());
        System.out.println("jsonObject = " + jsonObject);

    }

    @Test
    public void testGet() {
        String url = "localhost:8083/card/ss";
        long dateTime = new Date().getTime();
        //传入参数
        JSONObject data = new JSONObject();
        data.put("name", "123");
        data.put("age", "sss");
        data.put("app_id", "1234");
        data.put("date_time", dateTime);
        String sign = getSign(data);
        url = url + "?name=123&age=sss&appid=1234&dateTime=" + dateTime + "&sign=" + sign;
        HttpResponse response = HttpRequest.get(url).execute();
        System.out.println("response.body() = " + response.body().trim());

    }


    /**
     * 计算签名
     *
     * @param params
     * @return
     */
    public static String getSign(JSONObject params) {
        String sortStr = getFormatParams(params);
        System.out.println("sortStr = " + sortStr);
        //第二步:将tradeKey拼接在1中排序后的字符串后面得到待签名字符串。
        sortStr += "key="+ secretKey;
        System.out.println("sortStr = " + sortStr);
        //sortStr += "key=BF1BDE5A649724056F904A9335B1C1C9";
        //第三步:使用md5算法加密待加密字符串并转为大写即为sign
        String sign = DigestUtils.md5DigestAsHex(sortStr.getBytes()).toUpperCase();
        System.out.println("sign = " + sign);
        return sign;
    }


    /**
     * 字典排序
     * 获得参数格式化字符串
     * 参数名按字典排序,小写在后面
     */
    private static String getFormatParams(Map<String, Object> params) {
        List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(params.entrySet());
        Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {
            public int compare(Map.Entry<String, Object> arg0, Map.Entry<String, Object> arg1) {
                return (arg0.getKey()).compareTo(arg1.getKey());
            }
        });
        String ret = "";
        for (Map.Entry<String, Object> entry : infoIds) {
            ret += entry.getKey();
            ret += "=";
            ret += entry.getValue();
            ret += "&";
        }
        ret = ret.substring(0, ret.length() - 1);
        return ret;
    }
}

这是小编在开发学习使用和总结的小Demo,  这中间或许也存在着不足,希望可以得到大家的理解和建议。如有侵权联系小编!

猜你喜欢

转载自blog.csdn.net/weixin_46522803/article/details/125606163