数据在网络传输过程中,容易被抓包。如果使用的是HTTP协议的请求/响应(Request OR Response),它是明文传输的,都是可以被截获、篡改、重放(重发)的。所以需要进行数据的加密验签,所以需要考虑以下几点。
常见的方式,就是对关键字段加密。比如查询订单接口,就可以对订单号进行加密。一般常用的加密算法对称加密算法(如:AES),或者哈希算法处理(如:MD5)
对称加密:加密和解密使用相同秘钥的加密算法
采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。
非对称加密:非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥和私钥是成对存在的,如果用公钥对数据加密,只有对应的私钥才能解密。 (非对称加密是更安全的做法,加密是算法RSA或SM2)
非对称加密算法需要两个密钥来进行加密和解密,这两个密钥是公开密钥(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参数不参与签名;
org.codehaus.groovy
groovy
3.0.6
com.alibaba
fastjson
1.2.78
org.springframework.boot
spring-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
*/
@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 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算法加密待加密字符串并转为大写即为sign
String 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() {
@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 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";
@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 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, 这中间或许也存在着不足,希望可以得到大家的理解和建议。如有侵权联系小编!