Using annotation + RequestBodyAdvice to realize the encryption and decryption mode of http request content

  • 2021-10-13 07:27:33
  • OfStack

Annotations are mainly used to specify those controller methods that need encryption and decryption

The implementation is relatively simple


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SecretAnnotation {
    boolean encode() default false;
    boolean decode() default false;
}

Add annotations to the methods of controller when using


    @PostMapping("/preview")
    @SecretAnnotation(decode = true)
    public ResponseVO<ContractSignVO> previewContract(@RequestBody FillContractDTO fillContractDTO)  {
        return contractSignService.previewContract(fillContractDTO);
    }

Request data from binary stream to class object data, for encrypted data, need to be decrypted before binary stream is processed, otherwise, when converted to class object, error will be reported because of data format mismatch.

Therefore, the beforeBodyRead method of RequestBodyAdvice is used for processing.


@Slf4j
@RestControllerAdvice
public class MyRequestControllerAdvice implements RequestBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return methodParameter.hasParameterAnnotation(RequestBody.class);
    }
    @Override
    public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }
    @Autowired
    private MySecretUtil mySecretUtil;
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        if (methodParameter.getMethod().isAnnotationPresent(SecretAnnotation.class)) {
            SecretAnnotation secretAnnotation = methodParameter.getMethod().getAnnotation(SecretAnnotation.class);
            if (secretAnnotation.decode()) {
                return new HttpInputMessage() {
                    @Override
                    public InputStream getBody() throws IOException {
                        List<String> appIdList = httpInputMessage.getHeaders().get("appId");
                        if (appIdList.isEmpty()){
                            throw new RuntimeException(" Request header missing appID");
                        }
                        String appId = appIdList.get(0);
                        String bodyStr = IOUtils.toString(httpInputMessage.getBody(),"utf-8");
                        bodyStr = mySecretUtil.decode(bodyStr,appId);
                        return  IOUtils.toInputStream(bodyStr,"utf-8");
                    }
                    @Override
                    public HttpHeaders getHeaders() {
                        return httpInputMessage.getHeaders();
                    }
                };
            }
        }
        return httpInputMessage;
    }
    @Override
    public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }
}

The content of mySecretUtil. decode (bodyStr, appId) is to search for the secret key in the database through AppID in the request header, then decrypt it and return the decrypted string.

Then, through the tool class IOUtils provided in common. io package, the string is converted into inputstream stream, replacing HttpInputMessage, and returning an HttpInputMessage with body data as decrypted binary stream.

How Stringboot RequestBodyAdvice Interface Realizes Request Response Encryption and Decryption

In actual projects, we often need to do some operations before and after the request, such as parameter decryption/return result encryption, printing request parameters and return result log, etc. These non-business things, we don't want to write in the controller method, resulting in poor code repetition readability. Here, let's talk about using @ ControllerAdvice, RequestBodyAdvice, ResponseBodyAdvice to process before and after requests (AOP in essence) to log the parameters of every request and return results.

1. Encryption and decryption tool class


