Postman脚本实现请求数据RSA加密和签名验证

1. 前言

写本文的初衷来自笔者最近参加的项目,项目的部分接口(比如登录)有数据加解密环节,前端请求时需要将数据进行 RSA 加密后传输到后端,后端解密处理后同样需要加密返回前端。在开发阶段,往往需要在配置文件中关闭 rsa加密, 方便 Postman 进行调试;而在联调阶段,又需要重新打开 rsa加密,否则前端无法登录到页面。这样在联调过程中出现问题,需要手动排查时,往往需要反复启停应用,并打开/关闭 rsa加密。因此,本文的要点就在编写 Postman 脚本,自动完成请求的加密和响应的解密,在能够始终联调的情况下还能够自己调试相关接口

2. 辅助工具

由于笔者的项目是位于公司内网下进行开发和部署,并且 Postman 中,一些库不方便使用,往往需要引入外部的 js 文件,这在内网环境下不太方便

本文需要额外部署一个加密/解密服务端,用来补充 postman 比较羸弱的库,如果你使用的是 Apifox 这种工具,则不需要像笔者这样繁琐,因为 apifox 内置了如 crypto 这类库,可以直接在脚本中完成加解密

本文使用 python 作为加解密服务器,优点是可以直接通过 py 文件启动执行,可以快速编写 web 功能,第三方库功能齐全,迁移方便。

脚本的大致逻辑如下:

graph 复制代码
Postman 请求 ---> python 服务器 ---> 加密数据 ---> 后端服务器
后端响应 ---> Postman 响应 ---> 解密数据 ---> Postman 处理

3. 后端服务器架构

后端服务器需要配置两个关键密钥:服务端私钥、客户端公钥

3.1. 响应加密

响应加密的大致流程如下:

1、拦截 JSON 响应体,将其转化为 JSON 字符串

2、生成长度为 16 的随机字符串,作为密钥 Key

3、使用 Key 对 JSON 进行 AES 加密,将加密结果使用 Base64 编码表示

4、使用客户端公钥 Client Public Key 对 Key 进行加密,生成加密密钥 Encrypt Key

5、使用服务端私钥 Private Key 对原始的 JSON 字符串进行签名,生成签名 Signature

6、将签名 Signature 和加密密钥 Encrypt Key 设置到响应头,加密数据 Encrypt Data 设置到响应体,响应类型仍为 application/json

关键代码如下:

java 复制代码
@RestControllerAdvice
@RequiredArgsConstructor
@SuppressWarnings({"NullableProblems"})
public class EncryptResponseAdvice implements ResponseBodyAdvice<Object> {
    private static final Logger log = LoggerFactory.getLogger(EncryptResponseAdvice.class);

    private final SecretKeyProperties secretKeyProperties;
    private final ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅支持 Controller 种,被 @EncryptResp 标记的方法
        final Method method = returnType.getMethod();
        if (method != null) {
            return method.isAnnotationPresent(EncryptResp.class);
        }
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        final String privateKey = secretKeyProperties.getPrivateKey();
        final String clientPublicKey = secretKeyProperties.getClientPublicKey();
        try {
            final String jsonStr = objectMapper.writeValueAsString(body);
            // 生成私钥签名
            final String sign = RSAUtil.base64Sign(jsonStr.getBytes(), privateKey);
            // 生成随机的对称加密密钥
            final String aes = SecureRandomUtil.randomString(16);
            // 用随机密钥对响应数据进行对称加密
            final String data = AESUtil.encrypt(jsonStr.getBytes(), aes);
            // 使用客户端公钥加密密钥
            final String encryptKey = RSAUtil.encryptBase64(aes.getBytes(), clientPublicKey);

            // 提供给客户端 私钥签名(用于认证数据完整性) 和 公钥加密密钥(用于客户端私钥解密并解密数据)
            response.getHeaders().add("Signature", sign);
            response.getHeaders().add("EncryptKey", encryptKey);
            // 客户端使用服务端的公钥, 验证传输数据的完整性, 并使用自己的私钥解密数据

            return data;
        } catch (Exception e) {
            log.error("failed to serialize body to json", e);
            throw new RuntimeException(e);
        }
    }
}

3.2 请求解密

请求解密的的大致流程如下:

1、拦截 JSON 请求体,该 JSON 请求体的为一串加密的Base64字符串

2、获取请求头中的 Signature 签名以及 Encrypt Key 加密密钥

3、使用服务端私钥 Private Key 解密 Encrypt Key,得到原本的密钥 Key

4、将Base64字符串进行解码,并使用 Key 进行 AES 解密,得到原始数据 data

5、使用客户端公钥 Client Public Key 以及签名 Signature 对原始数据 data 进行验证

6、验证通过后返回解密后的数据 data,请求继续执行

关键代码如下:

typescript 复制代码
@RestControllerAdvice
@SuppressWarnings({"NullableProblems"})
@RequiredArgsConstructor
public class DecryptRequestAdvice implements RequestBodyAdvice {
    private static final Logger log = LoggerFactory.getLogger(DecryptRequestAdvice.class);

    private final SecretKeyProperties secretKeyProperties;

    @Override
    public boolean supports(MethodParameter methodParameter,
                            Type targetType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        final Method method = methodParameter.getMethod();
        if (method != null) {
            return method.isAnnotationPresent(DecryptReq.class);
        }
        return false;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
                                           MethodParameter parameter,
                                           Type targetType,
                                           Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        try {
            String privateKey = secretKeyProperties.getPrivateKey();
            String clientPublicKey = secretKeyProperties.getClientPublicKey();
            return new ReqHttpInputMessage(inputMessage, privateKey, clientPublicKey);
        } catch (Exception e) {
            log.error("Decrypt request failed", e);
            return inputMessage;
        }
    }

