What is Spring Boot interface decryption?

Posted by daq on Wed, 22 Dec 2021 17:54:10 +0100

1. Develop encryption and decryption starter

In order to make the tool we developed more general and review the custom Spring Boot Starter, we will make this tool into a stater, which will be used in the future

You can directly reference it in the Spring Boot project.

First, we create a Spring Boot project and introduce the Spring Boot starter web dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided</scope>
    <version>2.4.3</version>
</dependency>

Because our tool is developed for Web projects and will be used in the Web environment in the future, the scope is set to provided when adding dependencies here.

After the dependency is added, we first define an encryption tool class for backup. There are many options for encryption, including symmetric encryption and asymmetric encryption, in which AES, DES and 3DES can be used for symmetric encryption

For different algorithms, we use the Cipher provided by Java to realize symmetric encryption, and use the AES algorithm:

public class AESUtils {
    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
    // Get cipher
    private static Cipher getCipher(byte[] key, int model) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(model, secretKeySpec);
        return cipher;
    }
    // AES encryption
    public static String encrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
        return Base64.getEncoder().encodeToString(cipher.doFinal(data));
    }
    // AES decryption
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
        return cipher.doFinal(Base64.getDecoder().decode(data));
    }
}

This tool class is relatively simple and does not need to be explained. It should be noted that the encrypted data may not be readable, so we generally need to use Base64 for the encrypted data

The algorithm is encoded to obtain a readable string. In other words, the return value of the above AES encryption method is a Base64 encoded string, and the parameters of the AES decryption method are also Base64

The encoded string is decoded first and then decrypted.

Next, we encapsulate a response tool class for backup. If you often watch SongGe videos, you already know it well:

public class RespBean {
    private Integer status;
    private String msg;
    private Object obj;
    public static RespBean build() {
        return new RespBean();
    }
    public static RespBean ok(String msg) {
        return new RespBean(200, msg, null);
    }
    public static RespBean ok(String msg, Object obj) {
        return new RespBean(200, msg, obj);
    }
    public static RespBean error(String msg) {
        return new RespBean(500, msg, null);
    }
    public static RespBean error(String msg, Object obj) {
        return new RespBean(500, msg, obj);
    }
    private RespBean() {
    }
    private RespBean(Integer status, String msg, Object obj) {
        this.status = status;
        this.msg = msg;
        this.obj = obj;
    }
    public Integer getStatus() {
        return status;
    }
    public RespBean setStatus(Integer status) {
        this.status = status;
        return this;
    }
    public String getMsg() {
        return msg;
    }
    public RespBean setMsg(String msg) {
        this.msg = msg;
        return this;
    }
    public Object getObj() {
        return obj;
    }
    public RespBean setObj(Object obj) {
        this.obj = obj;
        return this;
    }
}

Next, we define two annotations @ Decrypt and @ Encrypt:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PARAMETER})
public @interface Decrypt {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}

These two annotations are two tags. In the process of later use, which interface method adds the @ Encrypt annotation will Encrypt the data of which interface, and which interface / parameter adds @ Decrypt

The annotation decrypts which interface / parameter. This definition is also relatively simple. There is nothing to say. It should be noted that @ Decrypt has one more usage scenario than @ Encrypt

@Decrypt can be used on parameters.

Considering that users may configure their own encrypted keys, we will define an EncryptProperties class to read the user configured keys:

@ConfigurationProperties(prefix = "spring.encrypt")
public class EncryptProperties {
    private final static String DEFAULT_KEY = "www.itboyhub.com";
    private String key = DEFAULT_KEY;
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
}

Here, I set the default key to www.itboyhub.com COM and key are 16 bit strings. The website address of SongGe just meets the requirements. In the future, if the user wants to configure it himself

key, just in application Configure spring in properties encrypt. key = XXX.

After all the preparations are finished, it's time to officially encrypt and decrypt.

Because a very important purpose of SongGe's article is to share ResponseBodyAdvice and RequestBodyAdvice with you

There is no problem with RequestBodyAdvice when decrypting, while ResponseBodyAdvice

When doing encryption, there will be some limitations, but the impact is small. As I said earlier, if you want to control everything flexibly, you'd better customize the filter. Here I will use these two tools to realize it first.

In addition, it should be noted that ResponseBodyAdvice will take effect only when you use @ ResponseBody annotation. RequestBodyAdvice

It will take effect only when you use the @ RequestBody annotation. In other words, the front and back ends are JSON

These two are useful when interacting. However, generally speaking, the scene of interface encryption and decryption is only possible when the front and rear ends are separated.

Let's first look at interface encryption:

@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class EncryptResponse implements ResponseBodyAdvice<RespBean> {
    private ObjectMapper om = new ObjectMapper();
    @Autowired
    EncryptProperties encryptProperties;
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.hasMethodAnnotation(Encrypt.class);
    }
    @Override
    public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        byte[] keyBytes = encryptProperties.getKey().getBytes();
        try {
            if (body.getMsg()!=null) {
                body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes));
            }
            if (body.getObj() != null) {
                body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()), keyBytes));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return body;
    }
}