package com.linkus.common.utils;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Aes {
    /**
     *
     * @author ngh
     * AES128  Algorithm 
     *
     * CBC  Mode 
     *
     * PKCS7Padding  Fill mode 
     *
     * CBC The schema needs to add an offset parameter iv , must 16 Bit 
     *  Key  sessionKey , must 16 Bit 
     *
     *  Between java  Not supported PKCS7Padding , support only PKCS5Padding  But PKCS7Padding  And  PKCS5Padding  It doesn't make any difference 
     *  To implement the java End use PKCS7Padding Fill, need to use bouncycastle Component to implement 
     */
    private String sessionKey=" Encryption and decryption key ";
    //  Offset  16 Bit 
    private static  String iv=" Offset ";
    //  Algorithm name 
    final String KEY_ALGORITHM = "AES";
    //  Encryption and decryption algorithm / Mode / Filling mode 
    final String algorithmStr = "AES/CBC/PKCS7Padding";
    //  Encryption and decryption   Key  16 Bit 
    byte[] ivByte;
    byte[] keybytes;
    private Key key;
    private Cipher cipher;
    boolean isInited = false;
    public void init() {
        //  If the key is insufficient 16 Bit, then make up .   This if  The content in is very important 
        keybytes = iv.getBytes();
        ivByte = iv.getBytes();
        Security.addProvider(new BouncyCastleProvider());
        //  Convert into JAVA Key format of 
        key = new SecretKeySpec(keybytes, KEY_ALGORITHM);
        try {
            //  Initialization cipher
            cipher = Cipher.getInstance(algorithmStr, "BC");
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchProviderException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    /**
     *  Encryption method 
     *
     * @param content
     *             String to encrypt 
     *             Encryption key 
     * @return
     */
    public String encrypt(String content) {
        byte[] encryptedText = null;
        byte[] contentByte = content.getBytes();
        init();
        try {
            cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(ivByte));
            encryptedText = cipher.doFinal(contentByte);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return new String(Hex.encode(encryptedText));
    }
    /**
     *  Decryption method 
     *
     * @param encryptedData
     *             String to decrypt 
     *             Decryption key 
     * @return
     */
    public String decrypt(String encryptedData) {
        byte[] encryptedText = null;
        byte[] encryptedDataByte = Hex.decode(encryptedData);
        init();
        try {
            cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivByte));
            encryptedText = cipher.doFinal(encryptedDataByte);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return new String(encryptedText);
    }
    public static void main(String[] args) {
        Aes aes = new Aes();
        String a="{\n" +
                "\"distance\":\"1000\",\n" +
                "\"longitude\":\"28.206471\",\n" +
                "\"latitude\":\"112.941301\"\n" +
                "}";
        // Encrypted string 
        //String content = " Meng Fei runs fast ";
        // System.out.println(" Before encryption: " + content);
//        System.out.println(" Encryption key: " + new String(keybytes));
        //  Encryption method 
        String enc = aes.encrypt(a);
        System.out.println(" Encrypted content: " + enc);
        String dec="";
        //  Decryption method 
        try {
            dec = aes.decrypt(enc);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(" Decrypted content: " + dec);
    }
}

Step 2 Request decryption

The front-end page sends the ciphertext. We need to decrypt the ciphertext before Controller gets the request and then send it to Controller


package com.linkus.common.filter;
import com.alibaba.fastjson.JSON;
import com.linkus.common.constant.KPlatResponseCode;
import com.linkus.common.exception.CustomException;
import com.linkus.common.exception.JTransException;
import com.linkus.common.service.util.MyHttpInputMessage;
import com.linkus.common.utils.Aes;
import com.linkus.common.utils.http.HttpHelper;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.lang.reflect.Type;
/**
 *  Request parameter   Decryption operation 
 *
 * @Author: Java Broken thoughts 
 * @Date: 2019/10/24 21:31
 *
 */
@Component
// You can configure to specify the package to be decrypted, supporting multiple 
@ControllerAdvice(basePackages = {"com.linkus.project"})
@Slf4j
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
    Logger log = LoggerFactory.getLogger(getClass());
    Aes aes=new Aes();
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
    	//true Turn on the function, false Turn off this feature 
        return true;
    }
// Process the request before reading it 
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException {
        // Get request data 
        String string = "";
        BufferedReader bufferedReader = null;
        InputStream inputStream = inputMessage.getBody();
            // This request In fact, it is a reference   You can get the stream from here 
            // Insert parameters and put them HttpInputMessage Inside    The return value of this method is also HttpInputMessage
            try {
            string=getRequestBodyStr(inputStream,bufferedReader);
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    throw ex;
                }
            }
        }
        /***************** Decrypt start*******************/
        String decode = null;
        if(HttpHelper.isEncrypted(inputMessage.getHeaders())){
        try {
            //            // Decryption operation 
            //Map<String,String> dataMap = (Map)body;
            //log.info(" The original request data is received ={}", string);
            // inputData  Is the data source to be encrypted and decrypted 
			// Decryption 
            decode= aes.decrypt(string);
            //log.info(" Decrypted data ={}",decode);
        } catch (Exception e ) {
            log.error(" Encryption and decryption error :",e);
           throw  new CustomException(KPlatResponseCode.MSG_DECRYPT_TIMEOUT,KPlatResponseCode.CD_DECRYPT_TIMEOUT);
        }
        // Put the data in the object we encapsulate 
        }else{
            decode = string;
        }
       // log.info(" Received request data ={}", decode);
//        log.info(" Interface request address {}",((HttpServletRequest)inputMessage).getRequestURI());
        return new MyHttpInputMessage(inputMessage.getHeaders(), new ByteArrayInputStream(decode.getBytes("UTF-8")));
    }
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
    @Override
    public Object handleEmptyBody(@Nullable Object var1, HttpInputMessage var2, MethodParameter var3, Type var4, Class<? extends HttpMessageConverter<?>> var5) {
        return var1;
    }
	// A method written by yourself, not an interface method, handles ciphertext 
    public String getRequestBodyStr( InputStream inputStream,BufferedReader bufferedReader) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        String string = stringBuilder.toString();
        return string;
    }
}

Step 3 Encrypt the response

Encrypt the response returned to the front end to ensure the security of data


package com.linkus.common.filter;
import com.alibaba.fastjson.JSON;
import com.linkus.common.utils.Aes;
import com.linkus.common.utils.DesUtil;
import com.linkus.common.utils.http.HttpHelper;
import io.swagger.models.auth.In;
import lombok.experimental.Helper;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 *  Request parameter   Encryption operation 
 *
 * @Author: Java Broken thoughts 
 * @Date: 2019/10/24 21:31
 *
 */
@Component
@ControllerAdvice(basePackages = {"com.linkus.project"})
@Slf4j
public class EncryResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    Logger log = LoggerFactory.getLogger(getClass());
    Aes aes=new Aes();
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }
    
    @Override
    public Object beforeBodyWrite(Object obj, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        String returnStr = "";
        Object retObj = null;
        log.info(" Interface request address {}",serverHttpRequest.getURI());
        // Log filtering 
        //retObj=infofilter.getInfoFilter(returnType,obj);
        if(HttpHelper.isEncrypted(serverHttpRequest.getHeaders())) {
            try {
                // Add encry header Tells the front end that the data is encrypted 
                //serverHttpResponse.getHeaders().add("infoe", "e=a");
                // Get request data 
                String srcData = JSON.toJSONString(obj);
                // Encryption 
                returnStr = aes.encrypt(srcData).replace("\r\n", "");
                //log.info(" Raw data ={}, Encrypted data ={}", obj, returnStr);
                return returnStr;
            } catch (Exception e) {
                log.error(" Abnormal! ", e);
            }
        }
        log.info(" Raw data ={}",JSON.toJSONString(obj));
        return obj;
    }
}

Related articles: