API接口安全性设计,项目中该如何保证API接口安全?

很多时候在开发对外接口的时候,为了保证接口的安全以及服务的稳定,要对接口的访问添加一定的限制规则。

那么就有几个问题需要注意一下:

  1. 请求参数是否被篡改;

  2. 请求来源是否合法;

  3. 请求是否具有唯一性;

  4. 参数签名方式。

我们也是围绕这几个点进行展开说明。

为什么需要接口签名?

前后端分离架构早已成为目前行业主流,现在越来越多的项目以API 的形式对外提供服务,这些 API 接口大多暴露在公网上,所以安全性就变的很重要了。最直接的风险如下:

  • 非法使用 API 服务。(收费接口非法调用)

  •       恶意攻击和破坏。(数据篡改、DOS)

因此需要设计一些接口安全保护的方式来增强接口安全,目前主流的一种方式是API签名。

可能有人会有疑问,现在项目中使用了https后,还需要对数据进行签名来确保数据没有被篡改吗?

答案是有必要。

      这里就需要明确https的工作机制了。

HTTPS是保证通过中间人攻击抓到的报文是密文,无法或者说很难破解。但仍然可以将报文重发,形成DOS攻击。同时,如果不签名,只用 HTTP 简单认证,通过抓包,直接可以获取到 Authorization,就可以随意发起请求了。

因此最安全的方法就是结合 HTTPS 和 API 签名。

所以,综上所述:

  • API 签名保证的是应用的数据安全和防篡改,并且可以作为业务的参数校验和处理重放攻击。

  • HTTPS 保证的是运输层的加密传输,但是无法防御重放攻击。

接口签名

一个服务调用另外一个服务,肯定是有参数需要传递的,我们可以将参数按照以下步骤进行签名。

对所有非空参数值的的参数进行拼接:

key1value1key2value2...
  • 参数名ASCII码从小到大排序(字典序);

  • 如果参数的值为空不参与签名;

  • 参数名区分大小写;

  • 传送的sign参数不参与签名;

  • 1.在stringA最后拼接上secret密钥得到stringSignTemp字符串

  • 2.对stringSignTemp进行MD5加密得到signValue

接口在网络传输过程中,参数值可以被修改,但是因为不了解sign计算方式,一般没法修改sign的值。当服务器调用接口前会按照sign的规则重新计算出sign的值然后和接口传递的sign参数的值做比较,如果相等表示参数值没有被篡改,如果不等,表示参数被非法篡改了。

public class AntiReplayAttackV1Example {

    //如果是对称加密,则是调用方和被调用方都知道的私钥;如果是
    //非对称加密,调用方这里是被调用方生成的公钥
    private static final String SECRET_KEY = "your_secret_key"; 

    // 模拟支付请求类
    static class PaymentRequest {
        private String transactionId;
        private double amount;
        private String nonce;

        public PaymentRequest(String transactionId, double amount, String nonce) {
            this.transactionId = transactionId;
            this.amount = amount;
            this.nonce = nonce;
        }

        public String getTransactionId() {
            return transactionId;
        }

        public double getAmount() {
            return amount;
        }

        public String getNonce() {
            return nonce;
        }
    }