    @Override
    public Object afterBodyRead(Object body,
                                HttpInputMessage inputMessage,
                                MethodParameter parameter, Type targetType,
                                Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body,
                                  HttpInputMessage inputMessage,
                                  MethodParameter parameter,
                                  Type targetType,
                                  Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}
java 复制代码
@SuppressWarnings("NullableProblems")
public class ReqHttpInputMessage implements HttpInputMessage {
    private HttpHeaders headers;
    private InputStream inputStream;

    public ReqHttpInputMessage(HttpInputMessage inputMessage, String privateKey, String clientPublicKey) throws Exception {
        this.headers = inputMessage.getHeaders();
        // 客户端私钥的签名
        String signature = this.headers.getFirst("Signature");
        // 服务端公钥加密的密钥
        String encryptKey = this.headers.getFirst("EncryptKey");

        String data = new BufferedReader(new InputStreamReader(inputMessage.getBody()))
                .lines().collect(Collectors.joining(System.lineSeparator()));
        // 使用服务端私钥解密出密钥
        final String aesKey = RSAUtil.decryptBase64(encryptKey, privateKey);
        // 使用密钥解密出原始数据
        final byte[] signData = AESUtil.decrypt(data, aesKey);
        // 使用客户端公钥对原始数据和签名进行验证
        final boolean verify = RSAUtil.verifyBase64(signData, signature, clientPublicKey);

        if (verify) {
            this.inputStream = new ByteArrayInputStream(signData);
        }
    }

    @Override
    public InputStream getBody() throws IOException {
        return this.inputStream;
    }

    @Override
    public HttpHeaders getHeaders() {
        return this.headers;
    }
}

3. 共性点

1、数据流出时,始终使用目的端和公钥进行加密,自己的私钥进行签名

2、数据流入时,始终使用自己的私钥进行解密,目的端的公钥验证签名

3、整个流程中均使用 application/json 媒体类型

4. 工具类

在这里会给出使用的两个工具类 AESUtil 以及 RSAUtil 的代码,前者为对称加密工具,后者为非对称加密

AESUtil 使用的加密算法为 AES/CBC/PKCS5Padding, iv 参数为 加密密钥全字节数组

java 复制代码
public class AESUtil {
    public static Key generateKey() throws NoSuchAlgorithmException {
        final KeyGenerator kg = KeyGenerator.getInstance("AES/CBC/PKCS5Padding");
        final SecureRandom sr = new SecureRandom();
        kg.init(sr);
        return kg.generateKey();
    }

    public static String encrypt(byte[] data, String key) throws Exception {
        final SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        final IvParameterSpec ivp = new IvParameterSpec(key.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivp);
        return Base64Util.encodeToString(cipher.doFinal(data));
    }

    public static byte[] decrypt(String base64Text, String key) throws Exception {
        final SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        final IvParameterSpec ivp = new IvParameterSpec(key.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivp);
        return cipher.doFinal(Base64Util.decodeFromString(base64Text));
    }
}

RSAUtil 使用的算法为 RSA/ECB/PKCS1Padding,签名时使用的算法是 SHA256WithRSA

java 复制代码
public class RSAUtil {
    /**
     * 生成 Pem格式 表示的 RSA 密钥对
     *
     * @return 密钥对
     * @throws Exception 生成密钥对时错误
     */
    public static Map<String, String> generatePemKeyPair() throws Exception {
        final SecureRandom sr = new SecureRandom();
        final KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA");
        rsaGen.initialize(2048, sr);
        final KeyPair keyPair = rsaGen.generateKeyPair();

        final PublicKey pubKey = keyPair.getPublic();
        final PrivateKey priKey = keyPair.getPrivate();
        final BigInteger modulus = ((RSAPublicKey) pubKey).getModulus();

        return Map.of(
                "pemPublicKey", PemUtil.getPemPublicKey(pubKey.getEncoded()),
                "pemPrivateKey", PemUtil.getPemPrivateKey(priKey.getEncoded()),
                "modulus", modulus.toString()
        );
    }

    /**
     * 使用 RSA/ECB/PKCS1Padding 算法加密
     *
     * @param originalData 原始数据
     * @param pemPublicKey pem格式的公钥
     * @return 公钥加密后的数据
     * @throws Exception 加密过程中的异常
     */
    public static byte[] encrypt(byte[] originalData, String pemPublicKey) throws Exception {
        final byte[] derPublicKey = PemUtil.getDerPublicKey(pemPublicKey);
        return encrypt(originalData, derPublicKey);
    }

    /**
     * 使用 RSA/ECB/PKCS1Padding 算法加密
     *
     * @param originalData  原始数据
     * @param derPublickKey der格式的公钥
     * @return 公钥加密后的数据
     * @throws Exception 加密过程中的异常
     */
    public static byte[] encrypt(byte[] originalData, byte[] derPublickKey) throws Exception {
        final PublicKey pbKey = getPublicKey(derPublickKey);
        final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, pbKey);

        return cipher.doFinal(originalData);
    }

    /**
     * 使用 RSA/ECB/PKCS1Padding 算法加密, 并转化为 Base64 字符串
     *
     * @param originalData 原始数据
     * @param pemPublicKey pem格式的公钥
     * @return 公钥加密后的数据
     * @throws Exception 加密过程中的异常
     */
    public static String encryptBase64(byte[] originalData, String pemPublicKey) throws Exception {
        byte[] bytes = encrypt(originalData, pemPublicKey);
        return Base64Util.encodeToString(bytes);
    }

    /**
     * 使用 RSA/ECB/PKCS1Padding 算法加密, 并转化为 Base64 字符串
     *
     * @param originalData 原始数据
     * @param derPublicKey der格式的公钥
     * @return 公钥加密后的数据
     * @throws Exception 加密过程中的异常
     */
    public static String encryptBase64(byte[] originalData, byte[] derPublicKey) throws Exception {
        byte[] bytes = encrypt(originalData, derPublicKey);
        return Base64Util.encodeToString(bytes);
    }

