91名师指路-头部
91名师指路

java 如何保证API接口安全

由于某些原因,现在不支持支付宝支付,如需要购买源码请加博主微信进行购买,微信号:13248254750

背景:最近因公司业务发展,需要和A公司有业务上的对接。A公司需要调用我们公司的接口,我们需要考虑如下几个事情:

1)接口如何保证安全。

2)接口需要防止重复提交。

3)对请求链接有一定的时效性,来保证安全。

4)在接口在网络传递过程中被抓包篡改数据。


我们的解决方案:

1)对参数进行RSA加密传递。用公钥进行加密,用私钥进行解密。

2)对接口中的参数进行MD5签名,防止数据被篡改。

3)验证签名,和颁发给调用方的clientId是否有效。

4)采用redisson防止表单重复提交。根据序列号来进行判断。

5)根据用户传递的时间戳和当前系统时间判断改请求是否在有效期内。


整体思路:

一:调用方:

1)对参数按照ASCII进行排序拼接。

2)采用RSA公钥进行加密。

3)对参数进行签名。

4)采用httpClient进行请求。

二:接收方:

1)验证clientId是否正确。

2)验证sign是否正确。

3)判断链接是否在有效期内。

4)判断请求是否重复。


一:调用方和接收方的代码如下

package com.mszl.controller;

import cn.hutool.core.util.IdUtil;
import com.mszl.config.RedissonConfig;
import com.mszl.exception.BusinessException;
import com.mszl.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

/**
* 功能:保证接口安全
* @Author:bobo
* @Date:2021-10-23
* @email:895341748@qq.com
* @website: https://91mszl.com
*/
@Slf4j
@RestController
public class TestApiController {

@Autowired
private RedissonConfig redissonClient;

/**
* 调用方
*/
@PostMapping("/api/encrypt")
public ReturnMsgUtils apiEncrypt() throws Exception {
String clientId="1451913769495171072";
String userId="111";
String productId="222";
String remark="91名师指路";
String timestamp=String.valueOf(System.currentTimeMillis()); // 当前系统时间戳
String serialNo=String.valueOf(IdUtil.getSnowflake(1,1).nextId()); // 保证唯一即可

// 将请求参数的首字母按照按照ASCII进行排列
TreeMap<String, String> parmas=new TreeMap<>();
parmas.put("clientId", clientId);
parmas.put("userId", userId);
parmas.put("productId", productId);
parmas.put("remark", remark);
parmas.put("timestamp", timestamp);
parmas.put("serialNo", serialNo);
String str=ApiUtils.paramsSplice(parmas); // 参数拼接
String sign=Md5.getMD5ofStr(str+BusinessUtils.SECRET_KEY); // 签名

// 加密
String encrypt=URLEncoder.encode(RSAUtils.encrypt(str, BusinessUtils.PUBLIC_KEY), "UTF-8"); // URLEncoder.encode解决字符转义问题
log.info("encrypt : {}", encrypt);
Map<String, String> map=new HashMap<>(16);
map.put("encrypt", encrypt);
map.put("sign", sign);

// 发起请求
String url="http://127.0.0.1:9001/api/decrypt";
String result=HttpClientUtils.sendHttpPost(url, null, map);
log.info("接口返回结果: {}", result);
return ReturnMsgUtils.success(result);
}

/**
* 接收方
*/
@PostMapping("/api/decrypt")
public ReturnMsgUtils apiDecrypt(String encrypt, String sign) throws Exception {
log.info("接收参数 encrypt: {}, sign: {}", encrypt, sign);

// 1 必填参数校验
Map<String, String> map=ApiUtils.judgeParmas(encrypt, sign);
// start 根据自己的业务需求判断参数是否为空:.......
String userId=map.get("userId");
String productId=map.get("productId");
String remark=map.get("remark");
if(StringUtils.isBlank(userId)){
throw new BusinessException(BusinessUtils.BUSINESS_CODE_201, "userId不能为空");
}
if(StringUtils.isBlank(productId)){
throw new BusinessException(BusinessUtils.BUSINESS_CODE_201, "productId不能为空");
}
// end 根据自己的业务需求判断参数是否为空:.......

// 2 验证clientId是否存在
String clientId=map.get("clientId");
if(!clientId.equals(BusinessUtils.CLIENT_ID)){
throw new BusinessException(BusinessUtils.BUSINESS_CODE_201, "clientId不能存在");
}

// 3 验证sign是否有效
String serialNo=map.get("serialNo");
String timestamp=map.get("timestamp");
TreeMap<String, String> parmas=new TreeMap<>();
parmas.put("clientId", BusinessUtils.CLIENT_ID);
parmas.put("userId", userId);
parmas.put("productId", productId);
parmas.put("remark", remark);
parmas.put("timestamp", timestamp);
parmas.put("serialNo", serialNo);
String str=ApiUtils.paramsSplice(parmas);
String receiveSign=Md5.getMD5ofStr(str+BusinessUtils.SECRET_KEY);
log.info("receiveSign : {}", receiveSign);
if(!sign.equals(receiveSign)){
throw new BusinessException(BusinessUtils.BUSINESS_CODE_201, "sign错误");
}

// 4 判断是否超过时间5分钟
long difference=System.currentTimeMillis() - Long.parseLong(timestamp);
if(Math.abs(difference) > 1000 * 60 * 5){ // 大于5分钟
throw new BusinessException(BusinessUtils.BUSINESS_CODE_201, "链接已失效");
}

// 5 判断请求是否重复:
String key=clientId+"_"+serialNo;
RLock lock = redissonClient.redissonClient().getLock(key);
boolean res = lock.tryLock(0, 10, TimeUnit.SECONDS); // 尝试加锁,最多等待0秒,上锁以后10秒自动解锁
if(!res){
throw new BusinessException(BusinessUtils.BUSINESS_CODE_201, "请勿重复提交");
}

// start 处理自己的业务逻辑。。。。。。。。。。。。。

// end 处理自己的业务逻辑。。。。。。。。。。。。。

return ReturnMsgUtils.success();
}


}

二:测试

2.1 测试接口重复提交

我们将调用方的serialNo改成123456,是否重复提交就是根据序列号字段来进行判断的

重启idea,然后采用postman进行请求,第一次请求正常,第二次请求时提示我们不要重复提交。


2.2 测试接口是否在5分钟以后失效。

时间戳:1635000326793,对应的年月日:2021-10-23 22:45:26,我们将调用方的时间戳改成这个。或者你将5分钟改成1分钟都可以,如果你不想改,也可以喝一杯咖啡或茶了在回来。

重启idea,采用postman请求

到此关键的测试情况都演示了,还有验证必填项非空,clientId等就让大家自己去验证。


完整项目下载,请点击文章最后的下载源码 按钮进行下载,给作者晚餐加一个鸡腿。



参考资料:

https://mp.weixin.qq.com/s/zjKWqW-Wpbi2GnhSs5fwew

https://segmentfault.com/a/1190000039859234

2021-10-21 09:26:57     阅读(605)

名师出品,必属精品    https://www.91mszl.com

用户登录遮罩层
x

账号登录

91名师指路-底部