开发API接口的安全验证:token,参数签名,时间戳 |
您所在的位置:网站首页 › 肯德基app地址请求数据不合法怎么解决 › 开发API接口的安全验证:token,参数签名,时间戳 |
前言:服务端与前端对接的API接口,如果被第三方抓包并进行恶意篡改参数,可能会导致数据泄露和篡改数据,下面主要围绕token,签名,时间戳,三个部分来保证API接口的安全性。 强烈推荐,简单明了: 瞧瞧别人家的API接口,那叫一个优雅 1.用户成功登陆站点后,服务器会返回一个token,用户的任何操作都必须带了这个参数,可以将这个参数直接放到header里。 2.客户端用需要发送的参数和token生成一个签名sign,作为参数一起发送给服务端,服务端在用同样的方法生成sign进行检查是否被篡改。 3.但这依然存在问题,可能会被进行恶意无限制访问,这时我们需要引入一个时间戳参数,如果超时即是无效的。 4.服务端需要对token,签名,时间戳进行验证,只有token有效,时间戳未超时,签名有效才能被放行。 一、接口安全问题请求身份是否合法? 请求参数是否被篡改? 请求是否唯一? 开放接口 没有进行任何限制,简单粗暴的访问方式,这样的接口方式一般在开放的应用平台,查天气,查快递,只要你输入正确对应的参数调用,即可获取到自己需要的信息,我们可以任意修改参数值。 /* * Description: 开放的接口 */ @RestController @RequestMapping("/token") public class TokenSignController { @Autowired private TokenSignService tokenSignService; @RequestMapping(value = "openDemo",method = RequestMethod.GET) public List openDemo(int personId){ return tokenSignService.getPersonList(personId); } } 二、请求身份合法性:Token认证参数在APP开放API接口的设计中,由于大多数接口涉及到用户的个人信息以及产品的敏感数据,所以要对这些接口进行身份验证,为了安全起见让用户暴露的明文密码次数越少越好,然而客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上Token身份验证信息。 用户登录成功后,会获取一个ticket值,接下去任何接口的访问都需要这个参数。我们把它放置在redis内,有效期为10分钟,在ticket即将超时,无感知续命。延长使用时间,如果用户在一段时间内没进行任何操作,就需要重新登录系统。 @RequestMapping(value = "login",method = RequestMethod.POST) public JSONObject login(@NotNull String username, @NotNull String password){ return tokenSignService.login(username,password); }登录操作,查看是否有这个用户,用户名和密码匹配即可成功登录。 /** * * Description:验证登录,ticket成功后放置缓存中, * @param */ public JSONObject login(String username,String password){ JSONObject result = new JSONObject(); PersonEntity personEntity = personDao.findByLoginName(username); if (personEntity == null || (personEntity != null && !personEntity.getPassword().equals(password))){ result.put("success",false); result.put("ticket",""); result.put("code","999"); result.put("message","用户名和密码不匹配"); return result; } if (personEntity.getLoginName().equals(username) && personEntity.getPassword().equals(password)){ String ticket = UUID.randomUUID().toString(); ticket = ticket.replace("-",""); redisTemplate.opsForValue().set(ticket,personEntity.getLoginName(),10L, TimeUnit.MINUTES); result.put("success",true); result.put("ticket",ticket); result.put("code",200); result.put("message","登录成功"); return result; } result.put("success",false); result.put("ticket",""); result.put("code","1000"); result.put("message","未知异常,请重试"); return result; } 三、防止篡改:Sign签名参数抓包工具拦截http请求后将里面的参数修改,然后再提交到后台,这就已经可以造成比较严重的后果了:比如删除一条记录,在删除过程中篡改记录id,就可以删除任何不再当前用户权限范围内的记录。 把所有的参数拼接一起,在加入系统秘钥SecretKey(前后端约定好,不能泄漏),进行MD5计算生成一个sign签名,防止参数被人恶意篡改,后台按同样的方法生成秘钥,进行签名对比。 /** * @param request * @return */ public static Boolean checkSign(HttpServletRequest request,String sign){ Boolean flag= false; //检查sigin是否过期 Enumeration pNames = request.getParameterNames(); Map params = new HashMap(); while (pNames.hasMoreElements()) { String pName = (String) pNames.nextElement(); if("sign".equals(pName)) continue; String pValue = (String)request.getParameter(pName); params.put(pName, pValue); } System.out.println("现在的sign-->>" + sign); System.out.println("验证的sign-->>" + getSign(params,secretKeyOfWxh)); if(sign.equals(getSign(params, secretKeyOfWxh))){ flag = true; } return flag; }请求携带参数Token和Sign,只有拥有合法的身份Token和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到SecretKey(仅作本地加密使用,不参与网络传输),无法伪造合法的请求。 四、重放攻击:时间戳参数 4.1、重放攻击Sign签名参数虽然解决了请求参数被篡改的隐患,但是还存在着重复使用请求参数伪造二次请求的隐患。举个例子,用户发起一个付款请求,请求中带着付款参数,攻击者拦截了该请求,向服务器重复的发送该请求,如果服务器端没有配置防止重放攻击策略,则可能会进行多次付款,造成用户损失。 4.2、引入timestamp参数引入一个时间戳参数,保证接口仅在一分钟内有效,需要和客户端时间保持一致。 public static long getTimestamp() { long timestampLong = System.currentTimeMillis(); long timestampsStr = timestampLong / 1000; return timestampsStr; }在请求中增加 timestamp 参数要来表示请求时间戳,服务方端接收该请求后,根据当前时间生成一个接收时间戳,然后根据两个时间戳的差值进行请求判定,如果差值大于指定的阈值,则认为请求无效,否则请求通过。关于阈值的选定,可以根据接口的响应速度进行适当的调整,一般默认为 60 秒。 //需要跟当前服务器时间进行对比,如果超过一分钟,就拒绝本次请求,节省服务器查询的请求消耗 if((接收时间戳-请求时间戳) > 60) { "请求失败" } else { "请求通过" }该方案要求请求和响应的双方必须进行时间同步,如果服务的双方本身存在时间上的差异,会对差值的计算产生影响,最后导致请求的判定产生偏差。 缺点:通过上面的判定逻辑可以发现,在小于阈值的时间段内是可以进行重复请求的,该方案不能保证请求仅一次有效。 4.3、Redis或者Guava缓存保证请求仅一次有效引入timestamp参数不能保证请求仅一次有效,此时可以引入Redis分布式缓存或者Guava本地缓存来解决。 在请求中增加一个通过指定规则产生的 token,标识请求的唯一性,服务方接收该请求后,先判断缓存集合中是否存在该 token,如果存在则认为此次请求无效,否则将 token 放入缓存中,通过请求通过。 伪代码如下: if(token 存在于缓存集合中) { "请求失败" } else { 将 token 放入集合中 "请求通过" } 4.4、timestamp 和 缓存 结合优化方案加时间戳判断主要是为了减少缓存查询的次数,超时的请求直接就返回了。 if((接收时间戳-请求时间戳) > 60秒){ "请求失败" } if(token 存在于缓存集合中){ "请求失败" } else { 将 token 放入集合中,缓存时间60秒 "请求通过" } 五、拦截器每次请求都带有这三个参数,我们都需要进行验证,只有在三个参数都满足我们的要求,才允许数据返回或被操作。 public class LoginInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws IOException { JSONObject jsonObject = new JSONObject(); String ticket = request.getParameter("ticket"); String sign = request.getParameter("sign"); String ts = request.getParameter("ts"); if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts)){ jsonObject.put("success",false); jsonObject.put("message","args is isEmpty"); jsonObject.put("code","1001"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); return false; } //如果redis存在ticket就认为是合法的请求 if (redisTemplate.hasKey(ticket)){ System.out.println(redisTemplate.opsForValue().getOperations().getExpire(ticket)); String values = (String) redisTemplate.opsForValue().get(ticket); //判断ticket是否即将过期,进行续命操作 if (redisTemplate.opsForValue().getOperations().getExpire(ticket) != -2 && redisTemplate.opsForValue().getOperations().getExpire(ticket) 600){ jsonObject.put("success",false); jsonObject.put("message","Overtime to connect to server"); jsonObject.put("code","1002"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); return false; } //验证签名 if (!SignUtils.checkSign(request,sign)){ jsonObject.put("success",false); jsonObject.put("message","sign is invalid"); jsonObject.put("code","1003"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); return false; } return true; }else { jsonObject.put("success",false); jsonObject.put("message","ticket is invalid,Relogin."); jsonObject.put("code","1004"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); } return false; } } |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |