Как элегантно реализовать шифрование и дешифрование параметров интерфейса Spring Boot?

Spring Boot Spring
Как элегантно реализовать шифрование и дешифрование параметров интерфейса Spring Boot?

Поскольку маленький друг только что задал этот вопрос, Сонг Гэ нашел время, чтобы написать статью, чтобы обсудить эту тему со всеми.

Шифрование и дешифрование само по себе не сложно, вопрос когда этим заниматься? Это также способ определить фильтр для перехвата запроса и ответа отдельно для обработки.Хотя этот метод является грубым, он является гибким, поскольку вы можете получить параметры запроса и данные ответа из первых рук. Однако SpringMVC предоставляет нам ResponseBodyAdvice и RequestBodyAdvice, которые можно использовать для предварительной обработки запросов и ответов, что очень удобно.

Итак, сегодняшняя статья преследует две цели:

  • Поделитесь идеей шифрования и дешифрования параметра/ответа.
  • Разделите использование ResponseBodyAdvice и RequestBodyAdvice.

Ладно, давайте не будем нести чушь, давайте посмотрим.

1. Разработайте пусковое устройство для шифрования и дешифрования

Чтобы сделать разработанный нами инструмент более общим и рассмотреть пользовательский Spring Boot Starter, здесь мы сделаем этот инструмент статером, на который в будущем можно будет напрямую ссылаться в проекте Spring Boot.

Сначала мы создаем проект Spring Boot и вводим зависимость spring-boot-starter-web:

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

Поскольку наш инструмент разработан для веб-проекта и будет использоваться в веб-среде в будущем, при добавлении зависимостей здесь устанавливается область действия.

После добавления зависимости давайте сначала определим класс инструмента шифрования для резервного копирования.Существует множество вариантов шифрования, симметричное шифрование, асимметричное шифрование, симметричное шифрование может использовать разные алгоритмы, такие как AES, DES, 3DES и т. д. Здесь мы используем Java поставляется с Cipher для реализации симметричного шифрования с использованием алгоритма AES:

public class AESUtils {

    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";

    // 获取 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加密
    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解密
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
        return cipher.doFinal(Base64.getDecoder().decode(data));
    }
}

Этот класс инструментов относительно прост и не требует особых пояснений. Следует отметить, что зашифрованные данные могут быть нечитаемыми, поэтому обычно нам нужно использовать алгоритм Base64 для кодирования зашифрованных данных, чтобы получить читаемую строку. Другими словами, возвращаемое значение вышеуказанного метода шифрования AES представляет собой строку в кодировке Base64, а параметр метода дешифрования AES также представляет собой строку в кодировке Base64, которая сначала декодируется, а затем расшифровывается.

Далее мы инкапсулируем класс инструмента ответа для резервного копирования.Если вы часто смотрите видео Songge, вы уже хорошо его знаете:

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;
    }
}

Далее мы определяем две аннотации@Decryptи@Encrypt:

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

Эти две аннотации являются двумя метками.В процессе будущего использования, какой метод интерфейса добавляется с аннотацией @Encrypt, данные какого интерфейса шифруются и возвращаются, и какой интерфейс/параметр добавляется с аннотацией @Decrypt, какой интерфейс/параметр расшифровывается. Это определение тоже относительно простое, тут и говорить нечего, следует отметить, что@DecryptСравнивать@EncryptЕще один сценарий использования@Decryptможно использовать в параметрах.

Учитывая, что пользователь может настроить зашифрованный ключ самостоятельно, давайте определим класс EncryptProperties для чтения настроенного пользователем ключа:

@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;
    }
}

Здесь я установил ключ по умолчаниюwww.itboyhub.com, ключ представляет собой 16-битную строку, и адреса веб-сайта Songge вполне достаточно. В дальнейшем, если пользователь захочет настроить ключ самостоятельно, ему нужно будет только настроить его в application.propertiesspring.encrypt.key=xxxВот и все.

После того, как вся подготовительная работа проделана, пришло время официально зашифровать и расшифровать.

Поскольку очень важной целью статьи Сонгге является поделиться с вами использованием ResponseBodyAdvice и RequestBodyAdvice, у RequestBodyAdvice нет проблем при расшифровке, в то время как у ResponseBodyAdvice есть некоторые ограничения при шифровании, но это малоэффективно, или, как я уже говорил, если вы хотите чтобы очень гибко все контролировать, то следует настроить фильтр. Здесь я сначала буду использовать эти два инструмента для достижения.

Еще один момент, который следует отметить, заключается в том, что ResponseBodyAdvice вступит в силу только при использовании аннотации @ResponseBody, а RequestBodyAdvice вступит в силу только при использовании аннотации @RequestBody. Однако, как правило, сцена шифрования и дешифрования интерфейса возможна только при разделении передней и задней частей.

Давайте сначала посмотрим на шифрование интерфейса:

@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;
    }
}

