API接口安全之签名验签(一)

您所在的位置:网站首页 短信接口发送失败是什么意思 API接口安全之签名验签(一)

API接口安全之签名验签(一)

2023-12-29 19:28| 来源: 网络整理| 查看: 265

简介 现在越来越多人关注接口安全,传统的接口在传输的过程中,容易被抓包然后更改里面的参数值达到某些目的。传统的做法是用安全框架或者在代码里面做验证,但是有些系统是不需要登录的,随时可以调。这时候我们可以通过对参数进行签名验证,如果参数与签名值不匹配,则请求不通过,直接返回错误信息 项目代码地址:

https://github.com/loafer7423/signature.git 

包含了服务端、客户端、数据库脚本,下载下来,需要修改服务端连接数据库的连接。

验签流程

案例

假设app客户端请求后台服务端的地址为:http://localhost:8080/test/list参数:{"name": "李四2"},参数是json格式。但是通常我们开发的时候,可能需要一些公共的参数如登录的用户ID(userId)、来源(form),对于这样的我们需求,我们传参可能是

{ "name": "李四2", "base": { "from": "android", "userId": 12 } }

但是这样直接传参,可能会被别人拦截请求,直接修改参数,提交到我们的后台服务系统。对于这样的API接口,我们需要对参数进行签名验证。

首先,我们需要定义一个参数signature用来传客户端生成的签名;但是如果别人通过数据分析,知道了我们的加密形式,也可以破解进行修改参数。我们还需要定义一个参数是nonce用来生成随机数,这时候我们的签名生成公式=md5(参数列表+nonce(随机数));同时为了保证我们接口在有效的时间进行访问,我们还需要定义一个参数是timestamp用来传参客户端的时间戳,所以整个签名的公式为:

signature(签名)=md5(参数列表+nonce(随机数)+timestamp(时间戳))

所以最终我们的json参数为