    public static String generateSign(Map<String, String> params, String secret) throws NoSuchAlgorithmException {
        // 将参数按ASCII码从小到大排序
        Map<String, String> sortedParams = new TreeMap<>(params);
        StringBuilder stringA = new StringBuilder();

        // 拼接成字符串stringA
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
                stringA.append(entry.getKey()).append(entry.getValue());
            }
        }

        // 在stringA最后拼接上secret密钥得到stringSignTemp字符串
        String stringSignTemp = stringA.toString() + secret;

        // 对stringSignTemp进行MD5加密得到signValue
        return md5(stringSignTemp);
    }

    private static String md5(String input) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] messageDigest = md.digest(input.getBytes());
        StringBuilder sb = new StringBuilder();

        for (byte b : messageDigest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        try {
            // 模拟一个支付请求
            PaymentRequest request = new PaymentRequest("txn-001", 100.0, "nonce-123");

            // 模拟请求参数集合
            Map<String, String> params = new TreeMap<>();
            params.put("transactionId", request.getTransactionId());
            params.put("amount", String.valueOf(request.getAmount()));
            params.put("nonce", request.getNonce());

            // 生成签名
            String sign = generateSign(params, SECRET_KEY);
            System.out.println("Generated Sign: " + sign);

            // 模拟服务器端验证签名
            boolean isValid = verifySign(params, sign, SECRET_KEY);
            System.out.println("Is Sign Valid: " + isValid);

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    public static boolean verifySign(Map<String, String> params, String providedSign, String secret) throws NoSuchAlgorithmException {
        // 生成签名
        String generatedSign = generateSign(params, secret);
        // 比较生成的签名和提供的签名
        return generatedSign.equals(providedSign);
    }
}

//输出:
Generated Sign: 1f7cc39bcb0eb7e293c29d49906d69d6
Is Sign Valid: true

示例代码中严格来说是一个demo,大家还需根据实际情况进行对应关键属性的传递设计。

通过代码可以发现,签名的方式是只能验证数据有没有被修改,但是,防不了重放攻击。

我们继续往下进行。

重放攻击

      什么是重放攻击

图片

API重放攻击(Replay Attacks)又称为重播攻击。就是把你的请求原封不动地再发送一次,两次...n次,一般正常的请求都会通过验证进入到正常逻辑中,如果这个正常逻辑是插入数据库操作,那么一旦插入数据库的语句写的不好,就有可能出现多条重复的数据。一旦是比较慢的查询操作,就可能导致数据库堵住等情况,如果是付款接口,或者购买接口就会造成损失。因此需要采用防重放的机制来做请求验证,下面介绍如何对接口做防重放攻击。

带时间戳的签名算法

请求端:timestamp由请求方生成,代表请求被发送的时间(需双方共用一套时间计数系统)随请求参数一并发出,并将 timestamp作为一个参数加入 sign 加密计算。

服务端:平台服务器接到请求后对比当前时间戳,设定不超过30s 即认为该请求正常,否则认为超时拒绝服务

但是这样还是有缺陷的,若攻击者如果在30s之内进行重放攻击那就没办法了,因为30s之内的请求都认为是合法请求,那将这30s设置的小一些,那多小算小了?太小的话,如果网络拥挤,会将正常请求也拒绝掉的 !因此将时间改小这不是一个解决问题的根本办法。所以更进一步地,可以为sign 加上一个随机码(称之为盐值)这里我们定义为 nonce。

带nonce的签名算

请求方:nonce 是由请求方生成的随机数(在规定的时间内保证有充足的随机数产生,即在60s 内产生的随机数重复的概率为0)也作为参数之一加入 sign 签名。 

服务端:服务器接受到请求先判定 nonce 是否被请求过(一般会放到redis中),如果发现 nonce 参数在规定时间是全新的则正常返回结果,反之,则判定是重放攻击拒绝服务。

这里注意对于处理过的请求,将其nonce存放到redis的时候设置过期时间,一定要配置过期时间。否则占用Redis空间会越来越大。

接口签名总结

上面所有内容可以概括为接口签名,接口签名是目前主流的方案。核心处理流程为:

接口签名总结起来就是通过一些签名规则对参数进行签名,然后把签名的信息放入请求头部,服务端收到客户端请求之后,同样的只需要按照已定的规则生产对应的签名串与客户端的签名信息进行对比,如果一致,就进入业务处理流程;如果不通过,就提示签名验证失败。

图片

在接口签名方案中,主要有四个核心参数:

1、appid表示应用ID(可以视情况添加),接口请求数据记性签名加密,不同的对接项目分配不同的appid,保证数据安全。

2、timestamp 表示时间戳,当请求的时间戳与服务器中的时间戳,差值在5分钟之内,属于有效请求,不在此范围内,属于无效请求

3、nonce 表示随机数,用于防止重复提交验证

4、signature 表示签名字段,用于判断接口请求是否有效。

我再补充一个大家会首先想到但存在较大问题的方案,就是token方案。

token方案

图片

    从上图,我们可以很清晰的看到,token 方案的实现主要有以下几个步骤:

  • 1、用户登录成功之后,服务端会给用户生成一个唯一有效的凭证,这个有效值被称为token

  • 2、当用户每次请求其他的业务接口时,需要在请求头部带上token

  • 3、服务端接受到客户端业务接口请求时,会验证token的合法性,如果不合法会提示给客户端;如果合法,才会进入业务处理流程。

在实际使用过程中,当用户登录成功之后,生成的token存放在redis中时是有时效的,一般设置为2个小时,过了2个小时之后会自动失效,这个时候我们就需要重新登录,然后再次获取有效token。

token方案,是目前业务类型的项目当中使用最广的方案,而且实用性非常高,可以很有效的防止黑客们进行抓包、爬取数据。

但是 token 方案也有一些缺点!最明显的就是与第三方公司进行接口对接的时候,当你的接口请求量非常大,这个时候 token 突然失效了,会有大量的接口请求失败。

正常流程是当token失效时,会调用刷新token接口,刷新完成之后,在token失效与重新刷新token这个时间间隔期间,就会出现大量的请求失败的日志,因此在实际API对接过程中,我不推荐大家采用 token方案。

以上为全部内容。

更多技术内容,欢迎扫码关注10W+技术社区。

  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值