    /**
     * 生成公钥规范
     *
     * @param key 指定公钥二进制数据
     * @return 公钥规范
     * @throws Exception 创建公钥规范时的异常
     */
    private static PublicKey getPublicKey(byte[] key) throws Exception {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key);
        final KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(keySpec);
    }

    /**
     * 使用 RSA/ ECB/ PKCS1Padding 算法解密
     *
     * @param encryptData   加密后的数据
     * @param pemPrivateKey pem格式的私钥
     * @return 解密后的原始数据
     * @throws Exception 解密过程中的异常
     */
    public static String decrypt(byte[] encryptData, String pemPrivateKey) throws Exception {
        final byte[] derPrivateKey = PemUtil.getDerPrivateKey(pemPrivateKey);
        return decrypt(encryptData, derPrivateKey);
    }

    /**
     * 使用 RSA/ ECB/ PKCS1Padding 算法解密
     *
     * @param encryptData   加密后的数据
     * @param derPrivateKey der格式的私钥
     * @return 解密后的原始数据
     * @throws Exception 解密过程中的异常
     */
    public static String decrypt(byte[] encryptData, byte[] derPrivateKey) throws Exception {
        final PrivateKey privKey = getPrivateKey(derPrivateKey);
        final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privKey);

        final byte[] parsedText = cipher.doFinal(encryptData);
        return new String(parsedText);
    }

    /**
     * 使用 RSA/ ECB/ PKCS1Padding 算法解密
     *
     * @param base64Cipher  使用Base64表示的加密后的数据字符串
     * @param pemPrivateKey pem格式的私钥
     * @return 解密后的原始数据
     * @throws Exception 解密过程中的异常
     */
    public static String decryptBase64(String base64Cipher, String pemPrivateKey) throws Exception {
        byte[] decodedBytes = Base64Util.decodeFromString(base64Cipher);
        return decrypt(decodedBytes, pemPrivateKey);
    }

    /**
     * 使用 RSA/ ECB/ PKCS1Padding 算法解密
     *
     * @param base64Cipher  使用Base64表示的加密后的数据字符串
     * @param derPrivateKey der格式的私钥
     * @return 解密后的原始数据
     * @throws Exception 解密过程中的异常
     */
    public static String decryptBase64(String base64Cipher, byte[] derPrivateKey) throws Exception {
        byte[] decodedBytes = Base64Util.decodeFromString(base64Cipher);
        return decrypt(decodedBytes, derPrivateKey);
    }

    /**
     * 生成私钥规范
     *
     * @param key 指定私钥二进制数据
     * @return 私钥规范
     * @throws Exception 创建私钥规范时的异常
     */
    public static PrivateKey getPrivateKey(byte[] key) throws Exception {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);
        final KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(keySpec);
    }

    /**
     * 生成 Base64 签名字符串
     *
     * @param data          被签名的原始数据
     * @param derPrivateKey der格式的私钥
     * @return 使用私钥对原始数据进行签名后的Base64字符串
     * @throws Exception 签名过程中的异常
     */
    public static String base64Sign(byte[] data, byte[] derPrivateKey) throws Exception {
        final PrivateKey privKey = getPrivateKey(derPrivateKey);
        final Signature signature = Signature.getInstance("SHA256WithRSA");
        signature.initSign(privKey);
        signature.update(data);
        return Base64Util.encodeToString(signature.sign());
    }

    /**
     * 生成 Base64 签名字符串
     *
     * @param data          被签名的原始数据
     * @param pemPrivateKey pem格式的私钥
     * @return 使用私钥对原始数据进行签名后的Base64字符串
     * @throws Exception 签名过程中的异常
     */
    public static String base64Sign(byte[] data, String pemPrivateKey) throws Exception {
        final byte[] derPrivateKey = PemUtil.getDerPrivateKey(pemPrivateKey);
        return base64Sign(data, derPrivateKey);
    }

    /**
     * 验证数据签名
     *
     * @param data         待验证的原始数据
     * @param base64Sign   使用Base64表示的签名字符串
     * @param derPublicKey der格式的公钥
     * @return 签名验证一致返回true, 否则返回false
     * @throws Exception 验证过程中的异常
     */
    public static boolean verifyBase64(byte[] data, String base64Sign, byte[] derPublicKey) throws Exception {
        PublicKey pubKey = getPublicKey(derPublicKey);
        final Signature signature = Signature.getInstance("SHA256WithRSA");
        signature.initVerify(pubKey);
        signature.update(data);
        return signature.verify(Base64Util.decodeFromString(base64Sign));
    }

    /**
     * 验证数据签名
     *
     * @param data         待验证的原始数据
     * @param base64Sign   使用Base64表示的签名字符串
     * @param pemPublicKey pem格式的公钥
     * @return 签名验证一致返回true, 否则返回false
     * @throws Exception 验证过程中的异常
     */
    public static boolean verifyBase64(byte[] data, String base64Sign, String pemPublicKey) throws Exception {
        final byte[] derPublicKey = PemUtil.getDerPublicKey(pemPublicKey);
        return verifyBase64(data, base64Sign, derPublicKey);
    }
}

此外,还有一个 PemUtil,方便接收 RSA/ECB/PKCS1Padding pem 格式的密钥,如果直接使用 der 格式的字符,可以不使用该工具类,直接进行 base64 解码后使用即可

arduino 复制代码
public class PemUtil {
    private static final String START_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----";
    private static final String END_PRIVATE_KEY = "-----END RSA PRIVATE KEY-----";
    private static final String START_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----";
    private static final String END_PUBLIC_KEY = "-----END PUBLIC KEY-----";

