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都通过则正常请求业务

实现 

org.codehaus.groovygroovy3.0.6


com.alibabafastjson1.2.78

org.springframework.bootspring-boot-starter-web

服务端拦截器:

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 cache = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(30).weigher(Weighers.singleton()).build();/*** 初始化** @param filterConfig* @throws ServletException*/@Overridepublic void init(FilterConfig filterConfig) throws ServletException {// key=APPID ,value=密钥  在数据库中加载出来,这里为了演示写死cache.put("1234", "XA12#Da");}/*** 签名验证时间(TIMES =分钟 * 秒 * 毫秒)* 当前设置为:5分钟有效期*/protected static final Integer TIMES = 5 * 60 * 1000;@Overridepublic 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 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 allRequestParam = getAllRequestParam(request);Set> 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 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算法加密待加密字符串并转为大写即为signString sign = SecureUtil.md5(sortStr).toUpperCase();return sign;}/*** 参数字典排序** @param params* @return*/private static String getFormatParams(Map params) {List> infoIds = new ArrayList>(params.entrySet());Collections.sort(infoIds, new Comparator>() {public int compare(Map.Entry arg0, Map.Entry arg1) {return (arg0.getKey()).compareTo(arg1.getKey());}});String ret = "";for (Map.Entry entry : infoIds) {ret += entry.getKey();ret += "=";ret += entry.getValue();ret += "&";}return ret;}/*** 获取客户端GET请求中所有的请求参数** @param request* @return*/private Map getAllRequestParam(final HttpServletRequest request) {Map res = new HashMap();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() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {}@Overridepublic int read() throws IOException {  return bais.read();  }  };  }  @Override  public String getHeader(String name) {  return super.getHeader(name);  }  @Override  public Enumeration getHeaderNames() {  return super.getHeaderNames();  }  @Override  public Enumeration 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";@Testpublic 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);}@Testpublic 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算法加密待加密字符串并转为大写即为signString sign = DigestUtils.md5DigestAsHex(sortStr.getBytes()).toUpperCase();System.out.println("sign = " + sign);return sign;}/*** 字典排序* 获得参数格式化字符串* 参数名按字典排序,小写在后面*/private static String getFormatParams(Map params) {List> infoIds = new ArrayList>(params.entrySet());Collections.sort(infoIds, new Comparator>() {public int compare(Map.Entry arg0, Map.Entry arg1) {return (arg0.getKey()).compareTo(arg1.getKey());}});String ret = "";for (Map.Entry entry : infoIds) {ret += entry.getKey();ret += "=";ret += entry.getValue();ret += "&";}ret = ret.substring(0, ret.length() - 1);return ret;}
}

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


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部