前言
1.什么是安全接口?
通常来说要将暴露在外网的 API 接口视为安全接口,需要实现防篡改和防重放的功能。
1.1 什么是篡改问题?
由于 HTTP 是一种无状态协议,服务端无法确定客户端发送的请求是否合法,也不了解请求中的参数是否正确。以一个充值接口为例:
http://127.0.0.1:8080/api/user/recharge?user_id=1001&amount=10
如果非法用户通过抓包获取接口参数并修改 user_id 或 amount 的值,就能为任意账户添加余额。
1.2 如何解决篡改问题?
虽然使用 HTTPS 协议能对传输的明文进行加密,但黑客仍可截获数据包进行重放攻击。两种通用解决方案是:
- 使用 HTTPS 加密接口数据传输,即使被黑客破解,也需要耗费大量时间和精力。
- 在接口后台对请求参数进行签名验证,以防止黑客篡改。
签名的实现过程如下图所示:
步骤1:客户端使用约定好的规则对传输的参数进行加密,得到签名值sign1,并且将签名值也放入请求的参数中,随请求发送至服务端。
步骤2:服务端接收到请求后,使用约定好的规则对请求的参数再次进行签名,得到签名值 sign2。
步骤3:服务端比对 sign1 和 sign2 的值,若不一致,则认定为被篡改,判定为非法请求。
1.3 什么是重放问题?
防重放也叫防复用。简单来说就是我获取到这个请求的信息之后什么也不改,,直接拿着接口的参数去重复请求这个充值的接口。此时我的请求是合法的, 因为所有参数都是跟合法请求一模一样的。重放攻击会造成两种后果:
- 针对插入数据库接口:重放攻击,会出现大量重复数据,甚至垃圾数据会把数据库撑爆。
- 针对查询的接口:黑客一般是重点攻击慢查询接口,例如一个慢查询接口1s,只要黑客发起重放攻击,就必然造成系统被拖垮,数据库查询被阻塞死。
1.4 如何解决重放问题?
防重放,业界通常基于 nonce + timestamp 方案实现。每次请求接口时生成 timestamp 和 nonce 两个额外参数,其中 timestamp 代表当前请求时间,nonce 代表仅一次有效的随机字符串。生成这两个字段后,与其他参数一起进行签名,并发送至服务端。服务端接收请求后,先比较 timestamp 是否超过规定时间(如60秒),再查看 Redis 中是否存在 nonce,最后校验签名是否一致,是否有篡改。文章来源:https://www.toymoban.com/news/detail-837323.html
文章来源地址https://www.toymoban.com/news/detail-837323.html
一、生成及校验Token
1.1 生成Token
public static final String equipmentSecret = "Equipment_Secret";
@PostMapping("/getToken/app")
@ApiOperation("获取鉴权token")
public Message.DataRespone<AppTokenVo> getToken(@RequestBody AppTokenRequest appTokenRequest) {
//兼容正负3分钟
Date endTime = DateTimeUtils.getDateAfterNow(3, "m");
Date startTime = DateTimeUtils.getDateAfterNow(-3, "m");
Date targetTime = new Date(appTokenRequest.getTime());
if (startTime.after(targetTime) || targetTime.after(endTime)) {
return Message.Time_Not_In_Use.create();
}
PProduct product = productService.getProductByProductKey(appTokenRequest.getProductKey());
Map<String, String> claims = new HashMap<>();
claims.put("productKey", appTokenRequest.getProductKey());
claims.put("time", String.valueOf(appTokenRequest.getTime()));
String targetSign = SignUtil.sign(claims, product.getProductSecret());
if (!targetSign.equals(appTokenRequest.getSign())) {
return Message.Sign_Error.create();
}
String token = Jwts.builder()
.setClaims(claims)
.setExpiration(DateTimeUtils.getDateAfterNow(2, "H"))
//采用什么算法是可以自己选择的,不一定非要采用HS512
.signWith(SignatureAlgorithm.HS512, equipmentSecret)
.compact();
AppTokenVo appTokenVo1 = new AppTokenVo();
appTokenVo1.setToken(token);
appTokenVo1.setExpiration(DateTimeUtils.getDateAfterNow(2, "H").getTime());
return Message.Success.createWithData(appTokenVo1);
}
1.2 校验Token
@GetMapping("/checkToken")
@ApiOperation("校验token")
public Message.DataRespone<CheckTokenResultVo> getToken(@RequestParam(required = true, defaultValue = "") String token) {
CheckTokenResultVo checkTokenResultVo = new CheckTokenResultVo();
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(equipmentSecret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
if (claims == null) {
return Message.Token_CHECK_ERROR.create();
}
String productKey = String.valueOf(claims.get("productKey"));
checkTokenResultVo.setProductKey(productKey);
checkTokenResultVo.setExpiration(claims.getExpiration().getTime());
return Message.Success.createWithData(checkTokenResultVo);
}
1.3 SignUtil 签名工具类
@Deprecated
public class SignUtil {
/**
* @param @param sPara
* @param @param appecret
* @param @return 参数描述
* @return String 返回类型描述
* @throws
* @Title: buildRequestMysign
* @Description: 签名方法
*/
public static String sign(Map<String, String> sPara, String appecret) {
String prestr = SignUtil.createLinkString(paraFilter(sPara)); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
String mysign = "";
mysign = MD5.sign(prestr, appecret, "utf-8");
return mysign;
}
/**
* 除去数组中的空值和签名参数
*
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
private static Map<String, String> paraFilter(Map<String, String> sArray) {
Map<String, String> result = new HashMap<String, String>();
if (sArray == null || sArray.size() <= 0) {
return result;
}
for (String key : sArray.keySet()) {
String value = sArray.get(key);
if (value == null || value.equals("") || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param params 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
private static String createLinkString(Map<String, String> params) {
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
}
到了这里,关于如何保证对外接口的安全?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!