    public static byte[] getDerPrivateKey(String pemPrivateKey) {
        return Base64Util.decodeFromString(
                pemPrivateKey.replace(START_PRIVATE_KEY, "")
                        .replace(END_PRIVATE_KEY, "")
                        .replaceAll("\s", "")
        );
    }

    public static byte[] getDerPublicKey(String pemPublicKey) {
        return Base64Util.decodeFromString(
                pemPublicKey.replace(START_PUBLIC_KEY, "")
                        .replace(END_PUBLIC_KEY, "")
                        .replaceAll("\s", "")
        );
    }

    public static String getPemPrivateKey(byte[] derPrivateKey) {
        return String.join(
                System.lineSeparator(),
                START_PRIVATE_KEY,
                Base64Util.encodeToString(derPrivateKey),
                END_PRIVATE_KEY
        );
    }

    public static String getPemPublicKey(byte[] derPublicKey) {
        return String.join(
                System.lineSeparator(),
                START_PUBLIC_KEY,
                Base64Util.encodeToString(derPublicKey),
                END_PUBLIC_KEY
        );
    }
}

4. Postman 请求和响应脚本

在后端服务器设计完成后,此时可以着手编写 Postman 的脚本

假设后端拥有一个登录接口,需要进行请求加密和解密

http 复制代码
POST {server}/login
Content-Type: application/json

{
    "username": "admin",
    "password": "123456",
    "uuid": "ybas2-a9hdi-asd9h1-asd98",
    "verify-code": "YZ1P"
}

那么首先在 Postman 中创建一个 Environment,记为测试环境,并勾选生效

添加 (客户端)私钥 privateKey 和 服务端公钥 serverPublicKey 变量,并输入相应的值

这里给出客户端和服务端的一组密钥对示例

服务端密钥对:

pem 复制代码
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7jkLaYA6Do9AGACVdknCW+fvHUSe8OmxD+qX9O/ayiSRnB
1MicnPIKvotACgetWcabW/Kqn5WmIzMABGyfkCWnc1/aiocQBBVjdXA9ps8Q6xSGoYbeiKN6KR3RxFHGaMwEl/D9j3o
Zdhp/nbu1HOZyu695B/mVhAFOqjybITzWBrcLbPZEgaGDuBQar2hGi0HLgYRZp5X9CQjPqrvrdYm9z/T2nmIqFss9lM
Ox2kundiZ8/ZjKHchTGYJ329WZNusD1FSAEm9XErBEYeMJpr+PC/+DvpOOTOfwGJH6RkzYJ6/PKyuy5PwyGtCJlcye5
R4rMaJv2EnZ7vQU9AYQljQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCjuOQtpgDoOj0AYAJV2ScJb5+8dRJ7w6bEP6pf079
rKJJGcHUyJyc8gq+i0AKB61Zxptb8qqflaYjMwAEbJ+QJadzX9qKhxAEFWN1cD2mzxDrFIahht6Io3opHdHEUcZozAS
X8P2Pehl2Gn+du7Uc5nK7r3kH+ZWEAU6qPJshPNYGtwts9kSBoYO4FBqvaEaLQcuBhFmnlf0JCM+qu+t1ib3P9PaeYi
oWyz2Uw7HaS6d2Jnz9mModyFMZgnfb1Zk26wPUVIASb1cSsERh4wmmv48L/4O+k45M5/AYkfpGTNgnr88rK7Lk/DIa0
ImVzJ7lHisxom/YSdnu9BT0BhCWNAgMBAAECggEALJYD8diE47lEdo8u46EtC9lNt31saK8KAeTSqaEZPn4Ag9VJXTy
jZ0uxuBWMsUPdOTs1zf5NleDNI/Ff+7y70cJ2JYwhkws1OfcO5Wy/HPkiBiBZ4i6npxifsMDpsKcVVrGH3i+HK4kM/E
YcuS0+GdbtHgfbkgOazIN8bdqib0tT/IEx32EG3BfxCKhkhFaOTfDdRyqImmd5h92n9e5/9YCHXBDB/kHk5WFNZx3vy
SpxWZlC/nvQrXtV3haIZrsjPTOs7lIByyXUgVM/Fq9LWK1iYp193N7aNWGGZkcGyNtIXVjhAi1RsK6ZRqLwXtEddY2D
OthIrkcvX5j87w1fcwKBgQDQe7CkqlZt8xVRae77LxBSXd70v2impLcVMSKQqJOa4khBfP32bSeZd2eIS09Ayax1BPH
BnrU24rJml1hDn/tBDX6ysnlnj8qbaNG73qzxp9+lLkSmeO+DeZQE8Kl7dDf4CCwwS7hMqUSM28AbhrVDJdeETk21qM
X58ZJ2MLAebwKBgQDJCYwmsDgtJUvEh7yY/1wSfzoA5yQb7U6xT6WYxT2DjKk5rb0BX0/49Uw9bKB9Mej7EyyN1zoyr
1s2LRWrr0Yku8pOzp36FeVe0evPhmQ+iOmv7m3IodduJs9iGr8IMekwe60tXm6KLRxW3G7W5b+w/vejz8D8o0RnOAhR
H+35wwKBgQCE13ubwMnnry9TO/vB46Azy4qISvqEzIm4ICHVKQU8eJjv2ZP9FFpaKEI5DzuFnbucqLTe2aDAQzzHsAH
WvTacodusQ5qmCXJhCi4x1lY+eOhBWTT3GjILhUlyyGJFvqz3B0YY0/awKl76nf9Pysru6UrlC/vqF4tmkq3vT2C5Uw
KBgQCVwdr5ZwQx/Tp36HWBs5gu3z+iNI0dkKWySBafuy6btEjLgrTtNMcqOfDVQPo8yNU5U4s5Dj94SlC0BtnBzwt9i
banBhuAlJcND2uOaBp8yxjpyb9WWdlVYOvTtQDhZezEBR14UoQdwoT9369hvjwR6Z0oFjc4+5aVHZR0ekiYaQKBgQCQ
Va/5aEcdS2gcTjSUqTum5DG6R/fny/8UMOYDHHsmcgqKDw8c/xVY7xU//QI3Brq97wY/2eN4xeAJFyfX7c+ak4vSDD7
CIh9D9/cxJzOscag43O4UvD1oJSKldaSrmlAopRU+MXJk9WRCu3m1eT2Fgom0P8vgd9/rYqfhiZtXnw==
-----END RSA PRIVATE KEY-----

