- 一、定义
- 二、签名
- 三、相应工具类
- 四、测试get请求,参数写url上
- 五、post请求,参数放入body中
- 六、使用过滤器配置接口防篡改
- 一、相关工具类
- 二、测试
一、定义
在客户端与服务端请求交互的过程中,请求的数据容易被拦截并篡改,比如在支付场景中,请求支付金额为 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,可以可以看到请求接口失败,无论是改了内容还是改了签名,都请求不成功,这样就防止了第三方而已者篡改接口内容
测试内容同上
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: 复制输入流
*/
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、测试
如下可以看到,只要我修改了内容就验证不通过,就判定接口是被第三方恶意篡改过的
get请求测试同上,自行测试
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)