Мы настраиваем класс EncryptResponse для реализации интерфейса ResponseBodyAdvice, а универсальный тип представляет возвращаемый тип интерфейса.Здесь нужно реализовать два метода:

  1. поддерживает: этот метод используется для определения того, какой интерфейс необходимо зашифровать. Параметр returnType указывает тип возвращаемого значения. Наша логика суждения здесь заключается в том, содержит ли метод@EncryptЕсли есть аннотация, значит, интерфейс нужно шифровать, если нет, значит, интерфейс не нужно шифровать.
  2. beforeBodyWrite: этот метод будет выполняться перед ответом данных, то есть мы сначала выполним вторичную обработку данных ответа, а после завершения обработки они будут преобразованы в json и возвращены. Способ обработки здесь очень простой.Статус в RespBean — это код состояния, поэтому его не нужно шифровать.Два других поля можно перешифровать и потом снова установить значение.
  3. Также обратите внимание, что пользовательский ResponseBodyAdvice должен использовать@ControllerAdviceАннотация к пометке.

Посмотрим на расшифровку интерфейса:

@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. Прежде всего, обратите внимание, что мы не реализуем напрямую класс DecryptRequest.RequestBodyAdviceинтерфейс, но наследуется от класса RequestBodyAdviceAdapter, который является подклассом интерфейса RequestBodyAdvice и реализует некоторые методы в интерфейсе, поэтому, когда мы наследуем от RequestBodyAdviceAdapter, нам нужно реализовать только определенные методы в соответствии с нашими реальными потребностями.
  2. поддерживает: этот метод используется для определения того, какие интерфейсы должны обрабатывать расшифровку интерфейса. Наша логика суждения здесь заключается в том, что метод или параметр содержит@DecryptАннотированный интерфейс, решающий проблемы с расшифровкой.
  3. beforeBodyRead: Этот метод будет выполняться до того, как параметры будут преобразованы в определенные объекты.Мы сначала загружаем данные из потока, затем расшифровываем данные, а затем реконструируем объект HttpInputMessage и возвращаем его после завершения расшифровки.

Далее давайте определим класс автоматической конфигурации следующим образом:

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

}

Об этом и говорить нечего, все относительно просто.

Наконец, определите META-INF в каталоге ресурсов, а затем определите файл spring.factories со следующим содержимым:

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

Этот класс конфигурации автоматически загружается при запуске проекта.

На этом наша начальная разработка завершена.

2. Выпуск пакета

Мы можем установить проект в локальный репозиторий или опубликовать его в Интернете для использования другими.

2.1 Установить в локальный репозиторий

Относительно просто установить на локальный склад напрямуюmvn install, или в IDEA щелкните Maven справа, а затем дважды щелкните установить, как показано ниже:

2.2 Публикация в Интернете

Если он недоступен онлайн, мы можем использовать для этого JitPack.

Сначала мы создаем хранилище на GitHub и загружаем наш код, мне не нужно больше рассказывать об этом процессе.

После успешной загрузки нажмите справаCreate a new releaseкнопку, чтобы выпустить официальную версию, следующим образом:

После успешного выпуска откройте джитпак, введите полный путь к складу, нажмите кнопку поиска и, найдя его, нажмитеGet itкнопку для завершения сборки следующим образом:

После успешной сборки на JitPack будет указан метод ссылки на проект:

Обратите внимание, что при цитировании измените тег на конкретный номер версии.

На данный момент наш инструмент успешно выпущен! Друзья могут относиться к этому стартеру следующим образом:

<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. Применение

Мы создаем обычный проект Spring Boot, вводим веб-зависимости, а затем вводим наши только стартовые зависимости следующим образом:

<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>

Затем создайте класс сущностей для резервного копирования:

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

Создайте два тестовых интерфейса:

@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);
    }
}

Первый интерфейс использует@EncryptAnnotation, поэтому данные этого интерфейса будут зашифрованы (если аннотация не используется, то не будет зашифрована), а второй интерфейс использует@DecryptТаким образом, загруженные параметры будут расшифрованы.@DecryptАннотации можно размещать как на методах, так и на параметрах.

Далее запускаем проект на тестирование.

Сначала протестируйте интерфейс запроса на получение:

Как видите, возвращаемые данные зашифрованы.

Давайте еще раз протестируем почтовый запрос:

Видно, что зашифрованные данные в параметрах восстановлены.

Если пользователь хочет изменить ключ шифрования, он может добавить следующую конфигурацию в application.properties:

spring.encrypt.key=1234567890123456

Когда зашифрованные данные поступают на внешний интерфейс, на нем также есть несколько инструментов js для обработки зашифрованных данных.Этот Сонг Гэ расскажет вам о шифровании и расшифровке js, когда у него будет время позже.

4. Резюме

Что ж, сегодняшняя статья в основном хочет поговорить с вами об использовании ResponseBodyAdvice и RequestBodyAdvice, а также о некоторых идеях шифрования. Конечно, есть много других сценариев использования ResponseBodyAdvice и RequestBodyAdvice. Друзья могут исследовать сами ~ В этой статье используется симметричное шифрование. алгоритм, вы также можете попробовать перейти на асимметричное шифрование.

Хорошо, я сегодня столько наговорил, можете попробовать~ Скачать кейс этой статьи можно после ответа на 20210309 на фоне официального аккаунта~