客户端密钥对:

pem 复制代码
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmLqUjt/sQrXDJIwZ7SYCzLEYnz8x1LPikA3cdxGXowlSHfQ
x8ZpvnmyrHR1dgJN77JRoAtlVCqWJoeU6MRnVxcEpu6sCKTJv/ozVQBamkscaXRYC9AooszKDXPfZjkOcN2pKfdV4zv
1K6k4ZWlPz301M5uiVdt3QYRNz2YdTBLauTbliYV+gDwioC6WHOVL9CPH5uUGQogIJMZ1at6TutVEMX/9zJjzmxmUzl
g26YZd8Xftm8Sp0dNdkUk9LJSUTfQx/lhaRLmQfUBEqvn1pRF9OOj/MgVJlvIwlSauw3EQvRAkGT+4fLT/924uFp69m
U6C6BT+AHS/WfwihLXoldQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCYupSO3+xCtcMkjBntJgLMsRifPzHUs+KQDdx3EZe
jCVId9DHxmm+ebKsdHV2Ak3vslGgC2VUKpYmh5ToxGdXFwSm7qwIpMm/+jNVAFqaSxxpdFgL0CiizMoNc99mOQ5w3ak
p91XjO/UrqThlaU/PfTUzm6JV23dBhE3PZh1MEtq5NuWJhX6APCKgLpYc5Uv0I8fm5QZCiAgkxnVq3pO61UQxf/3MmP
ObGZTOWDbphl3xd+2bxKnR012RST0slJRN9DH+WFpEuZB9QESq+fWlEX046P8yBUmW8jCVJq7DcRC9ECQZP7h8tP/3b
i4Wnr2ZToLoFP4AdL9Z/CKEteiV1AgMBAAECggEAFEZ2uNXswzlmh9hKi9NHPV3IT0HAL4TW77lLWoo9F3GCE4RqyfB
u33j9KV8P3eUWZ0SXX0NzWPe7YwDvnfuYR8m6LNYJsibmO8R9/eVT5Tnl29QzfarIlut+iW3E1bBPkYK8k2JuyInTM3
I9RuLMxgj4y9G2H+LYCj8fGtJueLpEE2xKuvwQopVHCGtYctLCQ2+JTQdvQ1nqtr3owtzP+2CdGCYma7RntHgfN/rZQ
d4sawp/Tt3rNrzck/xqJEpLB5H++rjnT/sMH/9/dEU1IJg//KuTM9RBgvwA9hnFVTaOdFW+CMtumpSgvUwcFDp72hOb
OLLehcHsInwWL9PV+QKBgQC2dDFpoyua9HtLB3AcCQLXxXnqjz8luKjEYO1M9APG6JoBlltWJ9bPRRTGptoZC1nfmFh
UOb1afqNlyZfjyLfyYz+k/ISnSO/YHTF2SIOArZhvozo6+mAHgBpkH6zECa7m37eJGSsqn68PP4wm0b5nMmQyd85wxz
D9CbKDG+2cLQKBgQDWSv7ktEOF6Ya7aV+cIZ3nikFf0mnOd9LsGIf+B2CLQYyeys+UFgoCvDR1nJGJ6J1WHhyG5UNFr
o1n5m0WrFZvA2wTUQFhOSJT8/BPH14yidVswafr6/3ThbBOHmcxSzJ6WbR78LIRko3a61SM7S9Sk2Wrum3PMbtDwcKG
2fzTaQKBgA5y6S7acyuUTOdGMYSm2gjIZL1EltWf6A2VN3WupVXtObUCeT07bnF/oQOSVxdApN9mKyiQYgR2nu4Cpvq
s/JQ/c9zW+pJc3lGZDj+1wmRAWyAMJyJgjZZKeMyRDZeAxM8XzGsZCSgY+T4V5D12wsNdZD3y6vBXdfOz/uUPIyQJAo
GBAMJlbi2Syd5lJnBE+xLr/A7bgMqoWouOb6z15AgyMQajBCnY2c4A4Dvy97PpwK1Wc8R3tHE68Xf5DRZAFp4G4LH8b
MJpLdNAvT9jf5CHaDB4kUADvY1rm+uSz4lOU1aIXNZZIj188Ey4oi8CZMUjNVa3l/fdO1hGSRcCYrFqdsxhAoGBAJ+Y
s9EFJ7Ia9D3MvTQrMI0Cvt5VbdZJ3JMTzgto7eaKqyqhHz5gw0FPKg3WO/9xBXJ95lo3MJxUjHapmen/r/L+DF/+ODK
oRLdQc2AXhNlTPykEAgZGit2dm4UtTAjHtQq0jXsACYkv7Wy9sVGvMWiqU9yAPeL1BmnKJPbfgEfm
-----END RSA PRIVATE KEY-----

将客户端的私钥和服务端的公钥填充到对应的变量上

接着返回到 Postman 页面,点击集合,创建一个新的集合,并添加一个 POST 的登录请求

