前言
相信有很多小伙伴,在日常的开发中都有遇到过需要调用第三方接口的需求吧,但是自己有没有写过接口提供给第三方使用呢,常规的都是我们调用别人的接口,但是自己需要开发接口提供给第三方使用的场景应该不是很多,很多小伙伴可能会想不就开发一个接口对外开放嘛岂不是很简单,但是在开发接口对外开放,我们需要考虑一个问题,没有限制条件,那岂不是太不安全了,谁都可以调我这个接口了啊。
所以接下来的就是我们需要考虑的问题了,在开发接口的时候就要考虑到安全性的问题,那么应该如何去解决这个问题呢?提供接口给第三方使用的时候需要加上校验保证接口的安全性。
下面是我写的一个例子希望对大家有帮助。
接口Controller
在写接口前一定要签名做签名校验,我的签名方式做了特殊处理,因为接口是对外开放的,这个是为了避免恶意调用接口做的处理,叫做签名的混淆值,这个签名混淆值的作用是就算别人知道了接口,并且知道签名方式也不能被攻击,是为了避免被恶意篡改数据,签名混淆值就是一组特定加密后的数据。
@PostMapping("refundDeductionPoints")
public Result<SysIntegralStatement> refundDeductionPoints (@RequestParam Map<String,String> params){
Result<SysIntegralStatement> result = new Result<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
@Override
public Result<SysIntegralStatement> refundDeductionPoints(Map<String, String> params) {
String msgData = params.get("msgData");
ParamIntegral entity = new Gson().fromJson(msgData, ParamIntegral.class);
if (Common.isNull(entity)) {
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("请求参数不能为空");
}
if (Common.isEmpty(entity.getBitems())) {
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("请求参数不能为空");
}
int row = 0;
for (ParamIntegral bitem : entity.getBitems()) {
if (Common.isEmpty(bitem.getDdh())) {
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("订单号为必传参数");
}
if (null == bitem.getJfz()) {
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("扣减积分不能为空");
}
List<MallOrderInfo> orderInfo = mallOrderInfoMapper.selectByDdh(bitem.getDdh());
if (orderInfo == null) {
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR)
.setReason("订单号为" + bitem.getDdh() + "没有此订单请联系客服核对信息。");
}
if (orderInfo != null && orderInfo.size() > 1) {
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR)
.setReason("订单号为" + bitem.getDdh() + "有多个相同订单请联系客服核对信息。");
}
if (!"E".equals(orderInfo.get(0).getDdzt())) {
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR)
.setReason("订单号为" + bitem.getDdh() + "未确认收货还没产生积分不允许退货。");
}
SysIntegral integral = Common.first(integralMapper.selectByMdbm(orderInfo.get(0).getMdbm()));
if (integral == null) {
return new Result<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 = new SysIntegral();
dataMap.setIntegralId(integral.getIntegralId());
dataMap.setMdbm(integral.getMdbm());
dataMap.setKyjf(kyjf);
dataMap.setUpdateTime(new Date());
dataMap.setUpdateUser(entity.getUserName());
dataMap.setUpdateUserid(entity.getUserId().intValue());
row = integralMapper.updateByPrimaryKeySelective(dataMap);
if (row == 0) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("更新积分失败");
}
//推送到ERP门店信息
BdMdxxH mdxx =new BdMdxxH();
mdxx.setMdbm(integral.getMdbm());
mdxx.setMdjf(kyjf);
com.lkfs.cw.common.Result<BdMdxxH> bdMdxxh = dataBaseServiceApi.updateStorePoints(mdxx);
if (!bdMdxxh.isComplete()) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return new Result<SysIntegralStatement>().setCode(bdMdxxh.getCode()).setReason(bdMdxxh.getReason());
}
SysIntegralStatement statement = new SysIntegralStatement();
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(new Date());// 流水生成时间
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();
return new Result<SysIntegralStatement>().setCode(ErrorCodes.INNER_ERROR).setReason("插入积分流水失败");
}
}
return new Result<SysIntegralStatement>().setCode(ErrorCodes.SUCCESS).setReason("操作成功");
}
第三方调用接口Api实现类
模拟第三方合作方调用接口
//此方式以上已写了封装信息就不一一展示了,可以根据实际情况自行操作
private void pushIntegral(Long djlsh) {
FiSjysjsH fiSjysjsh = mapper.selectByPrimaryKey(djlsh);
//订单退货调用某某退货扣减积分接口
List<FiSjysjsB> sjysjsbList = bmapper.selectByKhddh(djlsh);
if (sjysjsbList != null && sjysjsbList.size() > 0) {
List<ParamIntegral> list = new ArrayList<ParamIntegral>();
for (FiSjysjsB bitem : sjysjsbList) {
ParamIntegral temp = new ParamIntegral();
temp.setDdh(bitem.getKhddh());
temp.setJfz(bitem.getJfz());
list.add(temp);
}
ParamIntegral param = new ParamIntegral();
param.setBitems(list);
param.setDjh(fiSjysjsh.getDjh());
param.setUserId(AppRealm.getCurrentUser().getUserId());
param.setUserName(AppRealm.getCurrentUser().getUserName());
if (new Short("1").equals(fiSjysjsh.getLocked())) {
param.setReturnGoods(true);
}else {
param.setReturnGoods(false);
}
String msgData = new Gson().toJson(param).toString();
Map<String, String> params = new HashMap<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 = new Gson().fromJson(result, B2bIntegralResponse.class);
if (null == res) {
throw new RuntimeException("调用积分接口系统异常");
}
if (res.getCode() != 0) {//接口返回失败异常代码提示
throw new RuntimeException("调用积分接口发生异常,异常代码为:"+res.getCode()+"异常信息为:"+res.getReason());
}
}
}
生成签名工具类
package com.cy.xgsm.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
*
* @author Dylan
*
*/
public class SignUtil {
private static final Logger log = LoggerFactory.getLogger(SignUtil.class);
/**
*
*/
public static String sign(String str, String secret) {
StringBuilder enValue = new StringBuilder();
enValue.append(secret);
enValue.append(str);
enValue.append(secret);
return encryptByMD5(enValue.toString());
}
private static String encryptByMD5(String data) {
String re_md5 = new String();
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data.getBytes());
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer();
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();
}
public static String compare(String jsonStr,String secret ){
JsonParser jsonParser = new JsonParser();
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 = new StringBuilder();
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
*/
public static String post(String url, Map<String, String> parameters) {
String result = "";// 返回的结果
BufferedReader in = null;// 读取响应输入流
PrintWriter out = null;
StringBuffer sb = new StringBuffer();// 处理请求参数
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 = new PrintWriter(httpConn.getOutputStream());
// 发送请求参数
out.write(params);
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应,设置编码方式
in = new BufferedReader(new InputStreamReader(
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;
}