API接口安全方案

Posted noname

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了API接口安全方案相关的知识,希望对你有一定的参考价值。

案例

有个真实案例,安全方案为:

  1. 线下向客户分配clientIdsecretKey
  2. 请求参数会附加上clientId=xxx&ts=xxx&sign=xxx,其中:
  3. ts为请求时的时间戳,跟服务器的收到请求时的时间戳不能超过1分钟
  4. sign为签名串,签名规则是sign = sha256(clientId + "," + secretKey + "," + ts)
  5. 数据以HTTP body的方式明文发送。

以上方案有什么问题?

背景

随着Internet网的广泛应用,信息安全问题日益突出,系统间的接口交互,每个请求都有可能被抓取到数据、被伪造请求去获取数据或者攻击服务,所以接口安全至关重要。
API接口要做到:

  • 防伪装攻击:第三方恶意调用接口。
  • 防篡改攻击:请求在传输过程被修改。
  • 防重放攻击:请求被截获后,被多次重放。
  • 防数据信息泄漏:被截获到系统数据,例如账号、密码、交易信息等。

安全方案

重复校验:时间戳 + 流水号

不管数据加密算法做的多么好,如果没有防重放攻击措施,攻击者可以直接把抓取到的请求重复发送来攻击,所以接口需要对请求做重复校验。
可以在请求信息里加入:

  1. 加入时间戳,1分钟内有效。
  2. 加入流水号,流水号在时间戳有效时间范围内需保证唯一。

这两个信息缺一不可:

  • 如果只有时间戳,那在有效时间戳范围内,请求就存在被重复请求的风险。
  • 如果只有流水号,那就要把历史所有流水号都永久存储起来,否则假如只存储最近3个月的数据,那3个月前的请求串就有被人拿出来重复请求的风险,但是永久存储数据的话,就存在存储成本高查询性能(数据量大)问题。

签名

为了防篡改攻击,需要将传递的参数做一次校验,可以在请求信息里增加签名(sign),将请求的内容拼装加密,服务端做同样的事情,然后将加密后的签名跟传过来的签名做等值比较。
常见的一种签名方式是:将请求参数&值按自然排序拼接后加密。例如:/test?a=5&c=1&b=3&z=value,那拼接后sign的值就是以下参数串加密后的结果:

a=5&b=3&c=1&nonce=7536080117&time=1618851899342&z=value

注意这里也要将时间戳和流水号一起拼进去。
最终的请求串是:

/test?a=5&c=1&b=3&z=value&nonce=7536080117&time=1618851899342&sign=加密串

这里有用到加密,就涉及的秘钥的问题,可以线下分配APP IDsecret,请求串里加上appId=xxx,让服务端可以通过appId查出secretappId也参与到sign的加密内容里。
有了sign可以校验后,除非黑客知道了secret加密算法,否则即使取到数据,也无法篡改/伪造数据。appIdtimenoncesign也可以不拼接在url后,而是放在请求头Header里。
如果系统是以将数据串放在HTTP body里的方式传递,为了防止出现数据被篡改的风险(请求被拦截,然后拦截方将body数据修改,其他信息不改,之后再转到服务端),可以将body的内容一起拼接进签名中(后面会讲到加密,用了加密之后可以不拼接进签名中)。

加密

为了防止数据信息泄漏,我们需要将业务数据加密传输,同时签名里也有用到加密,这里就涉及到了加密算法。

  • 对称加密:较传统的加密体制,通信双方在加/解密过程中使用他们共享的单一密钥,鉴于其算法简单和加密速度快的优点,目前仍然是主流的密码体制之一,但是相比非对称加密的安全性就没那么高。
  • 非对称加密:它有一对秘钥(公钥私钥),通过公钥加密的内容,只有私钥才可以解开,而通过私钥加密的内容,只有公钥才可以解开;算法的密钥很长,具有较好的安全性,但加密的计算量很大,加密速度较慢限制了其应用范围。