登录请求中的 {{server}} 变量个人喜欢在集合的 Scripts 的 pre-request 中确定,方便修改不同的环境;请求体中的 {{uuid}} 占位符,为登录请求时获取到的验证码的 uuid,一般在获取验证码接口的 tests 中编写对应的脚本设置到 environment 中

4.1 请求加密脚本

点开登录请求的 Script 的 Pre-Request,编写对应的脚本

脚本内容如下:

js 复制代码
// 获取环境变量中的 encrypt 变量, 确定是否启用加密
const enc = pm.environment.get('encrypt')
if (Boolean(enc)) {
    // 获取私钥和服务端公钥
    const private_key = pm.environment.get('privateKey')
    const public_key = pm.environment.get('serverPublicKey')
    // 获取解析占位符后的请求体数据
    const body = pm.request.body
    const raw_body = body.raw
    const parsed_body = pm.variables.replaceIn(raw_body)
    // 将 data 转为 base64 编码的字符串
    const body_obj = JSON.parse(parsed_body)
    const utf8_data = CryptoJS.enc.Utf8.parse(JSON.stringify(body_obj))
    const base64_data = CryptoJS.enc.Base64.stringfy(utf8_data)
    // 获取加密的服务器地址, 一般为本地部署, 可以直接写 url
    const server = pm.environment.get('plugServer')
    const url = `${server}/encrypt`
    const encRequest = {
        url: url,
        method: 'POST',
        body: {
            mode: 'raw',
            raw: JSON.stringify({
                private_key: private_key,
                server_public_key: public_key,
                base64_data: base64_data,
            })
        },
        header: 'Content-Type: application/json; charset=UTF-8',
    }

    pm.sendRequest(encRequest, (err, res) => {
        // 发送加密请求, 获取响应中对应的字段
        const json = res.json()
        const encrypt_data = json['encrypt_data']
        const encrypt_key = json['encrypt_key']
        const signature = json['signature']

        // 修改原始请求的请求体, 添加新的请求头
        pm.request.body = encrypt_data
        pm.request.headers.upsert({
            key: 'Content-Type',
            value: 'application/json; charset=UTF-8',
        })
        pm.request.headers.upsert({
            key: 'Encrypt-Key',
            value: encrypt_key,
        })
        pm.request.headers.upsert({
            key: 'Signature',
            value: signature,
        })
    })
}

上面的脚本将Body页面中携带变量的 JSON 体获取到

json 复制代码
{
    "username": "admin",
    "password": "123456",
    "uuid": "{{uuid}}",
    "verify-code": "YZRP"
}

并通过 pm.variables.replaceIn 将变量解析为实际的值,随后构建请求,发向本地的加解密服务器

http-request 复制代码
POST {{plugServer}}/encrypt
Content-Type: application/json

{
    // 客户端私钥
    "private_key": {{privateKey}},
    // 服务端公钥
    "server_public_key": {{serverPublicKey}},
    // 通过 base64 编码的原 JSON 数据
    "base64_data": {{base64_data}},
}

最后接收加密成功的响应体,结构如下:

json 复制代码
{
    // 加密后的数据,以base64编码表示,可以直接使用
    "encrypt_data": ...,
    // 加密密钥, 需要设置到请求头中
    "encrypt_key": ...,
    // 数据签名, 需要设置到请求头中
    "signature": ...,
}

最后脚本中通过获取此次请求的响应体,并修改原始请求体就能够达到完成自动加密功能

4.2 响应解密脚本

点开登录请求的 Script 的 Post-Response(如果为老版 Postman,那么这里为 Tests),编写对应的脚本

脚本的内容如下:

js 复制代码
// 获取环境变量中的 encrypt 变量, 确定是否启用加密
const enc = pm.environment.get('encrypt')
// 响应体 json,如果启用了加密,那么该 json 为一串base64字符串, 否则为正常json字符串
const json = pm.reponse.json()
if (Boolean(enc)) {
    // 获取私钥和服务端公钥
    const private_key = pm.environment.get('privateKey')
    const public_key = pm.environment.get('serverPublicKey')
    const server = pm.environment.get('plugServer')
    // 获取响应体和响应头
    const encrypt_resp = json
    const signature = pm.reponse.headers.get('Signature')
    const encrypt_key = pm.response.headers.get('Encrypt-Key')
    // 拼接解密请求
    const url = `${server}/decrypt`
    const decRequest = {
        url: url,
        method: 'POST',
        body: {
            mode: 'raw',
            raw: JSON.stringify({
                private_key: private_key,
                server_public_key: public_key,
                base64_encrypt: encrypt_resp,
                signature: signature,
                encrypt_key: encrypt_resp
            })
        },
        header: 'Content-Type: application/json'
    }
    // 发送请求
    pm.sendRequest(decRequest, (err, res) => {
        const data_json = res.json()
        // 解码 base64 响应
        const base64_data = data_json['base64_data']
        const json_str = CryptoJS.enc.Base64.parse(base64_data).toString(CryptoJS.enc.Utf8)
        const json_obj = JSON.parse(json_str)
        // 设置 token
        const token = json_obj['token']
        pm.environment.set('token', token)
        // 用于在 Visualizer 中可视化显示解码后的 Json 结构
        pm.visualizer.set(`<pre>${JSON.stringify(json_obj, null, 4)}</pre>`)
    })
} else {
    const token = json['token']
    pm.environment.set('token', token)
}

上面的脚本在获取到响应体的 json 后,如果已经启用了加密,那么直接将该响应体(base64加密的字符串)发送到加解密服务器进行解密

http 复制代码
POST {{plugServer}}/decrypt
Content-Type: application/json

{
    // 客户端私钥
    "private_key": {{privateKey}},
    // 服务端公钥
    "server_public_key": {{serverPublicKey}},
    // base64编码的加密响应数据
    "encrypt_data": {{encrypt_data}},
    // 响应头中的加密密钥
    "encrypt_key": {{encrypt-key}},
    // 响应头中的签名
    "signature": {{signature}}
}

从服务器返回一个 base64 编码的原始响应数据

json 复制代码
{
    "decrypt_data": ...
}

通过对该数据进行 base64 解码后就可以获取到原始的JSON字符串响应

5. Python 加解密服务器

由于 Postman 没有内置相关的库,所以加解密需要使用外部服务器提供相应的接口,这里使用了python这种轻量级的服务器来完成工作。如果使用的是 apifox 这种工具,则可以直接使用 crypto 等库不需要经过外部服务器完成数据的加解密

这一章节会给出python相关的代码

5.1 加解密服务器架构

python构建服务器很简单,只需要编写一个 main.py 即可,在其中定义两个接口 /encryptdecrypt

ini 复制代码
import base64

from flask import Flask, request, jsonify

from util.aesutil import AESUtil
from util.randomutil import RandomUtil
from util.rsautil import RSAUtil

app = Flask(__name__)

# 数据加密接口
@app.route('/encrypt', methods=['POST'])
def encrypt():
    # 1. 解析请求体
    data = request.json
    private_key = (data.get('private_key') if 'private_key' in data else '').strip()
    server_public_key = (data.get('server_public_key') if 'server_public_key' in data else '').strip()
    base64_data = (data.get('base64_data') if 'base64_data' in data else '').strip()
    # 2. 生成工具
    data_bytes = base64.b64decode(base64_data)
    pem_pk_s = RSAUtil.transform_base64_pem_public_key(server_public_key)
    pem_prk = RSAUtil.transform_base64_pem_private_key(private_key)
    # 3. 生成密钥, 加密数据, 加密密钥, 签名
    key = RandomUtil.random_string(16).encode('utf-8')
    encrypt_data = AESUtil.encrypt_base64(data_bytes, key)
    encrypt_key = RSAUtil.encrypt_base64(key, pem_pk_s)
    signature = RSAUtil.sign(data_bytes, pem_prk)
    # 4. 返回数据
    return jsonify({
        'encrypt_data': encrypt_data,
        'encrypt_key': encrypt_key,
        'signature': signature
    })


# 数据解密接口
@app.route('/decrypt', methods=['POST'])
def decrypt():
    # 1. 解析请求体
    data = request.json
    encrypt_key = (data.get('encrypt_key') if 'encrypt_key' in data else '').strip()
    signature = (data.get('signature') if 'signature' in data else '').strip()
    private_key = (data.get('private_key') if 'private_key' in data else '').strip()
    server_public_key = (data.get('server_public_key') if 'server_public_key' in data else '').strip()
    encrypt_data = (data.get('encrypt_data') if 'encrypt_data' in data else '').strip()
    # 2. 生成工具
    data_bytes = base64.b64decode(encrypt_data)
    pem_pk_s = RSAUtil.transform_base64_pem_public_key(server_public_key)
    pem_prk = RSAUtil.transform_base64_pem_private_key(private_key)
    # 3. 解析密钥, 解析数据, 验证签名
    decrypt_key = RSAUtil.decrypt_base64(encrypt_key, pem_pk_s)
    decrypt_data = AESUtil.decrypt_base64(encrypt_data, decrypt_key)
    if RSAUtil.verify(decrypt_data, signature, pem_pk_s):
        return jsonify({
            'decrypt_data': base64.b64encode(decrypt_data).decode('utf-8'),
        })
    else:
        return jsonify({
            'error': 'Invalid signature'
        })


if __name__ == '__main__':
    app.run(host='localhost', port=19876)

数据的加解密逻辑和后端服务器的加解密逻辑一致,这里不再重复

5.2 工具类

python 需要同样实现 aesutil 和 rsautil,并且保证和 Java 中使用的加密算法、参数等一致,这里给出相关代码,能够保证和后端服务器使用算法一致。

首先是 aesutil

python 复制代码
import base64

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


class AESUtil:
    @staticmethod
    def encrypt(data: bytes, key: bytes) -> bytes:
        """
        使用密钥加密数据
        :param data: 数据
        :param key: 密钥
        :return: 加密后的数据
        """
        cipher = AES.new(key, AES.MODE_CBC, iv=key)
        encrypt_data = cipher.encrypt(pad(data, AES.block_size, style='pkcs7'))
        return encrypt_data

    @staticmethod
    def encrypt_base64(data: bytes, key: bytes) -> str:
        encrypted = AESUtil.encrypt(data, key)
        return base64.b64encode(encrypted).decode('utf-8')

    @staticmethod
    def decrypt(data: bytes, key: bytes) -> bytes:
        """
        使用密钥解密数据
        :param data: 加密数据
        :param key: 密钥
        :return: 原始数据
        """
        cipher = AES.new(key, AES.MODE_CBC, iv=key)
        decrypt_data = cipher.decrypt(unpad(cipher.decrypt(data), AES.block_size, style='pkcs7'))
        return decrypt_data

    @staticmethod
    def decrypt_base64(data: str, key: bytes) -> bytes:
        """
        使用密钥解密数据
        :param data: 加密后的 base64 数据
        :param key: 密钥
        :return: 原始数据
        """
        decoded = base64.b64decode(data)
        return AESUtil.decrypt(decoded, key)

然后是 rsautil

python 复制代码
import base64

from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Hash import SHA256


