Java提供接口给第三方使用,校验保证接口的安全性
前言
相信有很多小伙伴,在日常的开发中都有遇到过需要调用第三方接口的需求吧,但是自己有没有写过接口提供给第三方使用呢,常规的都是我们调用别人的接口,但是自己需要开发接口提供给第三方使用的场景应该不是很多,很多小伙伴可能会想不就开发一个接口对外开放嘛岂不是很简单,但是在开发接口对外开放,我们需要考虑一个问题,没有限制条件,那岂不是太不安全了,谁都可以调我这个接口了啊。
所以接下来的就是我们需要考虑的问题了,在开发接口的时候就要考虑到安全性的问题,那么应该如何去解决这个问题呢?提供接口给第三方使用的时候需要加上校验保证接口的安全性。
下面是我写的一个例子希望对大家有帮助。
接口Controller
在写接口前一定要签名做签名校验,我的签名方式做了特殊处理,因为接口是对外开放的,这个是为了避免恶意调用接口做的处理,叫做签名的混淆值,这个签名混淆值的作用是就算别人知道了接口,并且知道签名方式也不能被攻击,是为了避免被恶意篡改数据,签名混淆值就是一组特定加密后的数据。
@PostMapping("refundDeductionPoints")publicResult<SysIntegralStatement> refundDeductionPoints (@RequestParamMap<String,String> params){Result<SysIntegralStatement> result =newResult<SysIntegralStatement>();try{//签名校验String msgDigest = params.get("msgDigest");//签名String msgData = params.get("msgData");String timeStamp = params.get("timeStamp");String secret = params.get("secret");// 秘钥String sign =SignUtil.sign(msgData+"wf8la1tw7p9o2xz",timeStamp);//wf8la1tw7p9o2xz为签名混淆值if(!msgDigest.equals(sign)){return result.setCode(1006).setReason("数字签名无效");}if(Common.isEmpty(secret)){//先签名后幂等校验return result.setCode(1001).setReason("密钥不能为空");}/**
* 幂等校验
* 1.同一个用户操作同一个退货单一分钟内操作该单据视为重复操作(此秘钥已通过特殊处理)
*/String value = redistempalte.opsForValue().get(secret);if(Common.isNotEmpty(value)){
logger.error("重复请求 secret={}",value);return result.setCode(1007).setReason("重复请求");}
redistempalte.opsForValue().set(secret,"1",60,TimeUnit.SECONDS);//设置缓存一分钟return service.refundDeductionPoints(params);}catch(Exception e){
logger.error("添加积分流水异常", e);return result.setCode(ErrorCodes.BUSINESS_ERROR).setReason("生成积分流水失败");}}
接口幂等性校验
此接口做幂等性校验,幂等性校验常见解决方案有很多,可以自行根据实际情况选择,
说到幂等首先要先了解什么是幂等
概念:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数.
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次,比如:
订单接口, 不能多次创建订单
支付接口, 重复支付同一笔订单只能扣一次钱
支付宝回调接口, 可能会多次回调, 必须处理重复回调
普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
等等
解决方案常见的几种方式
唯一索引 – 防止新增脏数据
token机制 – 防止页面重复提交
悲观锁 – 获取数据的时候加锁(锁表或锁行)
乐观锁 – 基于版本号version实现, 在更新数据那一刻校验数据
分布式锁 – redis(jedis、redisson)或zookeeper实现
状态机 – 状态变更, 更新数据时判断状态
如果有小伙伴不理解什么是幂等可以看看官方是解释
实现类ServiceImpl
@Transactional@OverridepublicResult<SysIntegralStatement>refundDeductionPoints(Map<String,String> params){String msgData = params.get("msgData");ParamIntegral entity =newGson().fromJson(msgData,ParamIntegral.class);if(Common.isNull(entity)){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("请求参数不能为空");}if(Common.isEmpty(entity.getBitems())){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("请求参数不能为空");}int row =0;for(ParamIntegral bitem : entity.getBitems()){if(Common.isEmpty(bitem.getDdh())){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("订单号为必传参数");}if(null== bitem.getJfz()){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("扣减积分不能为空");}List<MallOrderInfo> orderInfo = mallOrderInfoMapper.selectByDdh(bitem.getDdh());if(orderInfo ==null){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("订单号为"+ bitem.getDdh()+"没有此订单请联系客服核对信息。");}if(orderInfo !=null&& orderInfo.size()>1){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("订单号为"+ bitem.getDdh()+"有多个相同订单请联系客服核对信息。");}if(!"E".equals(orderInfo.get(0).getDdzt())){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("订单号为"+ bitem.getDdh()+"未确认收货还没产生积分不允许退货。");}SysIntegral integral =Common.first(integralMapper.selectByMdbm(orderInfo.get(0).getMdbm()));if(integral ==null){returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("门店编码为"+ orderInfo.get(0).getMdbm()+"积分汇总没有找到此门店,请联系客服核实");}BigDecimal kyjf =BigDecimal.ZERO;if(entity.getReturnGoods()==true){// 可用积分小于扣减积分不够扣ERP使用前抵扣if(bitem.getJfz().compareTo(integral.getKyjf())==1){
kyjf =BigDecimal.ZERO;}else{// 可用积分 = 当前可用积分-扣减积分
kyjf =Common.nvl(integral.getKyjf(),BigDecimal.ZERO).subtract(bitem.getJfz());}}else{// 可用积分 = 当前可用积分+退还积分
kyjf =Common.nvl(integral.getKyjf(),BigDecimal.ZERO).add(bitem.getJfz());}// 更新积分汇总SysIntegral dataMap =newSysIntegral();
dataMap.setIntegralId(integral.getIntegralId());
dataMap.setMdbm(integral.getMdbm());
dataMap.setKyjf(kyjf);
dataMap.setUpdateTime(newDate());
dataMap.setUpdateUser(entity.getUserName());
dataMap.setUpdateUserid(entity.getUserId().intValue());
row = integralMapper.updateByPrimaryKeySelective(dataMap);if(row ==0){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("更新积分失败");}//推送到ERP门店信息BdMdxxH mdxx =newBdMdxxH();
mdxx.setMdbm(integral.getMdbm());
mdxx.setMdjf(kyjf);com.lkfs.cw.common.Result<BdMdxxH> bdMdxxh = dataBaseServiceApi.updateStorePoints(mdxx);if(!bdMdxxh.isComplete()){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();returnnewResult<SysIntegralStatement>().setCode(bdMdxxh.getCode()).setReason(bdMdxxh.getReason());}SysIntegralStatement statement =newSysIntegralStatement();if(entity.getReturnGoods()==true){
statement.setJfz(bitem.getJfz().negate());// 消费的积分值if(bitem.getJfz().compareTo(integral.getKyjf())==1){// 可用积分小于扣减积分不够扣ERP使用前抵扣
statement.setTzhjfz(BigDecimal.ZERO);// 调整后积分值}else{
statement.setTzhjfz(Common.nvl(integral.getKyjf(),BigDecimal.ZERO).subtract(bitem.getJfz()));// 调整后积分值}
statement.setJfxflx("E");// 积分支出
statement.setXxsm("退货扣减积分(订单号为:"+ bitem.getDdh()+","+"退货单号为:"+ entity.getDjh()+")"+"已扣除:"+ bitem.getJfz().negate()+":积分");}else{// 取消退货
statement.setJfxflx("I");// 积分收入
statement.setJfz(bitem.getJfz());// 取消退货把积分赠送回来
statement.setTzhjfz(Common.nvl(integral.getKyjf(),BigDecimal.ZERO).add(bitem.getJfz()));// 调整后积分值
statement.setXxsm("取消退货(订单号为:"+ bitem.getDdh()+","+"退货单号为:"+ entity.getDjh()+")"+"已退还:"+ bitem.getJfz()+":积分");}
statement.setIntegralId(integral.getIntegralId());// 该门店积分编码
statement.setTzqjfz(integral.getKyjf());// 调整前积分值
statement.setDdh(entity.getDdh());
statement.setCreateTime(newDate());// 流水生成时间
statement.setCreateUser(entity.getUserName());
statement.setCreateUserid(entity.getUserId().intValue());
statement.setJftz("T");// 积分扣减为T
statement.setZt("Y");// 状态 Y:有效
row = mapper.insert(statement);if(row ==0){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("插入积分流水失败");}}returnnewResult<SysIntegralStatement>().setCode(ErrorCodes.SUCCESS).setReason("操作成功");}
第三方调用接口Api实现类
模拟第三方合作方调用接口
//此方式以上已写了封装信息就不一一展示了,可以根据实际情况自行操作privatevoidpushIntegral(Long djlsh){FiSjysjsH fiSjysjsh = mapper.selectByPrimaryKey(djlsh);//订单退货调用某某退货扣减积分接口List<FiSjysjsB> sjysjsbList = bmapper.selectByKhddh(djlsh);if(sjysjsbList !=null&& sjysjsbList.size()>0){List<ParamIntegral> list =newArrayList<ParamIntegral>();for(FiSjysjsB bitem : sjysjsbList){ParamIntegral temp =newParamIntegral();
temp.setDdh(bitem.getKhddh());
temp.setJfz(bitem.getJfz());
list.add(temp);}ParamIntegral param =newParamIntegral();
param.setBitems(list);
param.setDjh(fiSjysjsh.getDjh());
param.setUserId(AppRealm.getCurrentUser().getUserId());
param.setUserName(AppRealm.getCurrentUser().getUserName());if(newShort("1").equals(fiSjysjsh.getLocked())){
param.setReturnGoods(true);}else{
param.setReturnGoods(false);}String msgData =newGson().toJson(param).toString();Map<String,String> params =newHashMap<String,String>();String timeStamp =String.valueOf(System.currentTimeMillis());//时间戳
params.put("timeStamp", timeStamp);
params.put("msgData", msgData);
params.put("msgDigest",SignUtil.sign(msgData+"wf8la1tw7p9o2xz", timeStamp));//生成签名第二个值暂定(wf8la1tw7p9o2xz签名混淆值)
params.put("secret",IDEMPOTENT_SECRET_PREFIX + fiSjysjsh.getDjh()+AppRealm.getCurrentUser().getUserId()+param.getReturnGoods());//自定义密钥 做幂等校验String result =HttpCilent.post(B2B_URL, params);//发送http post请求B2bIntegralResponse res =newGson().fromJson(result,B2bIntegralResponse.class);if(null== res){thrownewRuntimeException("调用积分接口系统异常");}if(res.getCode()!=0){//接口返回失败异常代码提示thrownewRuntimeException("调用积分接口发生异常,异常代码为:"+res.getCode()+"异常信息为:"+res.getReason());}}}
生成签名工具类
packagecom.cy.xgsm.util;importjava.security.MessageDigest;importjava.security.NoSuchAlgorithmException;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importcom.google.gson.JsonElement;importcom.google.gson.JsonObject;importcom.google.gson.JsonParser;/**
*
* @author Dylan
*
*/publicclassSignUtil{privatestaticfinalLogger log =LoggerFactory.getLogger(SignUtil.class);/**
*
*/publicstaticStringsign(String str,String secret){StringBuilder enValue =newStringBuilder();
enValue.append(secret);
enValue.append(str);
enValue.append(secret);returnencryptByMD5(enValue.toString());}privatestaticStringencryptByMD5(String data){String re_md5 =newString();try{MessageDigest md =MessageDigest.getInstance("MD5");
md.update(data.getBytes());byte b[]= md.digest();int i;StringBuffer buf =newStringBuffer();for(int offset =0; offset < b.length; offset++){
i = b[offset];if(i <0)
i +=256;if(i <16)
buf.append("0");
buf.append(Integer.toHexString(i));}
re_md5 = buf.toString();}catch(NoSuchAlgorithmException e){
e.printStackTrace();}return re_md5.toUpperCase();}publicstaticStringcompare(String jsonStr,String secret ){JsonParser jsonParser =newJsonParser();JsonObject jsonObject = jsonParser.parse(jsonStr).getAsJsonObject();String sign1 ="null";JsonElement signElement = jsonObject.remove("sign");if( signElement !=null){
sign1 = signElement.getAsString();}
log.info("sign1: "+ sign1);StringBuilder enValue =newStringBuilder();
enValue.append(secret);
enValue.append(jsonObject.toString());
enValue.append(secret);String sign2 =encryptByMD5(enValue.toString());
jsonObject.addProperty("sign", sign2);return jsonObject.toString();}}
HttpCilent工具类
这个工具类在我之前的文章也有但是没有把这个方式的放上去,如果有需要用到可直接把一下代码复制到这个Http工具类 最后即可直接使用。
/**
* 发送post请求
* @param url 目的url
* @param parameters 参数
* @return
*/publicstaticStringpost(String url,Map<String,String> parameters){String result ="";// 返回的结果BufferedReader in =null;// 读取响应输入流PrintWriter out =null;StringBuffer sb =newStringBuffer();// 处理请求参数String params ="";// 编码之后的参数try{// 编码请求参数if(parameters.size()==1){for(String name : parameters.keySet()){
sb.append(name).append("=").append(java.net.URLEncoder.encode(
parameters.get(name),"UTF-8"));}
params = sb.toString();}else{for(String name : parameters.keySet()){
sb.append(name).append("=").append(java.net.URLEncoder.encode(
parameters.get(name),"UTF-8")).append("&");}String temp_params = sb.toString();
params = temp_params.substring(0, temp_params.length()-1);}// 创建URL对象java.net.URL connURL =new java.net.URL(url);// 打开URL连接java.net.HttpURLConnection httpConn =(java.net.HttpURLConnection) connURL
.openConnection();// 设置通用属性
httpConn.setRequestProperty("Accept","*/*");
httpConn.setRequestProperty("Connection","Keep-Alive");
httpConn.setRequestProperty("content-type","application/x-www-form-urlencoded");
httpConn.setRequestProperty("User-Agent","Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)");// 设置POST方式
httpConn.setDoInput(true);
httpConn.setDoOutput(true);// 获取HttpURLConnection对象对应的输出流
out =newPrintWriter(httpConn.getOutputStream());// 发送请求参数
out.write(params);// flush输出流的缓冲
out.flush();// 定义BufferedReader输入流来读取URL的响应,设置编码方式
in =newBufferedReader(newInputStreamReader(
httpConn.getInputStream(),"UTF-8"));String line;// 读取返回的内容while((line = in.readLine())!=null){
result += line;}}catch(Exception e){
e.printStackTrace();}finally{try{if(out !=null){
out.close();}if(in !=null){
in.close();}}catch(IOException ex){
ex.printStackTrace();}}return result;}
此文章仅供参考,如果能帮助到大家,可以给博主支持一下。
版权归原作者 小甘说码 所有, 如有侵权,请联系我们删除。