We customize the EncryptResponse class to implement responsebodyadvice < respbean >

Interface. Generics represent the return type of the interface. There are two methods to be implemented here:

  1. supports: this method is used to determine what kind of interface needs encryption. The parameter returnType indicates the return type. Our judgment logic here is whether the method contains @ Encrypt annotation. If yes, it means that the interface needs encryption. If not, it means that the interface does not need encryption.
  2. beforeBodyWrite: this method will be executed before the data response, that is, we will process the response data twice before it is converted to json return. The processing method here is very simple. If the status in RespBean is the status code, you don't need to encrypt it. You can re encrypt the other two fields and set the value again.
  3. In addition, it should be noted that the customized ResponseBodyAdvice needs to be marked with the @ ControllerAdvice annotation.

Let's look at interface decryption:

@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {
    @Autowired
    EncryptProperties encryptProperties;
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
    }
    @Override
    public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        byte[] body = new byte[inputMessage.getBody().available()];
        inputMessage.getBody().read(body);
        try {
            byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes());
            final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
            return new HttpInputMessage() {
                @Override
                public InputStream getBody() throws IOException {
                    return bais;
                }
                @Override
                public HttpHeaders getHeaders() {
                    return inputMessage.getHeaders();
                }
            };
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
    }
}
  1. First of all, please note that the DecryptRequest class does not directly implement the RequestBodyAdvice interface, but inherits from the RequestBodyAdvice adapter class. This class is a subclass of the RequestBodyAdvice interface and implements some methods in the interface. In this way, when we inherit from the RequestBodyAdvice adapter, we only need to implement some methods according to our actual needs.
  2. supports: this method is used to determine which interfaces need to handle interface decryption. Our judgment logic here is the interface with @ Decrypt annotation on the method or parameter to handle decryption.
  3. beforeBodyRead: this method will be executed before the parameters are converted to specific objects. We first load the data from the stream, then decrypt the data, and then reconstruct the HttpInputMessage object to return.

Next, we will define an automation configuration class as follows:

@Configuration
@ComponentScan("org.javaboy.encrypt.starter")
public class EncryptAutoConfiguration {
}

There's nothing to say about this. It's relatively simple.

Finally, META-INF is defined in the resources directory, and then spring. Inf is defined The contents of the factories file are as follows:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.javaboy.encrypt.starter.autoconfig.EncryptAutoConfiguration

This will automatically load the configuration class when the project starts.

So far, our starter has been developed.

2. Packaging and publishing

We can install the project in the local warehouse or publish it online for others to use.

2.1 installation to local warehouse

It is easy to install to the local warehouse. Directly mvn install, or in the IDEA, click Maven on the right, and then double-click install, as shown below:

image

2.2 publish online

If it can't be sent online, we can use JitPack to do it.

First, we create a repository on GitHub and upload our code. I don't need to say more about this process.

After the upload is successful, click the Create a new release button on the right to release an official version, as follows:

image

image

After publishing successfully, open jitpack, enter the full path of the warehouse, click lookup, find it, and then click Get it to complete the construction, as follows:

image

After the construction is successful, the JitPack will give the project reference method:

image

Note that when quoting, change the tag to your specific version number.

So far, our tool has been successfully released! Partners can refer to this starter in the following ways:

<dependencies>
    <dependency>
        <groupId>com.github.lenve</groupId>
        <artifactId>encrypt-spring-boot-starter</artifactId>
        <version>0.0.3</version>
    </dependency>
</dependencies>
<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

3. Application

We create an ordinary Spring Boot project, introduce the web dependency, and then the starter dependency we just introduced, as follows:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.lenve</groupId>
        <artifactId>encrypt-spring-boot-starter</artifactId>
        <version>0.0.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

Then create an alternate entity class:

public class User {
    private Long id;
    private String username;
    //Omit getter/setter
}

Create two test interfaces:

@RestController
public class HelloController {
    @GetMapping("/user")
    @Encrypt
    public RespBean getUser() {
        User user = new User();
        user.setId((long) 99);
        user.setUsername("javaboy");
        return RespBean.ok("ok", user);
    }
    @PostMapping("/user")
    public RespBean addUser(@RequestBody @Decrypt User user) {
        System.out.println("user = " + user);
        return RespBean.ok("ok", user);
    }
}

The first interface uses the @ Encrypt annotation, so the data of the interface will be encrypted (if the annotation is not used, it will not be encrypted), and the second interface uses @ Decrypt

Therefore, the uploaded parameters will be decrypted. Note that the @ Decrypt annotation can be placed on both methods and parameters.

Next, start the project for testing.

First, test the get request interface:

image

As you can see, the returned data has been encrypted.

Test the post request again:

image

You can see that the encrypted data in the parameter has been restored.

If you want to modify the encryption key, you can use the Add the following configuration to properties:

spring.encrypt.key=1234567890123456

When the encrypted data arrives at the front end, there are also some js tools at the front end to process the encrypted data. Brother song will talk to you about js encryption and decryption later.