class RSAUtil:
    @staticmethod
    def generate_key_pair():
        """
        生成 RSA 密钥对
        :return: 公钥,私钥,模数
        """
        key = RSA.generate(2048)
        private_key = key.exportKey()
        public_key = key.publickey().exportKey()
        modulus = key.n
        return public_key, private_key, modulus

    @staticmethod
    def transform_pem_public_key(public_key: bytes) -> str:
        """
        将二进制密钥转化为 PEM 格式公钥
        :param public_key: 公钥的二进制数据
        :return: PEM格式的公钥字符串
        """
        key = public_key.decode('utf-8').strip()
        return f"""
        -----BEGIN PUBLIC KEY-----
        {key}
        -----END PUBLIC KEY-----
        """.strip()

    @staticmethod
    def transform_base64_pem_public_key(public_key: str) -> str:
        """
        将 base64 编码的密钥二进制数据转化为 PEM 格式的公钥
        :param public_key: base64 编码的公钥数据
        :return: PEM格式的公钥字符串
        """
        return f"""
        -----BEGIN PUBLIC KEY-----
        {public_key.strip()}
        -----END PUBLIC KEY-----
        """.strip()

    @staticmethod
    def transform_pem_private_key(private_key: bytes) -> str:
        """
        将二进制密钥转化为 PEM 格式私钥
        :param private_key: 私钥的二进制数据
        :return: PEM格式的私钥字符串
        """
        encoded = base64.b64encode(private_key).decode('utf-8')
        return RSAUtil.transform_base64_pem_private_key(encoded)

    @staticmethod
    def transform_base64_pem_private_key(private_key: str) -> str:
        """
        将base64编码的二进制密钥转化为 PEM 格式密钥
        :param private_key: base64编码的私钥数据
        :return: PEM格式私钥
        """
        return f"""
        -----BEGIN RSA PRIVATE KEY-----
        {private_key.strip()}
        -----END RSA PRIVATE KEY-----
        """.strip()

    @staticmethod
    def encrypt(original_data: bytes, pem_pk: str) -> bytes:
        """
        使用公钥加密数据
        :param original_data: 原始数据
        :param pem_pk: 公钥字符串
        :return: 加密后的数据
        """
        public_obj = RSA.importKey(pem_pk)
        cipher = PKCS1_v1_5.new(public_obj)
        return cipher.encrypt(original_data)

    @staticmethod
    def encrypt_base64(original_data: bytes, pem_pk: str) -> str:
        """
        使用公钥加密数据
        :param original_data: 原始数据
        :param pem_pk: 公钥字符串
        :return: 加密后的数据, 以 base64 格式表示
        """
        encrypted = RSAUtil.encrypt(original_data, pem_pk)
        return base64.b64encode(encrypted).decode('utf-8')

    @staticmethod
    def decrypt(cipher_data: bytes, pem_prk: str) -> bytes:
        """
        使用私钥解密数据
        :param cipher_data: 加密数据
        :param pem_prk: 私钥字符串
        :return: 解密后的数据
        """
        private_obj = RSA.importKey(pem_prk)
        cipher = PKCS1_v1_5.new(private_obj)
        return cipher.decrypt(cipher_data, None)

    @staticmethod
    def decrypt_base64(cipher_data: str, pem_prk: str) -> bytes:
        """
        使用私钥解密数据
        :param cipher_data: 加密后以base64格式编码的数据
        :param pem_prk: 私钥字符串
        :return: 解密后的数据
        """
        decoded_data = base64.b64decode(cipher_data)
        return RSAUtil.decrypt(decoded_data, pem_prk)

    @staticmethod
    def sign(data: bytes, pem_prk) -> str:
        """
        使用私钥生成数据签名
        :param data: 待签名数据
        :param pem_prk: PEM格式的私钥字符串
        :return: 数据签名, 以base64格式表示
        """
        private_obj = RSA.importKey(pem_prk)
        h = SHA256.new(data)
        signature = pkcs1_15.new(private_obj).sign(h)
        return base64.b64encode(signature).decode('utf-8')

    @staticmethod
    def verify(data: bytes, signature: str, pem_pk: str) -> bool:
        """
        使用公钥验证数据签名
        :param data: 待验证数据
        :param signature: 数据签名, 以base64格式表示
        :param pem_pk: PEM格式的公钥字符串
        :return: 签名是否正确没有遭受篡改
        """
        public_obj = RSA.importKey(pem_pk)
        h = SHA256.new(data)
        sign_data = base64.b64decode(signature)
        try:
            pkcs1_15.new(public_obj).verify(h, sign_data)
            return True
        except (ValueError, TypeError):
            return False

最后还有一个随机生成密钥的 randomutil,由于代码很少,可以考虑直接内联到 main.py 里面

python 复制代码
import random
import string

class RandomUtil(object):
    @staticmethod
    def random_string(length: int) -> str:
        characters = string.ascii_letters + string.digits
        return ''.join(random.choice(characters) for _ in range(length))
相关推荐
m0_748235955 分钟前
SpringBoot:解决前后端请求跨域问题(详细教程)
java·spring boot·后端
坚定信念,勇往无前1 小时前
springboot单机支持1w并发,需要做哪些优化
java·spring boot·后端
后端小肥肠2 小时前
【AI编程】Java程序员如何用Cursor 3小时搞定CAS单点登录前端集成
前端·后端·cursor
老友@2 小时前
OnlyOffice:前端编辑器与后端API实现高效办公
前端·后端·websocket·编辑器·onlyoffice
风月歌3 小时前
基于springboot校园健康系统的设计与实现(源码+文档)
java·spring boot·后端·mysql·毕业设计·mybatis·源码
m0_748239473 小时前
Spring Boot框架知识总结(超详细)
java·spring boot·后端
m0_748236113 小时前
Spring Boot 实战:轻松实现文件上传与下载功能
linux·spring boot·后端
m0_748245924 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
青灯文案15 小时前
如何在 SpringBoot 项目使用 Redis 的 Pipeline 功能
spring boot·redis·后端
m0_748249545 小时前
SpringBoot教程(三十二) SpringBoot集成Skywalking链路跟踪
spring boot·后端·skywalking