java实现Api接口加密通信方式
作者:HuanBuXingDeXingXing
接口加密通信思路
1)约定双方通信的秘钥,如:appKey = wenzhou
2)通信安全校验通过签名sign
1.生成时间戳、随机数或随机字符串等,如:时间戳:time=677899002
2.将通信秘钥、时间戳、接口传递的参数通过双方约定的拼接方式拼接在一起,如:
- 约定方式例一:
key1=value1&key2=value2&key3=&key4=value4&appKey=wenzhou&time=677899002&
- 约定方式例二:
key1value1key2value2key3value3key4value4appKeywenzhoutime677899002
除此之外还有很多,用户自己约定即可
3.约定签名的复杂规则,如:可以替换掉拼接字符串的指定字符、字符串前后字符对调、字符左移或右移等
- 如给约定方式例一做字符串对调:
&200998776=emit&uohznew=yeKppa&4eulav=4yek&=3yek&2eulav=2yek&1eulav=1yek
- 如给约定方式例二做字符串中y字符替换为*:
ke*1value1ke*2value2ke*3value3ke*4value4appKe*wenzhoutime677899002
方式很多,用户自行约定即可,此步骤也可以不做
4.双方约定使用相同的加密方式,对最终字符串进行加密,生成签名sign(还可以将生成的签名全部大写一下)
MD5 、Base64 、RSA 等等加密算法或者自己编写加密算法
3)调用方将用上述方法生成的签名连同参与生成签名的接口传参的参数一起传递给接收方
4)接收方使用相同的方式生成签名,对比签名是否一致
5)接收方校验appKey是否一致
6)接收方校验时间戳time加上指定时长,如时间戳加5秒(时间戳有效时长为5秒),若当前时间已经大于时间戳加5秒后的时间,则此签名过期,拒绝访问
约定双方通信的秘钥(接口提供方,需将appKey配置下来)
这里,我配置在application.properties中。小伙伴可根据自己需要自行决定配置方式
MD5加密工具类(使用MD5加密生成签名)
这里加密拼接后的字符串生成签名,加密方式有很多种,如:RSA 、MD5、Base64
package com.gwssi.common.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.MessageDigest; public class MD5Util { final static Logger log = LoggerFactory.getLogger(MD5Util.class); /** * 十六进制下数字到字符的映射数组 */ private final static String[] HEX_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; /** * md5加密算法 * @param originString 待加密字符串 * @return 加密后的字符串 */ public static String encodeByMD5(String originString){ if (originString != null){ try{ //创建具有指定算法名称的信息摘要 MessageDigest md = MessageDigest.getInstance("MD5"); //使用指定的字节数组对摘要进行最后更新,然后完成摘要计算 byte[] results = md.digest(originString.getBytes()); //将得到的字节数组变成字符串返回 String resultString = byteArrayToHexString(results); return resultString.toUpperCase(); } catch(Exception ex){ log.error("md5加密算法失败", ex); } } return null; } /** * 转换字节数组为十六进制字符串 * @param * @return 十六进制字符串 */ private static String byteArrayToHexString(byte[] b){ StringBuffer resultSb = new StringBuffer(); for (int i = 0; i < b.length; i++){ resultSb.append(byteToHexString(b[i])); } return resultSb.toString(); } /** 将一个字节转化成十六进制形式的字符串 */ private static String byteToHexString(byte b){ int n = b; if (n < 0) { n = 256 + n; } int d1 = n / 16; int d2 = n % 16; return HEX_DIGITS[d1] + HEX_DIGITS[d2]; } }
api接口提供方演示接口签名校验
package com.gwssi.device.component; import com.gwssi.common.entity.Constant; import com.gwssi.common.entity.ResponseCode; import com.gwssi.common.entity.Result; import com.gwssi.common.utils.BgUtils; import com.gwssi.common.utils.MD5Util; import com.gwssi.common.utils.ThreadCache; import net.sf.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.Map; @Component public class WenZhouComponent { Logger log = LoggerFactory.getLogger(WenZhouComponent.class); @Value("${wenzhou.appKey}") String localAppKey; @SuppressWarnings("unchecked") public Map<String,Object> wenzhouApis(){ //获取请求参数 JSONObject js = JSONObject.fromObject(ThreadCache.getData(Constant.HTTP_PARAM)); Map<String,Object> map = (Map<String,Object>) js; //map的数据是调取方传递的所有参数,_sign调取方通过约定规则生成的签名,time调取方生成的时间戳,appKey双方约定的秘钥,type为普通必传参数 //必传参数校验 if(!BgUtils.cheakParamIsNull(map, Arrays.asList("_sign","time","type","appKey"))){ Result.putValue(ResponseCode.CodeEnum.REQUIRED_PARAM_NULL); return null; } //签名校验 if(!checkSign(map.get("_sign").toString(),map.get("appKey").toString(),Long.parseLong(map.get("time").toString()),map)){ return null; } int type = Integer.parseInt(map.get("type").toString()); //进行接口主体操作 return null; } //api连接安全检验 private boolean checkSign(String getSign,String appKey,long time,Map<String,Object> map){ //接收方校验appKey是否一致 //appKey配置是否一致 log.info("配置本地appkey:"+localAppKey+" 传递appkey:"+appKey); if(!appKey.equals(localAppKey)){ Result.putValue(ResponseCode.CodeEnum.APP_KEY_ERROR); return false; } //接收方使用相同的方式生成签名,对比签名是否一致 //此处签名规则是,将所有传入的参数(除sign签名参数),按照顺序,依次key=value&拼接在一起,然后进行MD5加密,再将加密后的结果全部大写,即为 签名生成规则 StringBuffer str = new StringBuffer(""); map.keySet().stream().filter(c->!c.equals("_sign")).forEach(c->str.append(c).append("=").append(map.get(c).toString()).append("&")); String mySign = MD5Util.encodeByMD5(str.toString()); //签名计算规则是否一致 log.info("加密字符串:"+str.toString()+" 计算签名:"+mySign+" 传递签名:"+getSign); if(!getSign.equals(mySign.toUpperCase())){ Result.putValue(ResponseCode.CodeEnum.PARAM_SIGN_INCORRECT); return false; } //接收方校验时间戳time加上指定时长,如时间戳加5秒(时间戳有效时长为5秒),若当前时间已经大于时间戳加5秒后的时间,则此签名过期,拒绝访问 //签名过期判断(时间戳有效期当且仅有5秒) if(new Date(time+5000).compareTo(new Date()) < 0){ log.info("签名已在"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time+5000))+"后过期"); Result.putValue(ResponseCode.CodeEnum.SIGN_EXPIRE); return false; } return true; } }
api接口地址演示
@ApiOperation("温州对外提供的api接口地址封装") @RequestMapping("/device/wenzhouApis") public void wenzhouApis(){ wenZhouComponent.wenzhouApis(); }
请求演示
模拟调取方请求:
接收方:
2021-05-18 17:34:24.682 [ http-nio-9080-exec-2 ] - [ INFO ] [ o.a.c.core.ContainerBase.[Tomcat].[localhost].[/] : 180 ] - Initializing Spring FrameworkServlet 'dispatcherServlet'
2021-05-18 17:34:24.683 [ http-nio-9080-exec-2 ] - [ INFO ] [ org.springframework.web.servlet.DispatcherServlet : 494 ] - FrameworkServlet 'dispatcherServlet': initialization started
2021-05-18 17:34:24.761 [ http-nio-9080-exec-2 ] - [ INFO ] [ org.springframework.web.servlet.DispatcherServlet : 509 ] - FrameworkServlet 'dispatcherServlet': initialization completed in 78 ms
2021-05-18 17:34:24.891 [ http-nio-9080-exec-2 ] - [ INFO ] [ com.gwssi.filter.ParameterFilter : 79 ] - http-nio-9080-exec-2 --- Request Begin --- /device/wenzhouApis,body={
"_sign":"EC7ED24DF32F306E8F4E822E263289A3",
"time":"677899002",
"type":"1",
"appKey":"wenzhou"
}
2021-05-18 17:34:24.891 [ http-nio-9080-exec-2 ] - [ INFO ] [ com.gwssi.filter.ParameterFilter : 83 ] - http-nio-9080-exec-2 --- Request Begin --- /device/wenzhouApis,sysuid=
2021-05-18 17:34:25.053 [ http-nio-9080-exec-2 ] - [ INFO ] [ com.gwssi.device.component.WenZhouComponent : 58 ] - 配置本地appkey:wenzhou 传递appkey:wenzhou
2021-05-18 17:34:25.057 [ http-nio-9080-exec-2 ] - [ INFO ] [ com.gwssi.device.component.WenZhouComponent : 67 ] - 加密字符串:time=677899002&type=1&appKey=wenzhou& 计算签名:EC7ED24DF32F306E8F4E822E263289A3 传递签名:EC7ED24DF32F306E8F4E822E263289A3
2021-05-18 17:34:25.058 [ http-nio-9080-exec-2 ] - [ INFO ] [ com.gwssi.device.component.WenZhouComponent : 75 ] - 签名已在1970-01-09 04:18:24后过期
2021-05-18 17:34:25.149 [ http-nio-9080-exec-2 ] - [ INFO ] [ com.gwssi.interceptor.ParameterInterceptor : 33 ] - http-nio-9080-exec-2 --- /device/wenzhouApis --- Request End --- {"code":14,"data":null,"msg":"签名过期"}
2021-05-18 17:34:25.150 [ http-nio-9080-exec-2 ] - [ INFO ] [ com.gwssi.interceptor.ParameterInterceptor : 34 ] - http-nio-9080-exec-2 --- /device/wenzhouApis --- Request Time --- 2021-05-18 17:34:24.905 used 245ms
在线MD5加密展示:可以看到跟我们使用MD5Util加密结果一致
总结
1)接口的提供方和调取方约定好统一的参数加密算法,得到一个签名sign
2)调取方在调取参数时,将计算签名sign的参数、需要的参数、签名一起传递给接口
3)接口提供方将拿到的参数按照约定的参数加密算法进行加密得到一个签名sign,比较两个签名是否一致,不一致,则接口停止剩下的操作
4)签名校验通过以后,如果用户还有签名有效时长设置,还可以要求提供方传递时间戳(要求接收方与提供方时间保持一致或者计算时冗余时间差),时间戳+有效时长 对比当前时间,若小于当前时长,时间戳过期,则接口停止剩下的操作
5)为了进一步保证安全性,也可以双方约定公钥用于加密,此公钥一般可以自己保存不作为参数传递(本案例作为参数传递了,一般不建议)
6)推介使用https协议代替http
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。