为什么非对称加密比对称加密慢?
因为对称加密主要的运算是位运算,速度非常快,如果使用硬件计算,速度会更快。但是非对称加密计算一般都比较复杂,比如 RSA,它里面涉及到大数乘法、大数模等等运算,在电路上实现“加法”比异或要麻烦的多,况且后面还有一个模运算。

AES和RSA结合
由于RSA加解密速度慢,不适合大内容加密。如果使用AES对传输数据加密,使用RSA来加密AES的密钥,就可以综合发挥AES和RSA的优点同时避免它们缺点来实现一种新的数据加密方案。

最终方案

  1. 线下分配APP IDSecretRSA公钥
  2. 随机生成16位的AES秘钥
  3. 请求头加入时间戳,1分钟内有效。
  4. 请求头加入流水号,流水号在时间戳有效时间范围内需保证唯一。
  5. 请求头加入APP ID。
  6. 请求头加入加密后的AES秘钥,通过RAS公钥加密。
  7. 请求头加入签名,签名算法为:AES加密(APPID + Secret + 加密后的AES秘钥 + 时间戳 + 流水号)。这里之所以额外加一个Secret,是假设被第三方拿到公钥,还有这一层保护。
  8. 请求body里的业务数据传的是AES加密后的内容。

如果服务端返回的数据也需要做加密的话,就再加一套RSA秘钥:服务端用RAS公钥+随机生成的AES秘钥加密,请求方用RSA私钥解密。或者直接复用一套RAS秘钥也行,服务器用私钥加密,请求方用公钥解密,但是这样安全性会低一点。

伪代码:

// 提前分配好的信息:
String appId = "appId"; //分配的appId
String secret = "Secret"; //分配的Secret
String rasPublicKey = "rasPublicKey"; // RSA 公钥
// 请求时动态生成的信息:
long time = now(); // 时间戳
String nonce = uuid(); //流水号,保证唯一
String aesSecret = randomString(16); //随机生成16位AES秘钥
String encodeAesSecret = ras(aesSecret, rasPublicKey); // 对AES秘钥做RAS加密
String sign = aes(appId + ":" + secret + ":" + time + ":" + nonce + ":" + encodeAesSecret, encodeAesSecret); // 生成签名,这里可以将body一起拼接进来
headers.header("appId", appId) // header放入鉴权信息
        .header("time", time)
        .header("nonce", nonce)
        .header("sign", encodeSign)
        .header("secretKey", encodeSecret);
String body = "body";
String encodeBody = ase(body, encodeAesSecret);
post(url, encodeBody, headers); // 请求

案例回顾

前面真实案例的问题是:

  1. 在有效期的一分钟内,可以无限制重放请求。
  2. 攻击者拦截了请求,即使不知道算法秘钥,也可以在不改sign的情况下,修改body里的数据,再重新发送。
  3. SHA哈希算法,假如算法秘钥泄漏,则攻击者会成为上帝,可以任意的拦截、篡改请求,获取请求数据和返回数据。

示例代码

加密工具类

import java.io.UnsupportedEncodingException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

/**
 * @author 
 * @data 
 * @description
 */
public class SecurityUtils {

    private static final String DEFAULT_CODING = "UTF-8";
    private static final String AES_ALGORITHM = "AES";
    private static final String RSA_ALGORITHM = "RSA";
    private static final int RSA_KEYSIZE = 1024;
    /**
     * 填充方式
     */
    private static final String AES_CIPHER_PADDING = "AES/ECB/PKCS5Padding";
    private static final int PASSWORD_LENGTH = 16;

    private SecurityUtils() {

    }

    /**
     * AES加密
     *
     * @param content
     * @param password
     * @return java.lang.String
     * @author 
     * @date 
     */
    public static String aes128Encrypt(String content, String password) {
        try {
            checkPassword(password);
            Cipher cipher = Cipher.getInstance(AES_CIPHER_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, buildAes128SecretKey(password));
            byte[] result = cipher.doFinal(getContentByte(content));
            return encodeContentByte(result);
        } catch (Exception e) {
            throw new RuntimeException("aes 128 encrypt error!", e);
        }
    }