{ "name": "李四2", "base": { "from": "android",//来源 "userId": 12,//登录用户id "signature": "9070D6BBE067283F2A25BE9ACBE0211E",//客户端生成的签名 "nonce": "LkFt7hCgGSmvgl7Z",//客户端生成的随机数 "timestamp": 1570518677803 //客户端生成的时间戳 } } 客户端完整代码

注意:因为我是模拟客户端的请求,所以客户端的代码我也是用Java,把请求的json数据拼装好同时生成对应的签名,利用postman请求服务端接口。

public class Demo01 { public static void main(String[] args) { String randomStr = getRandomString(16); String jsonstr = "{\"base\":{" + "\"nonce\":\"" + randomStr + "\"," + "\"timestamp\":" + System.currentTimeMillis() + "," + "\"userId\":12," + "\"from\":\"android\"" + "}," + "\"name\":\"李四2\"" + "}"; JSONObject jsonObject = JSON.parseObject(jsonstr); JSONObject base = (JSONObject) jsonObject.get("base"); Map map = generateSignStr(base); String param = formatUrlMap(map, true, true); String signature = md5(param); // System.out.println("客户端签名:" + signature); base.put("signature", signature); System.out.println("客户端生成的请求签名参数:"+jsonObject); } /** * @description: 将参数按照字段名排序 * @author wangdong */ public static String formatUrlMap(Map paraMap, boolean urlEncode, boolean keyToLower) { String buff = ""; Map tmpMap = paraMap; try { List infoIds = new ArrayList(tmpMap.entrySet()); // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) Collections.sort(infoIds, new Comparator() { @Override public int compare(Map.Entry o1, Map.Entry o2) { return (o1.getKey()).toString().compareTo(o2.getKey()); } }); // 构造URL 键值对的格式 StringBuilder buf = new StringBuilder(); for (Map.Entry item : infoIds) { if (StringUtils.isNotBlank(item.getKey())) { String key = item.getKey(); Object val = item.getValue(); if (urlEncode) { val = URLEncoder.encode(val.toString(), "utf-8"); } if (keyToLower) { buf.append(key.toLowerCase() + "=" + val); } else { buf.append(key + "=" + val); } buf.append("&"); } } buff = buf.toString(); if (buff.isEmpty() == false) { buff = buff.substring(0, buff.length() - 1); } } catch (Exception e) { return null; } return buff; } /** * @description: 将json格式转换为map对象 * @author wangdong */ private static Map generateSignStr(JSONObject base) { String timestamp = base.getString("timestamp"); String nonce = base.getString("nonce"); String userId = base.getString("userId"); Map map = new HashMap(); map.put("nonce", nonce); map.put("timestamp", timestamp); map.put("userId", userId); return map; } /** * @description: 客户端生成随机数 * @author wangdong */ public static String getRandomString(int length) { String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { int number = random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } /** * @description: md5加密 * @author wangdong */ public static String md5(String content) { // 用于加密的字符 char[] md5String = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; try { // 使用平台默认的字符集将md5String编码为byte序列,并将结果存储到一个新的byte数组中 byte[] byteInput = content.getBytes(); // 信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值 MessageDigest mdInst = MessageDigest.getInstance("MD5"); // MessageDigest对象通过使用update方法处理数据,使用指定的byte数组更新摘要 mdInst.update(byteInput); // 摘要更新后通过调用digest() 执行哈希计算,获得密文 byte[] md = mdInst.digest(); // 把密文转换成16进制的字符串形式 int j = md.length; char[] str = new char[j * 2]; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md[i]; str[k++] = md5String[byte0 >>> 4 & 0xf]; str[k++] = md5String[byte0 & 0xf]; } // 返回加密后的字符串 return new String(str); } catch (Exception e) { e.printStackTrace(); return null; } } }

运行客户端的代码,将生成的结果,放入到postman,进行请求服务端接口即可

服务端的代码

服务端的大致逻辑:

1.客户端把生成的签名已经传递到服务端,所以服务端需要和客户端的加密算法、参数排序等需要保持一致;

2.服务端生成的签名与客户端生成的签名进行比对,判断是否一致,如果一致,则说明客户端传过来的参数没有被修改;如果不一致,则证明客户端传过来的参数已经被修改。

3.因为服务端验签,不仅仅只只对于一个接口,所以我们需要将验签的过程,放在服务端的拦截器处理。

核心代码

核心代码,只介绍拦截器的代码,因为为了做demo,加密、排序等操作,我都放在拦截器对象里。服务端技术springboot+mysql。

拦截器RequestFilter.java核心代码,首先拦截器,我定义了只拦截了请求为/test/开头的地址,具体如下:

@WebFilter(filterName = "request", urlPatterns = "/test/*") public class RequestFilter implements Filter {

其次我们需要拿到客户端请求传过来的参数,并且将参数转成字符串。

ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request); String bodyString = getBodyString(requestWrapper.getReader()); /** * @description: 解析body数据的字符 * @author wangdong * @date 2019/10/8 16:38 */ public static String getBodyString(BufferedReader br) { String inputLine; StringBuffer str = new StringBuffer(); try { while ((inputLine = br.readLine()) != null) { str.append(inputLine); } br.close(); } catch (IOException e) { e.printStackTrace(); } return str.toString(); }

通过客户端传过来的参数,获取客户端生成的签名值

//将请求的参数转换为json对象 JSONObject jsonObject = JSON.parseObject(bodyString); //后去参数的base里的json对象 JSONObject base = (JSONObject) jsonObject.get("base"); String signature=base.getString("signature");

将客户端传过来的签名参数删除,只保留和客户端签名的几个参数,并按照字段名首字母排序(和客户端算法保持一致)

//删除客户端穿过来的签名 base.remove("signature"); //将base参数转换为map对象 Map map = generateSignStr(base); //拼装参数(按照字段名首字母排序) String param = formatUrlMap(map,true,true);

验证客户端签名和服务端签名是否一致,如果不一致则给出提示信息

if(!md5(param).equals(signature)){//验证参数签名是否正确(客户端的签名和服务端根据参数重新加密生成签名,再验签) outputStream(servletResponse,"参数被篡改..."); return; }

核心完整代码如下:

/** * @ClassName RequestFilter * @Description [拦截器,验证参数签名是否通过] * @Author wangdong * @Date 2019/10/6 18:42 * @Version V1.0 **/ @WebFilter(filterName = "request", urlPatterns = "/test/*") public class RequestFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } /** * @description: 拦截方法,处理业务逻辑 * @author wangdong * @date 2019/10/8 16:39 */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; //获取请求地址,如:/test/list String requestURI = request.getRequestURI(); //过滤哪些请求直接放行 if (requestURI.contains("/callBack")){ filterChain.doFilter(request, servletResponse); return; } ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request); String bodyString = getBodyString(requestWrapper.getReader()); //将请求的参数转换为json对象 JSONObject jsonObject = JSON.parseObject(bodyString); //后去参数的base里的json对象 JSONObject base = (JSONObject) jsonObject.get("base"); String signature=base.getString("signature"); //删除客户端穿过来的签名 base.remove("signature"); //将base参数转换为map对象 Map map = generateSignStr(base); //拼装参数(按照字段名首字母排序) String param = formatUrlMap(map,true,true); if(!md5(param).equals(signature)){//验证参数签名是否正确(客户端的签名和服务端根据参数重新加密生成签名,再验签) outputStream(servletResponse,"参数被篡改..."); return; } //比较请求的参数是否过期 if(!validateTimeStamp(base.getLong("timestamp"))){ outputStream(servletResponse,"请求参数已过期..."); return; } //拦截器放行,继续执行业务方法 filterChain.doFilter(requestWrapper, servletResponse); return; } /** * @description: 解析body数据的字符 * @author wangdong * @date 2019/10/8 16:38 */ public static String getBodyString(BufferedReader br) { String inputLine; StringBuffer str = new StringBuffer(); try { while ((inputLine = br.readLine()) != null) { str.append(inputLine); } br.close(); } catch (IOException e) { e.printStackTrace(); } return str.toString(); } /** * @description: 将参数按照字段名排序 * @author wangdong * @date 2019/10/8 16:39 */ public static String formatUrlMap(Map paraMap, boolean urlEncode, boolean keyToLower) { String buff = ""; Map tmpMap = paraMap; try { List infoIds = new ArrayList(tmpMap.entrySet()); // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) Collections.sort(infoIds, new Comparator() { @Override public int compare(Map.Entry o1, Map.Entry o2) { return (o1.getKey()).toString().compareTo(o2.getKey()); } }); // 构造URL 键值对的格式 StringBuilder buf = new StringBuilder(); for (Map.Entry item : infoIds) { if (StringUtils.isNotBlank(item.getKey())) { String key = item.getKey(); Object val = item.getValue(); if (urlEncode) { val = URLEncoder.encode(val.toString(), "utf-8"); } if (keyToLower) { buf.append(key.toLowerCase() + "=" + val); } else { buf.append(key + "=" + val); } buf.append("&"); } } buff = buf.toString(); if (buff.isEmpty() == false) { buff = buff.substring(0, buff.length() - 1); } } catch (Exception e) { return null; } return buff; } /** * @description: 向客户端返回响应信息(json格式) * @author wangdong * @date 2019/10/8 16:46 */ private void outputStream(ServletResponse servletResponse,String message){ try{ String string = JSON.toJSONString(JSONResponse.failure(5002, message)); servletResponse.setContentType("application/json;charset=UTF-8"); servletResponse.getOutputStream().write(string.getBytes("UTF-8")); servletResponse.getOutputStream().close(); }catch (Exception e){ e.printStackTrace(); } } /** * @description: 将json格式转换为map对象 * @author wangdong * @date 2019/10/8 16:47 */ private Map generateSignStr(JSONObject base) { String timestamp = base.getString("timestamp"); String nonce = base.getString("nonce"); String userId = base.getString("userId"); Map map = new HashMap(); map.put("nonce", nonce); map.put("timestamp", timestamp); map.put("userId", userId); return map; } /** * @description: md5加密 * @author wangdong * @date 2019/10/8 16:47 */ public static String md5(String content) { // 用于加密的字符 char[] md5String = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; try { // 使用平台默认的字符集将md5String编码为byte序列,并将结果存储到一个新的byte数组中 byte[] byteInput = content.getBytes(); // 信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值 MessageDigest mdInst = MessageDigest.getInstance("MD5"); // MessageDigest对象通过使用update方法处理数据,使用指定的byte数组更新摘要 mdInst.update(byteInput); //摘要更新后通过调用digest() 执行哈希计算,获得密文 byte[] md = mdInst.digest(); //把密文转换成16进制的字符串形式 int j = md.length; char[] str = new char[j*2]; int k = 0; for (int i=0;i>> 4 & 0xf]; str[k++] = md5String[byte0 & 0xf]; } // 返回加密后的字符串 return new String(str); }catch (Exception e) { e.printStackTrace(); return null; } } /** * @description: 判断客户端的请求是否超过30分钟 * @author wangdong * @date 2019/10/8 16:48 */ public boolean validateTimeStamp(long timestamp) { Long tims = (System.currentTimeMillis()-timestamp) / (1000 * 60); //验证时间戳是否超过30分钟 if (Math.abs(tims) >30) { return false; } else { return true; } } @Override public void destroy() { } }

演示结果:

参数没有被修改

我们现在把userid=12改为userid=121,我们在重新请求服务端接口

结论:我们会发现,就算请求被人拦截,修改直接修改参数,提交到后台,对我们的业务系统也没有任何影响。

注意:

1.实际开发中,我们还需要考虑到参数转码的问题,我们可以利用URLEncoder在客户端转码,然后再服务端解码。

2.本实例中,我没有用全部的参数进行加密生成签名,只是用了通用的参数base{}进行签名的。实际开发可以用全部的参数进行加密生成签名。(只为了演示签名验签过程,所以很多场景没有考虑,见谅!!!)

 

 

 

 

 

 

 

 

 

 

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3