springboot中如何优雅的对接口数据进行加密解密

在系统开发的过程中我们经常需要对外提供相应的API接口,为了保证系统数据的安全性,我们常常需要对传输的数据进行对称的加密。防止数据在传输的过程中被抓包,造成信息的泄露。通常的做法是我们在每个接口方法的前面先对请求的数据进行解密,解密完成后处理相应的业务逻辑,然后在对返回数据进行加密。这样做的坏处是代码太过于冗余,每写一个接口都要处理加密和解密方法。有没有什么办法可以把加密和解密的逻辑提取出来,在接口的方法中我们只关注处理业务逻辑。答案肯定是有的,springboot中的RequestBodyAdvice 和ResponseBodyAdvice就可以很好的解决这个问题。使用这两个的前提是方法中需要由@RequestBody,@ResponseBody注解,否则无效。

RequestBodyAdvice:主要是在HttpMessageConverter处理request body的前后做一些处理和body为空的时候做处理。RequestBodyAdvice有如下三个方法。只有 supports 方法返回true才会执行后面的几个方法。我们需要在请求达到方法之前对参数做一些处理,我们可以重新beforeBodyRead方法。

public interface RequestBodyAdvice {

	boolean supports(MethodParameter methodParameter, Type targetType,
			Class<? extends HttpMessageConverter<?>> converterType);

	Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

	HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

	Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

}

ResponseBodyAdvice:ResponseBodyAdvice 接口是在 Controller 执行 return 之后,在 response 返回给客户端之前,执行对 response 的一些处理。RequestBodyAdvice有如下两个方法。只有supports方法返回true才会执行beforeBodyWrite。我们需要在response 返回给客户端之前对返回的数据进行加密可以重写beforeBodyWrite方法。

public interface ResponseBodyAdvice<T> {

	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

	T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response);

}

我们先写报文解密的方法,新建一个EncryptRequestBodyAdvice类,继承RequestBodyAdvice,实现supports,handleEmptyBody,beforeBodyRead,afterBodyRead方法。并且在类上面添加@ControllerAdvice注解。supports返回true,handleEmptyBody和afterBodyRead不做任何处理,直接返回body。我们在beforeBodyRead方法中对报文进行解密:

@Slf4j
@ControllerAdvice
public class EncryptRequestBodyAdvice implements RequestBodyAdvice {

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

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

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
                                           Class<? extends HttpMessageConverter<?>> converterType)
            throws IOException {

        try {
            return new DecryptHttpInputMessage(inputMessage);
        } catch (Exception e) {
            log.error("数据解密失败", e);
        }
        return inputMessage;

    }

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

@Slf4j
class DecryptHttpInputMessage implements HttpInputMessage {
    private HttpHeaders headers;
    private InputStream body;

    public DecryptHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
        this.headers = inputMessage.getHeaders();
        String content = IOUtils.toString(inputMessage.getBody(), "utf-8");
        log.info("请求加密前报文:{}", content);
        content = AesUtil.decrypt(content, "123456");
        log.info("请求解密报文:{}", content);
        this.body = IOUtils.toInputStream(content, "utf-8");
    }

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

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

}

然后我们在写报文加密的方法,新建一个EncryptResponseBodyAdvice类,继承ResponseBodyAdvice<Object>。重写beforeBodyWrite,和supports方法,并且在类上面添加@ControllerAdvice注解Supports方法返回true,我们在beforeBodyWrite 方法中对数据进行加密。

@Slf4j
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    private Logger       logger       = LoggerFactory.getLogger(EncryptResponseBodyAdvice.class);
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Object beforeBodyWrite(Object arg0, MethodParameter arg1, MediaType arg2,
                                  Class<? extends HttpMessageConverter<?>> arg3, ServerHttpRequest arg4,
                                  ServerHttpResponse arg5) {
        String content = "";
        try {
            content = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arg0);
            log.info("返回报文加密前:{}", content);
            content = AesUtil.encrypt(content, "123456");
            log.info("返回报文加密后:{}", content);
        } catch (Exception e1) {
            logger.error("返回报文转换异常", e1);
        }
        return content;
    }

    @Override
    public boolean supports(MethodParameter arg0, Class<? extends HttpMessageConverter<?>> arg1) {
        return true;
    }

}

这里我们使用的是AES加密解密,方法如下:

public class AesUtil {
    /**
     * 加密
     */
    public static String encrypt(String text, String password) throws Exception {

        Cipher cipher = Cipher.getInstance("AES");
        byte[] byteContent = text.getBytes("utf-8");
        cipher.init(1, genKey(password));
        byte[] result = cipher.doFinal(byteContent);
        return parseByte2HexStr(result);

    }

    /**
     * 解密
     */
    public static String decrypt(String encryptText, String password) throws Exception {

        byte[] decryptFrom = parseHexStr2Byte(encryptText);
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(2, genKey(password));
        byte[] result = cipher.doFinal(decryptFrom);
        return new String(result);

    }

    private static SecretKeySpec genKey(String password) throws NoSuchAlgorithmException {
        byte[] enCodeFormat = { 0 };
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(password.getBytes());
        kgen.init(128, secureRandom);
        SecretKey secretKey = kgen.generateKey();
        enCodeFormat = secretKey.getEncoded();
        return new SecretKeySpec(enCodeFormat, "AES");
    }

    private static String parseByte2HexStr(byte[] bytes) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    private static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1) {
            return null;
        }
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);

            result[i] = ((byte) (high * 16 + low));
        }
        return result;
    }

    public static void main(String[] args) throws Exception {
        String content = "{\n" + "\"id\": 1,\n" + "\"name\": \"张三\",\n" + "\"address\":\"上海市浦东新区\"\n" + "}";

        System.out.println(AesUtil.encrypt(content, "123456"));

    }
}

然后我们在写一个Controller方法,方法很简单就是把接收到的参数直接返回如下:

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/getuser")
    public User getuser(@RequestBody User user) {
        return user;
    }
}

使用postman进行测试如下:

body中输入AES加密后的密文进行请求:

后台打印日志如下:

猜你喜欢

转载自blog.csdn.net/xinghui_liu/article/details/121208804