    /**
     * @param content
     * @param password
     * @return java.lang.String
     * @author 
     * @date 
     */
    public static String aes128Decrypt(String content, String password) {
        try {
            checkPassword(password);
            Cipher cipher = Cipher.getInstance(AES_CIPHER_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, buildAes128SecretKey(password));
            byte[] result = cipher.doFinal(decodeContentByte(content));
            return new String(result, DEFAULT_CODING);
        } catch (Exception e) {
            throw new RuntimeException("aes 128 decrypt error!", e);
        }
    }

    /**
     * @param password
     * @author 
     * @date 
     */
    private static void checkPassword(String password) {
        if (StringUtils.isBlank(password) || StringUtils.length(password) != PASSWORD_LENGTH) {
            throw new IllegalArgumentException("Password not available!");
        }
    }

    /**
     * 生成加密秘钥
     *
     * @param password
     * @return javax.crypto.spec.SecretKeySpec
     * @author 
     * @date 
     */
    private static SecretKeySpec buildAes128SecretKey(String password) {
        try {
            //返回生成指定算法密钥生成器的 KeyGenerator 对象
            KeyGenerator kgen = KeyGenerator.getInstance(AES_ALGORITHM);
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            random.setSeed(password.getBytes(DEFAULT_CODING));
            kgen.init(128, random);
            SecretKey secretKey = kgen.generateKey();
            return new SecretKeySpec(secretKey.getEncoded(), AES_ALGORITHM);
        } catch (Exception e) {
            throw new RuntimeException("build aes 128 secret key error!", e);
        }
    }

