网站的开发工具有哪些,天猫网站设计教程,网站开发类型,ps做图 游戏下载网站前言
一个web系统#xff0c;从接口的使用范围也可以分为对内和对外两种#xff0c;对内的接口主要限于一些我们内部系统的调用#xff0c;多是通过内网进行调用#xff0c;往往不用考虑太复杂的鉴权操作。但是#xff0c;对于对外的接口#xff0c;我们就不得不重视这个…前言
一个web系统从接口的使用范围也可以分为对内和对外两种对内的接口主要限于一些我们内部系统的调用多是通过内网进行调用往往不用考虑太复杂的鉴权操作。但是对于对外的接口我们就不得不重视这个问题外部接口没有做鉴权的操作就直接发布到互联网而这不仅有暴露数据的风险同时还有数据被篡改的风险严重的甚至是影响到系统的正常运转 方案一Spring BootAop注解实现Api接口签名验证 方案二在已有接口上拦截器拦截接口路径白名单匹配
Spring BootAop注解实现Api接口签名验证
appId和secrettoken时间戳 token的生成过程中在加入时间戳校验token正确性之前先校验时间戳是否在一定时间窗口内比如说1分钟如果超过一分钟直接拒绝请求通过后再校验token。 新接口加上注解 由于项目需要开发第三方接口给多个供应商为保证Api接口的安全性遂采用Api接口签名验证。 一、为什么需要 API 接口签名 对外开放的 API 接口都会面临一些安全问题例如伪装攻击、篡改攻击、重放攻击以及数据信息泄漏的风险。利用 API 接口签名能方便的防范这些安全问题和风险。在设计 API 接口签名时主要考虑以下几点 请求发起时间得在限制范围内 请求的用户是否真实存在 是否存在重复请求 请求参数是否被篡改 1.保证请求数据正确 当请求中的某一个字段的值变化时原有的签名结果就会发生变化。所以只要参数发生变化签名就要发生变化否则请求将会是一个无效的请求。 2.保证请求来源合法 一般情况下生成签名的算法都会成对出现一个 appKey 和一个 appSecret根据 appKey 能识别出调用者身份根据 appSecret 能识别出签名是否合法。 3.识别接口的时效性 一般情况下签名和参数中会包含时间戳这样服务端就可以验证客户端请求是否在有效时间内从而避免接口被长时间的重复调用 4.是否存在重复请求
API 接口签名验签实现机制
签名验签流程图
1 客户端向服务端申请 appKeyappSecret 服务端下发 appKeyappSecret。 2 客户端集成 SDK 产生 sign将 appKey请求参数时间戳sign,随机数nonce 发送到服务端服务端根据请求参数使用 SDK 中的签名规则生成签名来验证sign的合法性之后返回结果。
实现思路
我们按照主要防御措施先后顺序来实现首先已知我们得到以下四个参数
// 供应商的id验证用户的真实性
String appid request.getHeader(appid);
// 请求发起的时间
String timestamp request.getHeader(timestamp);
// 随机数
String nonce request.getHeader(nonce);
// 签名算法生成的签名
String sign request.getHeader(sign);1.请求发起时间得在限制范围内
像这种比较简单就是获取服务器的当前时间去跟请求发起时间比较。
// 限制为含60秒以内发送的请求
long time 60;
long now System.currentTimeMillis() / 1000;
if (now - Long.valueOf(timestamp) time) {return ObjectResponse.fail(请求发起时间超过服务器限制时间);
}2.请求的用户是否真实存在 一般会有以下两个场景 场景一在前后端分离的模式中用户登录后得到token用户调用接口时传递token来确保用户的真实性。 场景二接口调用方不需要登录那么我们接口提供方可以提供appid调用时需要传递与secret在签名算法中使用给接口调用方来验证用户的真实性。 这里我主要说一下场景二如下 // 查询appid是否正确来验证用户的真实性
CoreApiKey apiKey coreApiKeyService.selectByAppid(appid);
if (apiKey null) {return ObjectResponse.fail(appid参数错误);
}是否存在重复请求 这里利用nonce参数每次请求时先判断nonce在redis是否存在存在则认为是重复请求不存在就存放到redis中。但是这会有一个问题随着请求的 次数越来越多那么redis存放的nonce集合会越来越大这肯定不是我们所期望的。这时我们可以巧妙的利用在请求发起时间得在限制范围内中的time服务器限制60秒以内发生的请求因为此步骤主要是验证请求是否重复如果timestamp时间戳变了那就不是重复请求了所以我们可以在nonce存放到redis时给它设置一个过期时间60秒这样既保证了nonce的唯一性也不会发生nonce集合的无限大。
// 验证请求是否重复
if (redisService.hasKeyHashItem(third_party_key, apiKey.getAppid() nonce)) {return ObjectResponse.fail(请不要发送重复的请求);
} else {// 如果nonce没有存在缓存中则加入并设置失效时间秒redisService.setHashItem(third_party_key, apiKey.getAppid() nonce, nonce, time);
}请求参数是否被篡改 利用签名算法来生成签名。主要就是接口调用方的签名算法必须与接口提供方的签名算法一致。签名算法可以自己捣鼓捣鼓我这里是先对key进行字典序排序然后以url的参数格式进行拼接secret在最后拼接最后进行md5加密以下一个Api接口签名验证就大功告成啦
JSONObject signObj new JSONObject();
signObj.put(appid, appid);
signObj.put(timestamp, timestamp);
signObj.put(nonce, nonce);
String mySign getSign(signObj, apiKey.getSecret());
// 验证签名
if (!mySign.equals(sign)) {return ObjectResponse.fail(签名信息错误);
}/*** 获取签名信息* param data* param secret* return*/
private static String getSign(JSONObject data, String secret) {// 由于map是无序的这里主要是对key进行排序字典序SetString keySet data.keySet();String[] keyArr keySet.toArray(new String[keySet.size()]);Arrays.sort(keyArr);StringBuilder sbd new StringBuilder();for (String k : keyArr) {if (StringUtil.isNotEmpty(data.getString(k))) {sbd.append(k data.getString(k) );}}// secret最后拼接sbd.append(secret).append(secret);return MD5Util.encode(sbd.toString());
}5.基于SringBoot以及Redis使用Aop来实现Api接口签名验证的源码
Component
Aspect
Slf4j
public class ThridPartyApiAspect {Autowiredprivate HttpServletRequest request;Autowiredprivate HttpServletResponse response;Autowiredprivate RedisService redisService;Autowiredprivate CoreApiKeyService coreApiKeyService;/*** 表示匹配带有自定义注解的方法*/Pointcut(annotation(com.stan.framework.anno.ThridPartyApi))public void pointcut() {}Around(pointcut())public Object around(ProceedingJoinPoint point) {try {// 供应商的id验证用户的真实性String appid request.getHeader(appid);// 请求发起的时间String timestamp request.getHeader(timestamp);// 随机数String nonce request.getHeader(nonce);// 签名算法生成的签名String sign request.getHeader(sign);if (StringUtil.isEmpty(appid) || StringUtil.isEmpty(timestamp) || StringUtil.isEmpty(nonce) || StringUtil.isEmpty(sign)) {return ObjectResponse.fail(请求头参数不能为空);}// 限制为含60秒以内发送的请求long time 60;long now System.currentTimeMillis() / 1000;if (now - Long.valueOf(timestamp) time) {return ObjectResponse.fail(请求发起时间超过服务器限制时间);}// 查询appid是否正确CoreApiKey apiKey coreApiKeyService.selectByAppid(appid);if (apiKey null) {return ObjectResponse.fail(appid参数错误);}// 验证请求是否重复if (redisService.hasKeyHashItem(third_party_key, apiKey.getAppid() nonce)) {return ObjectResponse.fail(请不要发送重复的请求);} else {// 如果nonce没有存在缓存中则加入并设置失效时间秒redisService.setHashItem(third_party_key, apiKey.getAppid() nonce, nonce, time);}JSONObject signObj new JSONObject();signObj.put(appid, appid);signObj.put(timestamp, timestamp);signObj.put(nonce, nonce);String mySign getSign(signObj, apiKey.getSecret());// 验证签名if (!mySign.equals(sign)) {return ObjectResponse.fail(签名信息错误);}try {return point.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}} catch (Exception e) {e.printStackTrace();return ObjectResponse.fail(解析请求参数异常);}return null;}/*** 获取签名信息* param data* param secret* return*/private static String getSign(JSONObject data, String secret) {// 由于map是无序的这里主要是对key进行排序字典序SetString keySet data.keySet();String[] keyArr keySet.toArray(new String[keySet.size()]);Arrays.sort(keyArr);StringBuilder sbd new StringBuilder();for (String k : keyArr) {if (StringUtil.isNotEmpty(data.getString(k))) {sbd.append(k data.getString(k) );}}// secret最后拼接sbd.append(secret).append(secret);return MD5Util.encode(sbd.toString());}
}6. 测试签名
/*** Author lc* description:* Date 2022/4/26 15:19* Version 1.0*/RestController
Api(tags 对外接口)
RequestMapping(/test)
public class ThirdPartyApiAspectController {ThirdPartyApiApiOperation(对接接口测试)PostMapping(value /test)public ResponseVOString test(HttpServletRequest request){return new ResponseVO (签名校验);}
}