赞
踩
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 协议能对传输的明文进行加密,但黑客仍可截获数据包进行重放攻击。两种通用解决方案是:
签名的实现过程如下图所示:
步骤1:客户端使用约定好的规则对传输的参数进行加密,得到签名值sign1,并且将签名值也放入请求的参数中,随请求发送至服务端。
步骤2:服务端接收到请求后,使用约定好的规则对请求的参数再次进行签名,得到签名值 sign2。
步骤3:服务端比对 sign1 和 sign2 的值,若不一致,则认定为被篡改,判定为非法请求。
1.3 什么是重放问题?
防重放也叫防复用。简单来说就是我获取到这个请求的信息之后什么也不改,,直接拿着接口的参数去重复请求这个充值的接口。此时我的请求是合法的, 因为所有参数都是跟合法请求一模一样的。重放攻击会造成两种后果:
1.4 如何解决重放问题?
防重放,业界通常基于 nonce + timestamp 方案实现。每次请求接口时生成 timestamp 和 nonce 两个额外参数,其中 timestamp 代表当前请求时间,nonce 代表仅一次有效的随机字符串。生成这两个字段后,与其他参数一起进行签名,并发送至服务端。服务端接收请求后,先比较 timestamp 是否超过规定时间(如60秒),再查看 Redis 中是否存在 nonce,最后校验签名是否一致,是否有篡改。
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); }
@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); }
@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; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。