    /**
     * @return com.example.laboratory.security.util.SecurityUtils.RsaKey
     * @author 
     * @date 
     */
    public static RsaKey generateRsaKey() {
        KeyPairGenerator keyPairGen;
        try {
            keyPairGen = KeyPairGenerator.getInstance(RSA_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("generate rsa key error!", e);
        }
        keyPairGen.initialize(RSA_KEYSIZE);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RsaKey(encodeContentByte(publicKey.getEncoded()), encodeContentByte(privateKey.getEncoded()));
    }

    /**
     * @param publicKey
     * @return java.security.PublicKey
     * @author 
     * @date 
     */
    private static PublicKey buildRsaPublicKey(String publicKey) {
        try {
            //通过X509编码的Key指令获得公钥对象
            KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
            X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(decodeContentByte(publicKey));
            return keyFactory.generatePublic(x509KeySpec);
        } catch (Exception e) {
            throw new RuntimeException("build rsa public key error!", e);
        }
    }

    /**
     * @param privateKey
     * @return java.security.PrivateKey
     * @author 
     * @date 
     */
    private static PrivateKey buildRsaPrivateKey(String privateKey) {
        try {
            //通过PKCS#8编码的Key指令获得私钥对象
            KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
            PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(decodeContentByte(privateKey));
            return keyFactory.generatePrivate(pkcs8KeySpec);
        } catch (Exception e) {
            throw new RuntimeException("build rsa private key error!", e);
        }
    }

    /**
     * RSA默认对加密内容有最大限制,超出限制会出现"IllegalBlockSizeException: Data must not be longer than 117 bytes"
     * 如果要对长内容做加密,就用RSA分段加密的方式
     *
     * @param content
     * @param publicKey
     * @return java.lang.String
     * @author 
     * @date 
     */
    public static String rsaPublicEncrypt(String content, String publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, buildRsaPublicKey(publicKey));
            byte[] encryptedData = cipher.doFinal(getContentByte(content));
            return encodeContentByte(encryptedData);
        } catch (Exception e) {
            throw new RuntimeException("rsa public encrypt error!", e);
        }
    }

    /**
     * @param content
     * @param privateKey
     * @return java.lang.String
     * @author 
     * @date 
     */
    public static String rsaPrivateDecrypt(String content, String privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, buildRsaPrivateKey(privateKey));
            byte[] decryptData = cipher.doFinal(decodeContentByte(content));
            return new String(decryptData, DEFAULT_CODING);
        } catch (Exception e) {
            throw new RuntimeException("rsa public encrypt error!", e);
        }
    }

    /**
     * @param content
     * @return byte[]
     * @author 
     * @date 
     */
    private static byte[] getContentByte(String content) throws UnsupportedEncodingException {
        return content.getBytes(DEFAULT_CODING);
    }

    /**
     * @param bytes
     * @return java.lang.String
     * @author 
     * @date 
     */
    private static String encodeContentByte(byte[] bytes) {
        return Base64.encodeBase64String(bytes);
    }

    /**
     * @param content
     * @return byte[]
     * @author 
     * @date 
     */
    private static byte[] decodeContentByte(String content) {
        return Base64.decodeBase64(content);
    }

    /**
     * @author 
     * @date 
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class RsaKey {

        private String publicKey;
        private String privateKey;
    }

    public static void main(String[] args) throws Exception {
        String content = "测试DEMO";
        System.out.println("-------AES-------");
        String password = "123456789abcdefg";
        String encrypt = SecurityUtils.aes128Encrypt(content, password);
        System.out.println("aes encrypt: " + encrypt);
        System.out.println(SecurityUtils.aes128Decrypt(encrypt, password));
        System.out.println("-------RSA-------");
        RsaKey rsaKey = SecurityUtils.generateRsaKey();
        System.out.println("ras public key:" + rsaKey.getPublicKey());
        System.out.println("ras private key:" + rsaKey.getPrivateKey());
        encrypt = SecurityUtils.rsaPublicEncrypt(content, rsaKey.getPublicKey());
        System.out.println("rsa encrypt: " + encrypt);
        System.out.println(SecurityUtils.rsaPrivateDecrypt(encrypt, rsaKey.getPrivateKey()));
    }
}

测试用例:服务端接口

import com.example.laboratory.security.util.SecurityUtils;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 
 * @date 
 */
@RestController
@RequestMapping("/sign")
public class SignController {

    public static String KEY_PUBLIC = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD0dxq1W8t3rAmZSHRLAddGAWtX5wlDdtLXHoC5MUkXO6EFO/b+z+8jPfqxr4hgnnPseIM7qRx78sEKfI0iu/HzbfBz7uctF5+PU9m7axYfpHDHrl/59bdHBXiIA9/9UMcy+3aDEU6B4lnkhrJWD8OLUOrgWuy/uaQmGB3Dm0m6wwIDAQAB";
    public static String KEY_PRIVATE = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAPR3GrVby3esCZlIdEsB10YBa1fnCUN20tcegLkxSRc7oQU79v7P7yM9+rGviGCec+x4gzupHHvywQp8jSK78fNt8HPu5y0Xn49T2btrFh+kcMeuX/n1t0cFeIgD3/1QxzL7doMRToHiWeSGslYPw4tQ6uBa7L+5pCYYHcObSbrDAgMBAAECgYEAhVbZiIYTCqkZazPrymWsp5Bqnj1z/go3ogIPL/PD7BooD5TPedislMpfjL8zYY/LpvVsjwQEd07HIBMjYAinRJCO1K5J6esWZ43nqp96Mf4bulqFW34uvP3vPS+yAbZ/GI+KkmXGgB9zn5cCWLVqlTZSgrF/dZXMlXR5gVHoP5ECQQD8d21IV2J0Y5yTY0TjdylE3rZI2386hw6/SE2kxmdh6GAAgFafYQ0k/TYx3J9JjervtC5zAhDMHBqBKIdXiWudAkEA9+MCZyX0kavooymzZUDnu5XYkwZON73wtdKICjnYj7cOHdhTB4rInf7xJRwvwBk4Ct/5erNb6lm2/SIj7wDh3wJAdqJ4C+JkNWUJkoi3OlwoXGB7L8lVA9+rIl+LfL5unidf1Vx5V/N3BcamzM9rWlkB6Rm2Kfzyf7dFDSRKVOwSUQJAVrgM7CbkE14PiZ0aDE8TgpVeabjn/iotnn4jZ2hrMYO5pYk7KsVLf7JjjDb7IXnxGCTYsysx+Z8fHBkodwFZAwJAE706uG6seIi1zQAgnbioR4oYBOdQEnFMVOHDmv+0iaK/LQVUS99tZV6x+HzN2lZAxwt9ta+szWijvNGwneRa1Q==";
    public static String APP_ID = "testApp";
    // 假设被拿到公钥,还有这一层保护
    public static String APP_SECRET = "testSecret123456";
    /**
     * 请求允许时间差
     */
    private static int TIME_RANGE_MINUTES = 2;

