文章目录
- 前言
- 一、生成及校验Token
- 1.1 生成Token
- 1.2 校验Token
- 1.3 SignUtil 签名工具类
前言
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,最后校验签名是否一致,是否有篡改。
一、生成及校验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;}
}