HTTP Web Security

General Web Validation

Captcha Mechanism

HTTP Standard Authentication

API Features

Other Practical Features

API Interface Encryption/Decryption

To ensure data security, API interface data must be encrypted to prevent plaintext exposure. Encryption and decryption should be applied to API interfaces as follows:

Some approaches output the plaintext of the encryption result, as shown below:

However, this approach is considered an anti-pattern. It is better to retain the original JSON structure, as shown in the submitted JSON below:

{
    "errCode": "0",
    "data": "BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……"
}

This aligns with the existing unified response format, where only the data field is encrypted, while other fields like code and msg are displayed normally.

System Requirements: Only supports Spring + Jackson.

Encryption Algorithm

The encryption algorithm must be agreed upon by the caller (e.g., browsers) and the API interface. Typically, RSA encryption is used. Although RSA is slower than AES, it is advantageous due to its asymmetric encryption nature. AES, being a symmetric encryption mechanism, is unsuitable for this scenario because browsers cannot store any keys—except asymmetric public keys.

If the API interface is designed for third-party calls instead of browsers and can ensure key security, AES can be used. Similarly, other hashing algorithms (e.g., MD5, SHA1, SHA256) can be applied, provided the algorithm and salt value are agreed upon.

Currently, the component only supports RSA (1024-bit key). More algorithms are planned, including:

Usage

Initialization

Add the following configuration in YAML:

api:
  EncryptedBody:
    enable: true
    publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmkKluNutOWGmAK2U……
    privateKey: MIICdgIBADANBgkqhkiG9w0BAQ……

The main configuration includes RSA public/private keys. Then, add the following to the Spring configuration class WebMvcConfigurer:

@Value("${api.EncryptedBody.publicKey}")
private String apiPublicKey;

@Value("${api.EncryptedBody.privateKey}")
private String apiPrivateKey;

@Value("${api.EncryptedBody.enable}")
private boolean apiEncryptedBodyEnable;

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    EncryptedBodyConverter converter = new EncryptedBodyConverter(apiPublicKey, apiPrivateKey);
    converter.setEnabled(apiEncryptedBodyEnable);

    converters.add(0, converter);
}

Configure Encrypted Data

The usage is straightforward: add a custom Java annotation @EncryptedData to your Java Bean.

Decrypting Request Data

Observe the following Spring MVC interface declaration, which uses the standard JSON submission method with the @RequestBody annotation:

@PostMapping("/submit")
boolean jsonSubmit(@RequestBody User user);

The key part is the User DTO. To indicate encrypted data, declare the custom annotation @EncryptedData on the Bean:

package com.ajaxjs.api.encryptedbody;

@EncryptedData
public class User {
    private String name;
    private int age;

    // Getters and Setters
}

Client submissions are no longer in the form of User JSON but rather DecodeDTO (which is ultimately converted to User upon successful decryption):

package com.ajaxjs.api.encryptedbody;

import lombok.Data;

@Data
public class DecodeDTO {
    /**
     * Encrypted data
     */
    private String data;
}

The typical submission format is as follows:

{
    "data": "BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……"
}

The encrypted ciphertext is generated by the client after encryption, or it can be returned using the method described in the following section.

Encrypting Response Data

The following controller method returns a User object without any modifications:

@GetMapping("/user")
User User();

……

@Override
public User User() {
    User user = new User();
    user.setAge(1);
    user.setName("tom");
    
    return user;
}

Add the annotation @EncryptedData to encrypt the response object. Currently, field-level encryption is not supported; only entire object encryption is available.

The response example is as follows:

{
    "status": 1,
    "errorCode": null,
    "message": "Operation successful",
    "data": "ReSSPC34JE+O/SmLCxE5zVJb6D2tzp1f5pfQyKdjvOWkQQ+qDjcjw/2m/KPA+2+uc9kseqFryXNPIZCEfsaOCJAqzMtrXyZ0JPB1skeJxKOngS5USijsY0UZqN9hLS3O/7CBLlSGkEuyXZV//WcWDG9BpQ4TAKrlRfwM4bnCo+E="
}

Add Dependencies

Don't forget to add dependencies! Since no standalone jar package is provided, simply copy the source code—it's only three classes: Source Code.