    /**
     * 签名:唯一标识串前缀
     */
    private static String SIGN_UQKEY_PREFIX = "SIGN:";
    private LoadingCache<String, String> signCache;
    private Map<String, AppData> appCache;

    /**
     * @author 
     * @date
     */
    public SignController() {
        signCache = CacheBuilder.newBuilder()
                .refreshAfterWrite(TIME_RANGE_MINUTES * 60 + 1, TimeUnit.SECONDS)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) {
                        return buildCacheUniqueValue(key);
                    }
                });
        appCache = new HashMap<>();
        appCache.put(APP_ID, new AppData(APP_SECRET, KEY_PRIVATE));
    }

    /**
     * @param requestBody
     * @param sign
     * @param appId
     * @param time
     * @param nonce
     * @param encodeSecretKey
     * @return java.lang.String
     * @author 
     * @date
     */
    @PostMapping("/encode")
    public String encode(@RequestBody String requestBody,
            @RequestHeader("sign") String sign,
            @RequestHeader("appId") String appId,
            @RequestHeader("time") Long time,
            @RequestHeader("nonce") String nonce,
            @RequestHeader("secretKey") String encodeSecretKey) {
        checkAccessFrequently(time);
        String secretKey = decodeSecretKey(appId, encodeSecretKey);
        checkSign(sign, appId, time, nonce, secretKey, encodeSecretKey);
        checkUnique(appId, time, nonce);
        return SecurityUtils.aes128Decrypt(requestBody, secretKey);
    }

    /**
     * @param appId
     * @param secretKey
     * @return java.lang.String
     * @author 
     * @date 
     */
    private String decodeSecretKey(String appId, String secretKey) {
        return SecurityUtils.rsaPrivateDecrypt(secretKey, appCache.get(appId).privateKey);
    }

    /**
     * @param sign
     * @param appId
     * @param time
     * @param nonce
     * @param secretKey
     * @param encodeSecretKey
     * @author 
     * @date 
     */
    private void checkSign(String sign, String appId, Long time, String nonce, String secretKey,
            String encodeSecretKey) {
        String signStr = new StringJoiner(":")
                .add(appId)
                .add(appCache.get(appId).appSecret)
                .add(encodeSecretKey)
                .add(String.valueOf(time))
                .add(nonce)
                .toString();
        String encodeSign = SecurityUtils.aes128Encrypt(signStr, secretKey);
        if (!encodeSign.equals(sign)) {
            throw new RuntimeException("签名错误!");
        }
    }

    /**
     * @param appId
     * @param time
     * @param nonce
     * @author 
     * @date 
     */
    private void checkUnique(String appId, Long time, String nonce) {
        String key = new StringJoiner(":")
                .add(SIGN_UQKEY_PREFIX)
                .add(appId)
                .add(String.valueOf(time))
                .add(nonce)
                .toString();
        if (signCache.getIfPresent(key) != null) {
            throw new RuntimeException("请求重复!");
        }
        try {
            if (!signCache.get(key).equals(buildCacheUniqueValue(key))) {
                throw new RuntimeException("请求重复!");
            }
        } catch (Exception e) {
            throw new RuntimeException("判断请求是否重复异常!", e);
        }
    }

    /**
     * @param key
     * @return java.lang.String
     * @author 
     * @date 
     */
    private String buildCacheUniqueValue(String key) {
        return key + ":" + Thread.currentThread().getName();
    }

    /**
     * 判断访问时间戳是否在当前时间一分钟上下
     *
     * @param timestamp
     * @author 
     * @date
     */
    private void checkAccessFrequently(long timestamp) {
        Date date = new Date(timestamp);
        LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
        LocalDateTime beforeDateTime = LocalDateTime.now().minusMinutes(TIME_RANGE_MINUTES);
        LocalDateTime afterDateTime = LocalDateTime.now().plusMinutes(TIME_RANGE_MINUTES);
        if (localDateTime.isBefore(beforeDateTime) || localDateTime.isAfter(afterDateTime)) {
            throw new RuntimeException("请求时间戳超出范围!");
        }
    }

    @Data
    @AllArgsConstructor
    static class AppData {

        private String appSecret;
        private String privateKey;
    }
}

