教你如何使用AES对接口参数进行加密

1,233 阅读6分钟

教你如何使用AES对接口参数进行加密

原文链接:教你如何使用 AES 加密接口参数

前言

我们作为程序猿,在浏览网站的时候偶尔也会打开控制台看看请求的接口,我们会发现有些接口的传输是 "乱码" ,那么这个乱码究竟是什么呢?为什么要这么做?

其实这个所谓的 "乱码" 其实是一种加密后的密文,其原理是前后端提前约定好一种协议,在该协议下进行加解密的处理,例如:前端将数据加密后发送给后端,后端接收到参数后,第一时间先在约定好的协议下将密文解密成可识别的对象,再进行逻辑处理,最后将结果加密返回给前端,前端获取到密文后,同样依照约定好的协议对密文进行解密,最后将解密出来的数据拿来使用。

那么我们想实现同样的效果,应该如何做呢?别急,听哥们给你一一道来。

介绍

一般来说加密算法会分为两种:对称加密算法非对称加密算法

对称加密算法

摘自百度百科: 采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。

非对称加密算法

摘自百度百科: 不对称加密算法使用两把完全不同但又是完全匹配的一对钥匙—公钥和私钥。在使用不对称加密算法加密文件时,只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程。

经过百度百科中的简单概要,我们已经知道了对称加密算法非对称加密算法 都是什么,但它们中间又有什么不同呢?早就猜到你会这么问了,所以我已经把它们的区别一一列出来了。

区别

密钥

对称加密: 一共只有一种密钥,并将该密钥同时用来加解密。

非对称加密: 共有两种密钥:公钥私钥 ,使用公钥来加密,使用私钥来解密。

速度

对称加密: 算法简单且加解密容易,所以执行效率高、速度快。

非对称加密: 由于加密算法比较复杂,所以加解密的效率很低,速度远不如 对称加密

安全性

对称加密: 由于加解密均使用的为同一个密钥,那么若密钥泄露则有被破解密文的风险。

非对称加密: 由于使用了两种密钥,且公钥是可公开的密钥,使用私钥来进行解密,消除了用户交换密钥的条件,极大程度上保证了数据安全。

实现

在这里给大家介绍一下 AES + CBC + PKCS5Padding 的加密方式,具体实现如下:

引入依赖

<dependency>
            <groupId>org.apache.directory.studio</groupId>
            <artifactId>org.apache.commons.codec</artifactId>
            <version>1.8</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.13.0</version>
        </dependency>

编写密钥荷载

注意:这里的 AES_KEYAES_IV 可以自定义,但 必须是16位的

/**
 * @author Bummon
 * @description 荷载
 * @date 2023-08-12 10:27
 */
public class Common {

    /**
     * AES密钥
     */
    public static final byte[] AES_KEY = "Ct9x5IUNHlhq0siZ".getBytes();

    /**
     * AES偏移
     */
    public static final byte[] AES_IV = "MIIBIjANBgkqhkiG".getBytes();

}

编写AES工具类

import com.test.constant.Common;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * @author Bummon
 * @description AES工具类
 * @date 2023-08-12 09:26
 */
public class AESUtils {

    private static final String ALGORITHMSTR = "AES/CBC/PKCS5Padding";