The ResponseResultWrapper class is for unified response results. You can adapt this to your project's requirements. The RSA dependency uses the following utility package:

<dependency>
    <groupId>com.ajaxjs</groupId>
    <artifactId>ajaxjs-util</artifactId>
    <version>1.1.8</version>
</dependency>

It's a lightweight package, only 60 KB—safe to use!

Implementation Details

Here are the implementation principles and design considerations.

This usage essentially involves receiving Object A (encrypted, DecodeDTO) and converting it to Object B (decrypted, used by the controller). The simplest approach is as follows:

@PostMapping("/submit")
boolean jsonSubmit(@RequestBody DecodeDTO dto) {
    User user = conversionFunction(dto.getData());
}

However, this method results in numerous DecodeDTO instances scattered across the codebase, complicating API documentation and reducing clarity. Instead, a non-invasive approach is preferred. Non-invasiveness means not modifying existing code, only adding "decorations." Common techniques include AOP. Similar libraries (rsa-encrypt-body-spring-boot, encrypt-body-spring-boot-starter) also adopt AOP.

Despite its popularity, the author prefers to avoid AOP whenever possible. Alternatives like filters and interceptors were considered but ultimately deemed less suitable. Instead, the focus shifted to JSON serialization/deserialization layers, where encryption/decryption could be performed before serialization/deserialization.

The result is EncryptedBodyConverter, an extension of MappingJackson2HttpMessageConverter. The read method performs decryption before deserialization, and the writeInternal method handles encryption.

The core implementation is just one class, under 100 lines of code:

import com.ajaxjs.springboot.ResponseResultWrapper;
import com.ajaxjs.util.EncodeTools;
import com.ajaxjs.util.cryptography.RsaCrypto;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

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

public class EncryptedBodyConverter extends MappingJackson2HttpMessageConverter {
    public EncryptedBodyConverter(String publicKey, String privateKey) {
        super();
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    private final String publicKey;

    private final String privateKey;

    /**
     * Decrypts a string using the private key.
     *
     * @param encryptBody Encrypted string, Base64 encoded.
     * @param privateKey  Private key string for decryption.
     * @return Decrypted string.
     */
    static String decrypt(String encryptBody, String privateKey) {
        byte[] data = EncodeTools.base64Decode(encryptBody);

        return new String(RsaCrypto.decryptByPrivateKey(data, privateKey));
    }

    /**
     * Encrypts a string using the public key.
     *
     * @param body      Original string to be encrypted.
     * @param publicKey Public key string for encryption.
     * @return Encrypted string, Base64 encoded.
     */
    static String encrypt(String body, String publicKey) {
        byte[] encWord = RsaCrypto.encryptByPublicKey(body.getBytes(), publicKey);
        return EncodeTools.base64EncodeToString(encWord);
    }

    /**
     * Overrides the `read` method to support encrypted data reading.
     *
     * @param type         Data type for deserialization.
     * @param contextClass Context class, unused here.
     * @param inputMessage HTTP input message containing encrypted data.
     * @return Deserialized object instance.
     * @throws IOException                     If an I/O error occurs during reading.
     * @throws HttpMessageNotReadableException If the message cannot be parsed into an object.
     */
    @Override
    public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        Class<?> clz = (Class<?>) type;

        if (clz.getAnnotation(EncryptedData.class) != null) {
            ObjectMapper objectMapper = getObjectMapper();
            DecodeDTO decodeDTO = objectMapper.readValue(inputMessage.getBody(), DecodeDTO.class);
            String encryptBody = decodeDTO.getData();

            String decodeJson = decrypt(encryptBody, privateKey);

            return objectMapper.readValue(decodeJson, clz);
        }

        return super.read(type, contextClass, inputMessage);
    }

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        Class<?> clz = (Class<?>) type;

        if (object instanceof ResponseResultWrapper && clz.getAnnotation(EncryptedData.class) != null) {
            ResponseResultWrapper response = (ResponseResultWrapper) object;
            Object data = response.getData();
            String json = getObjectMapper().writeValueAsString(data);
            String encryptBody = encrypt(json, publicKey);

            response.setData(encryptBody);
        }

        super.writeInternal(object, type, outputMessage);
    }
}

TODO