写点什么

面试官:你讲下接口防重放如何处理?

  • 2024-06-11
    福建
  • 本文字数:2526 字

    阅读完需:约 8 分钟

前言


我们的 API 接口都是提供给第三方服务/客户端调用,所有请求地址以及请求参数都是暴露给用户的。

我们每次请求一个 HTTP 请求,用户都可以通过 F12,或者抓包工具 fd 看到请求的 URL 链接,然后 copy 出来。这样是非常不安全的,有人可能会恶意的刷我们的接口,那这时该怎么办呢?防重放攻击就出来了。


什么是防重放攻击


我们以掘金文章点赞为例。当我点赞之后,H5 会发送一个请求给到掘金后端服务器,我可以通过 f12 看到完整的请求参数,包括 url,param 等等,然后我可以通过 copy 把这个请求给 copy 出来,那么我就可以做到一个放重放攻击了。



具体如下。我们可以看到,服务端返回的是重复点赞,也就是掘金并没有做我们所谓世俗意义上的放重放攻击。掘金通过查询数据库(推测 item_id 是唯一索引值),来判断是否已经点赞然后返回前端逻辑。



那么什么是我们理解的放重放呢


简单来说就是,前端和客户端约定一个算法(比如 md5),通过加密时间戳+传入字段。来起到防止重复请求的目的。然后这个时间戳可以设定为 30 秒,60 秒过期。

那么如果 30 秒,有人不断刷我们的接口怎么办。我们还可以新加一个字段为 nonceKey,30 秒内随机不重复。这个字段存放在 Redis,并且 30 秒过期。如果下一次请求 nonceKey 还在 redis,我就认为是重复请求,拒绝即可。


算法实现


  1. 首先定义一个全局拦截器

@Componentpublic class TokenInterceptor implements HandlerInterceptor {
@Autowired private StringRedisTemplate redisService;
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String timestamp = request.getParameter("timestamp"); String token = request.getParameter("token"); if (timestamp == null || token == null) { return false; }
TreeMap<String, String> map = new TreeMap<>(); Enumeration<String> parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { String str = parameterNames.nextElement(); if (StringUtils.equals(str, "token")) { continue; } map.put(str, request.getParameter(str)); }
return SecretUtils.extractSecret(redisService, timestamp, token, map); }}
复制代码


  1. 定义具体的算法实现

public class SecretUtils {
private static final long NONCE_DURATION = 60 * 1000L; private static final String SALT = "salt"; // 注意这块加盐
public static boolean extractSecret(StringRedisTemplate redisService, String timestamp, String token, TreeMap<String, String> map) { if (StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(token)) { return false; } long ts = NumberUtils.toLong(timestamp, 0); long now = System.currentTimeMillis(); if ((now - ts) > SecretUtils.NONCE_DURATION || ts > now) { return false; }
StringBuilder sb = new StringBuilder(); map.put(SALT, SALT); for (Map.Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if (sb.length() > 0) { sb.append("&"); } sb.append(key).append("=").append(value); }
String targetToken = DigestUtils.md5DigestAsHex(sb.toString().getBytes()); if (!token.equals(targetToken)) { return false; }
String s = redisService.opsForValue().get(timestamp); if (StringUtils.isNotEmpty(s)) { return false; } else { redisService.opsForValue().set(timestamp, timestamp, NONCE_DURATION, TimeUnit.MILLISECONDS); }
return true; }
}
复制代码


前端会通过我们事先约定好的算法以及方式,将字符串从小到大进行排序 + timestamp,然后 md5 进行加密生成 token 传给后端。后端根据算法+方式来校验 token 是否有效。


如果其中有人修改了参数,那么 token 就会校验失败,直接拒绝即可。如果没修改参数,timestamp 如果大于 60s,则认为是防重放攻击,直接拒绝,如果小于 30s,则将 nonceKey 加入到 redis 里面,这里 nonceKey 用的是 timestamp 字段,如果不存在则第一次请求,如果存在,则直接拒绝即可。


通过这么简单的一个算法,就可以实现防重放攻击了。


Q&A


Q:客户端和服务端生成的时间戳不一致怎么办

A:客户端和服务端生成的是时间戳,不是具体的时间,时间戳是指格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数


Q:HTTPS 数据加密是否可以防止重放攻击

A:不可以。https 是在传输过程中保证了加密,也就是说如果中间人,获取到了请求,他是无法解开传输的内容的。

举个最简单的例子,上课和同学传纸条的时候,为了不让中间给递纸条的人看到或者修改,可以在纸条上写成只有双方能看明白密文,这样递纸条的过程就安全了,传纸条过程中的人就看不懂你的内容了。但是如果给你写纸条的人要搞事情,那就是加密解决不了的了。这时候就需要放重放来解决了。


Q:防重放攻击是否有用,属于脱裤子放屁

A:个人感觉有一点点吧。比如防重放攻击的算法+加密方式其实大多数用的都是这些,其实攻击人很容易就能猜到 token 生成的方式,比如 timestamp + 从小到大排序。因此我们加入了 salt 来混淆视听,这个 salt 需要前端、客户端安全的存储,不能让用户知道,比如 js 混淆等等。但其实通过抓包,js 分析还是很容易能拿到的。但无形中增加了攻击人的成本,比如网易云登录的 js 加密类似。


Q:做了防重放,支付,点赞等是否不需要做幂等了

A:需要。最重要的幂等,一定要用数据库来实现,比如唯一索引。其他都不可相信。


最后


以我个人的理解。防重放用处不大,其他安全措施,比如非对称的 RSA 验签更加有效。就算用户拿到了请求的所有信息,你的接口也一定要做幂等的,尤其是像支付转账等高危操作,幂等才是最有用的防线。而且防重发生成 token 的算法,大家都这样搞,攻击者怎么可能不知道呢?这点我不太理解。


文章转载自:程序员博博

原文链接:https://www.cnblogs.com/wenbochang/p/18240697

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
面试官:你讲下接口防重放如何处理?_程序员_不在线第一只蜗牛_InfoQ写作社区