    /**
     * @param content 加密内容
     * @return {@link String}
     * @date 2023-08-12 09:27
     * @author Bummon
     * @description 加密
     */
    public static String encrypt(String content) {
        String encode = null;
        try {
            Cipher cipher = initCipher(Cipher.ENCRYPT_MODE);
            byte[] encodeBytes = cipher.doFinal(content.getBytes());
            encode = Base64.encodeBase64String(encodeBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return encode;
    }

    public static String decrypt(String encryptStr) {
        String decode = null;
        try {
            Cipher cipher = initCipher(Cipher.DECRYPT_MODE);
            byte[] encodeBytes = Base64.decodeBase64(encryptStr);
            byte[] decodeBytes = cipher.doFinal(encodeBytes);
            decode = new String(decodeBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return decode;
    }

    /**
     * @param cipherMode 操作类型 加密/解密
     * @return {@link Cipher}
     * @date 2023-08-12 09:42
     * @author Bummon
     * @description 初始化Cipher
     */
    private static Cipher initCipher(int cipherMode) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        kgen.init(128);
        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
        SecretKeySpec keySpec = new SecretKeySpec(Common.AES_KEY, "AES");
        IvParameterSpec ivParam = new IvParameterSpec(Common.AES_IV);
        cipher.init(cipherMode, keySpec, ivParam);
        return cipher;
    }

    public static void main(String[] args) {
        String encrypt = AESUtils.encrypt("Hello World");
        String decrypt = AESUtils.decrypt(encrypt);
        System.out.println(encrypt);
        System.out.println(decrypt);
    }
}

自定义注解

该注解作用于接口上,可以对接口的加密或者解密实现更加粒子化的控制,默认入参解密,出参加密。

import org.springframework.web.bind.annotation.Mapping;
import java.lang.annotation.*;

/**
 * @author Bummon
 * @description AES加解密注解
 * @date 2023-08-12 09:44
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Mapping
@Documented
public @interface AES {

    /**
     * 入参是否解密,默认解密
     */
    boolean inDecode() default true;

    /**
     * 出参是否加密,默认加密
     */
    boolean outEncode() default true;
}

DecodeRequestBodyAdvice

import com.test.anno.AES;
import com.test.util.pwd.AESUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * @author Bummon
 * @date 2023-08-12 10:22
 * @description 请求数据解密
 */
@Slf4j
@ControllerAdvice(basePackages = "com.test.controller")
public class DecodeRequestBodyAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        try {
            boolean encode = false;
            if (methodParameter.getMethod().isAnnotationPresent(AES.class)) {
                //获取注解配置的包含和去除字段
                AES aes = methodParameter.getMethodAnnotation(AES.class);
                //入参是否需要解密
                encode = aes.decode();
            }
            if (encode) {
                log.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密");
                return new MyHttpInputMessage(inputMessage);
            } else {
                return inputMessage;
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
            return inputMessage;
        }
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    class MyHttpInputMessage implements HttpInputMessage {
        private HttpHeaders headers;

        private InputStream body;

        public MyHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
            this.headers = inputMessage.getHeaders();
            String param = IOUtils.toString(inputMessage.getBody(), "UTF-8");
            //去除请求数据中的转义字符
            String encryptStr = easpString(param).replace("\"", "");
            String decrypt = AESUtils.decrypt(encryptStr);
            this.body = IOUtils.toInputStream(decrypt, "UTF-8");
        }

        @Override
        public InputStream getBody() throws IOException {
            return body;
        }

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

        /**
         * @param param
         * @return
         */
        public String easpString(String param) {
            if (param != null && !param.equals("")) {
                String s = "{\"param\":";
                //去除param中的转义字符
                String data = param.replaceAll("\\s*|\r|\n|\t", "");
                if (!data.startsWith(s)) {
                    throw new RuntimeException("参数【param】缺失异常!");
                } else {
                    int closeLen = data.length() - 1;
                    int openLen = "{\"param\":".length();
                    String substring = StringUtils.substring(data, openLen, closeLen);
                    return substring;
                }
            }
            return "";
        }
    }
}

EncodeResponseBodyAdvice

import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.anno.AES;
import com.test.util.pwd.AESUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * @author Bummon
 * @date 2023-08-12 10:36
 * @description 返回参数加密
 */
@Slf4j
@ControllerAdvice(basePackages = "com.test.controller")
public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class aClass,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        boolean encode = false;
        if (methodParameter.getMethod().isAnnotationPresent(AES.class)) {
            //获取注解配置的包含和去除字段
            AES aes = methodParameter.getMethodAnnotation(AES.class);
            //出参是否需要加密
            encode = aes.encode();
        }
        if (encode) {
            log.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行加密");
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                String result = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
                return AESUtils.encrypt(result);
            } catch (Exception e) {
                e.printStackTrace();
                log.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
            }
        }
        return body;
    }
}

编写测试控制器

import java.util.HashMap;
import java.util.Map;

/**
 * @author Bummon
 * @description
 * @date 2023-08-12 10:37
 */
@RestController
public class TestController {

    @AES(decode = false)
    @GetMapping("/getSecret")
    public Object getSecret() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "Bummon");
        map.put("homeUrl", "https://www.bummon.com/");
        map.put("blogUrl", "https://blog.bummon.com/");
        return map;
    }

    @AES(encode = false)
    @PostMapping("/getBySecret")
    public Object getBySecret(@RequestBody Map<String, Object> map) {
        return map;
    }
}

我们在这里编写了两个接口,其中 getSecret 接口不对入参进行解密,对出参进行加密,也就是前端传明文,后端返回为密文。getBySecret 接口是对入参进行解密,不对出参加密,也就是前端传密文,后端返回为明文。

我们的测试思路就是先测试getSecret 接口,同时也获取到了密文,在测试getBySecret 接口时将getSecret 接口返回的密文作为参数传进去。

测试

image.png

我们通过getSecret 接口拿到了密文,接下来将该密文作为参数调用getBySecret 接口。

image.png

可以看到我们成功将密文解析了出来,并且对接口入参没有影响。

感谢观看。


推荐

关注博客和公众号获取最新文章

Bummon's BlogBummon's Home公众号

promotion-green.png