测试用例:请求端单元测试

import com.example.laboratory.security.util.SecurityUtils;
import java.util.StringJoiner;
import lombok.SneakyThrows;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

/**
 * @author 
 * @data 
 * @description
 */
public class SignControllerTest extends SimpleResultControllerTest {

    private String aesSecretKey;

    /**
     * @author 
     * @date 
     */
    @Before
    public void before() {
        aesSecretKey = RandomStringUtils.randomNumeric(16);
    }

    /**
     * @author 
     * @date 
     */
    @Test
    @SneakyThrows
    public void encode() {
        String content = "{\\"data1\\":\\"测试API接口安全校验\\",\\"data2\\":" + RandomStringUtils.randomNumeric(10) + "}";
        System.out.println(content);
        String result = post("/sign/encode",
                SecurityUtils.aes128Encrypt(content, aesSecretKey),
                String.class);
        Assert.assertEquals(result, content);
    }

    /**
     * @param builder
     * @author 
     * @date 
     */
    protected void replenishHttpServletRequestBuilder(MockHttpServletRequestBuilder builder) {
        String appId = SignController.APP_ID;
        long time = System.currentTimeMillis();
        String nonce = RandomStringUtils.randomNumeric(10);
        String encodeSecret = SecurityUtils.rsaPublicEncrypt(aesSecretKey, SignController.KEY_PUBLIC);
        // 可以将body一起拼接进来
        String signStr = new StringJoiner(":") 
                .add(SignController.APP_ID)
                .add(SignController.APP_SECRET)
                .add(encodeSecret)
                .add(String.valueOf(time))
                .add(nonce)
                .toString();
        String encodeSign = SecurityUtils.aes128Encrypt(signStr, aesSecretKey);
        builder.header("appId", appId)
                .header("time", time)
                .header("nonce", nonce)
                .header("sign", encodeSign)
                .header("secretKey", encodeSecret);
    }

}

web端方案

如果是web端接口交互,可以参考以下阿里、京东的做法:

《京东post登陆参数js分析,密码加密的RSA加密实现》
《淘宝sign加密算法》

参考

《接口非对称加密+Retrofit2》
android采用AES+RSA的加密机制对http请求进行加密》
《支付宝支付加密规则梳理,写的太好了!》
《阿里一面:如何保证API接口数据安全?》

以上是关于API接口安全方案的主要内容,如果未能解决你的问题,请参考以下文章

整套完整安全的API接口解决方案

开放接口/RESTful/Api服务的设计和安全方案

安全优雅的RESTful API签名实现方案(手机端)

网络安全中接口测试的解决方案

网络安全中接口测试的解决方案

网络安全中接口测试的解决方案