Click to follow the official account, Java dry goods will be delivered in time
Learn the correct posture of Spring Cloud microservices!
Use ChatGPT, it's ridiculously strong!
The blog garden is in a desperate situation to survive. .
1 Introduction
In our daily Java development, business interaction with other systems, or interface calls between microservices is inevitable
If we want to ensure the security of data transmission, the outgoing parameters of the interface are encrypted, and the incoming parameters are decrypted.
But don't want to write repetitive code, we can provide a general starter that provides general encryption and decryption functions
2. Prerequisite knowledge
2.1 hutool-crypto encryption and decryption tool
hutool-crypto provides many encryption and decryption tools, including symmetric encryption, asymmetric encryption, digest encryption, etc., which will not be introduced in detail.
2.2 The problem that the request stream can only be read once
2.2.1 Questions:
In the interface call chain, the request stream can only be called once. After processing, if you need to use the request stream to obtain data later, you will find that the data is empty.
For example, if filter or aop is used to obtain the data in the request and verify the parameters before the interface is processed, then the request stream cannot be obtained afterwards. In addition, if you are preparing for an interview to change jobs in the near future, it is recommended to brush up questions online in the Java interview library applet, covering 2000+ Java interview questions, covering almost all mainstream technical interview questions.
2.2.2 Solutions
Inheritance HttpServletRequestWrapper
, copy the stream in the request, overwrite getInputStream
and getReader method for external use. The method after each call getInputStream
is obtained from the copied binary array, which exists consistently during the existence of the object.
Using the Filter filter, at the beginning, replace the request with a request defined by yourself that can read the stream multiple times.
In this way, the repeated acquisition of the stream is realized
InputStreamHttpServletRequestWrapper
package xyz.hlh.cryptotest.utils;
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 请求流支持多次获取
*/
public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper {
/**
* 用于缓存输入流
*/
private ByteArrayOutputStream cachedBytes;
public InputStreamHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null) {
// 首次获取流时,将流放入 缓存输入流 中
cacheInputStream();
}
// 从 缓存输入流 中获取流并返回
return new CachedServletInputStream(cachedBytes.toByteArray());
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 首次获取流时,将流放入 缓存输入流 中
*/
private void cacheInputStream() throws IOException {
// 缓存输入流以便多次读取。为了方便, 我使用 org.apache.commons IOUtils
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
/**
* 读取缓存的请求正文的输入流
* <p>
* 用于根据 缓存输入流 创建一个可返回的
*/
public static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream input;
public CachedServletInputStream(byte[] buf) {
// 从缓存的请求正文创建一个新的输入流
input = new ByteArrayInputStream(buf);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return input.read();
}
}
}
HttpServletRequestInputStreamFilter
package xyz.hlh.cryptotest.filter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import xyz.hlh.cryptotest.utils.InputStreamHttpServletRequestWrapper;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
/**
* @author HLH
* @description:
* 请求流转换为多次读取的请求流 过滤器
* @email [email protected]
* @date : Created in 2022/2/4 9:58
*/
@Component
@Order(HIGHEST_PRECEDENCE + 1) // 优先级最高
public class HttpServletRequestInputStreamFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 转换为可以多次获取流的request
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
InputStreamHttpServletRequestWrapper inputStreamHttpServletRequestWrapper = new InputStreamHttpServletRequestWrapper(httpServletRequest);
// 放行
chain.doFilter(inputStreamHttpServletRequestWrapper, response);
}
}
2.3 SpringBoot parameter verification validation
The basics of Spring Boot will not be introduced. It is recommended to watch this free tutorial:
https://github.com/javastacks/spring-boot-best-practice
In order to reduce a large number of redundant parameter verification codes before the business code in the interface
SpringBoot-validation
Provides elegant parameter verification. The input parameters are all entity classes. Add corresponding annotations to the entity class fields to perform parameter verification before entering the method. If the parameters are wrong, an error will be thrown and it will not BindException
enter method.
This method must require annotations on the interface parameters @Validated
or@Valid
But in many cases, we want to call the verification function of a certain entity class in the code, so the following tool classes are needed.
ParamException
package xyz.hlh.cryptotest.exception;
import lombok.Getter;
import java.util.List;
/**
* @author HLH
* @description 自定义参数异常
* @email [email protected]
* @date Created in 2021/8/10 下午10:56
*/
@Getter
public class ParamException extends Exception {
private final List<String> fieldList;
private final List<String> msgList;
public ParamException(List<String> fieldList, List<String> msgList) {
this.fieldList = fieldList;
this.msgList = msgList;
}
}
ValidationUtils
package xyz.hlh.cryptotest.utils;
import xyz.hlh.cryptotest.exception.CustomizeException;
import xyz.hlh.cryptotest.exception.ParamException;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author HLH
* @description 验证工具类
* @email [email protected]
* @date Created in 2021/8/10 下午10:56
*/
public class ValidationUtils {
private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
/**
* 验证数据
* @param object 数据
*/
public static void validate(Object object) throws CustomizeException {
Set<ConstraintViolation<Object>> validate = VALIDATOR.validate(object);
// 验证结果异常
throwParamException(validate);
}
/**
* 验证数据(分组)
* @param object 数据
* @param groups 所在组
*/
public static void validate(Object object, Class<?> ... groups) throws CustomizeException {
Set<ConstraintViolation<Object>> validate = VALIDATOR.validate(object, groups);
// 验证结果异常
throwParamException(validate);
}
/**
* 验证数据中的某个字段(分组)
* @param object 数据
* @param propertyName 字段名称
*/
public static void validate(Object object, String propertyName) throws CustomizeException {
Set<ConstraintViolation<Object>> validate = VALIDATOR.validateProperty(object, propertyName);
// 验证结果异常
throwParamException(validate);
}
/**
* 验证数据中的某个字段(分组)
* @param object 数据
* @param propertyName 字段名称
* @param groups 所在组
*/
public static void validate(Object object, String propertyName, Class<?> ... groups) throws CustomizeException {
Set<ConstraintViolation<Object>> validate = VALIDATOR.validateProperty(object, propertyName, groups);
// 验证结果异常
throwParamException(validate);
}
/**
* 验证结果异常
* @param validate 验证结果
*/
private static void throwParamException(Set<ConstraintViolation<Object>> validate) throws CustomizeException {
if (validate.size() > 0) {
List<String> fieldList = new LinkedList<>();
List<String> msgList = new LinkedList<>();
for (ConstraintViolation<Object> next : validate) {
fieldList.add(next.getPropertyPath().toString());
msgList.add(next.getMessage());
}
throw new ParamException(fieldList, msgList);
}
}
}
2.5 Custom starter
Custom starter steps
Create factory, write function code
Declare the automatic configuration class, create the objects that need to be provided externally, and expose them uniformly through the configuration class
spring/spring.factories
Prepare a fileorg.springframework.boot.autoconfigure.EnableAutoConfiguration
named in the resource directory as the key, and automatically configure the class as the value list for registration
2.6 RequestBodyAdvice和ResponseBodyAdvice
RequestBodyAdvice
It is to process the requested json string, and the general use environment is to process the automatic decryption of interface parametersResponseBodyAdvice
It is to process the corresponding json transmission of the request, and is generally used for the encryption of the corresponding result
3. Function Introduction
When the interface corresponds to data, the encrypted data is returned. When the interface is entered as a parameter, the decrypted data is received, but before entering the interface, it will be automatically decrypted to obtain the corresponding data
4. Function Details
Encryption and decryption use the AES algorithm of symmetric encryption, and use the hutool-crypto module to implement
All entity classes extract a common parent class, including the attribute timestamp, which is used to encrypt the validity of the data after it is returned. If it exceeds 60 minutes, other interfaces will not be processed.
If the interface is annotated with encryption EncryptionAnnotation
and returns a unified json data Result class, the data will be automatically encrypted. If the data inherits the unified parent class RequestBase
, the time stamp is automatically injected to ensure the timeliness of the data
If the interface has a decryption annotation DecryptionAnnotation
, and the parameters are marked with the RequestBody annotation, the incoming json uses the unified format RequestData class, and the content inherits the parent class with a long time, it will be RequestBase
automatically decrypted and converted to the corresponding data type
The function provides the starter of Springboot, which can be used out of the box
5. Code implementation
https://gitee.com/springboot-hlh/spring-boot-csdn/tree/master/09-spring-boot-interface-crypto
5.1 Project structure
5.2 crypto-common
5.2.1 Structure
5.3 crypto-spring-boot-starter
5.3.1 Interface
5.3.2 Important codes
The parameter configuration required by crypto.properties AES
# 模式 cn.hutool.crypto.Mode
crypto.mode=CTS
# 补码方式 cn.hutool.crypto.Mode
crypto.padding=PKCS5Padding
# 秘钥
crypto.key=testkey123456789
# 盐
crypto.iv=testiv1234567890
spring.factories automatic configuration file
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
xyz.hlh.crypto.config.AppConfig
Configuration parameters required by CryptConfig AES
package xyz.hlh.crypto.config;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.Serializable;
/**
* @author HLH
* @description: AES需要的配置参数
* @email [email protected]
* @date : Created in 2022/2/4 13:16
*/
@Configuration
@ConfigurationProperties(prefix = "crypto")
@PropertySource("classpath:crypto.properties")
@Data
@EqualsAndHashCode
@Getter
public class CryptConfig implements Serializable {
private Mode mode;
private Padding padding;
private String key;
private String iv;
}
AppConfig automatic configuration class
package xyz.hlh.crypto.config;
import cn.hutool.crypto.symmetric.AES;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
/**
* @author HLH
* @description: 自动配置类
* @email [email protected]
* @date : Created in 2022/2/4 13:12
*/
@Configuration
public class AppConfig {
@Resource
private CryptConfig cryptConfig;
@Bean
public AES aes() {
return new AES(cryptConfig.getMode(), cryptConfig.getPadding(), cryptConfig.getKey().getBytes(StandardCharsets.UTF_8), cryptConfig.getIv().getBytes(StandardCharsets.UTF_8));
}
}
DecryptRequestBodyAdvice
Request automatic decryption, click to follow the official account, Java dry goods will be delivered in time
package xyz.hlh.crypto.advice;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import xyz.hlh.crypto.annotation.DecryptionAnnotation;
import xyz.hlh.crypto.common.exception.ParamException;
import xyz.hlh.crypto.constant.CryptoConstant;
import xyz.hlh.crypto.entity.RequestBase;
import xyz.hlh.crypto.entity.RequestData;
import xyz.hlh.crypto.util.AESUtil;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* @author HLH
* @description: requestBody 自动解密
* @email [email protected]
* @date : Created in 2022/2/4 15:12
*/
@ControllerAdvice
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 方法上有DecryptionAnnotation注解的,进入此拦截器
* @param methodParameter 方法参数对象
* @param targetType 参数的类型
* @param converterType 消息转换器
* @return true,进入,false,跳过
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(DecryptionAnnotation.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
/**
* 转换之后,执行此方法,解密,赋值
* @param body spring解析完的参数
* @param inputMessage 输入参数
* @param parameter 参数对象
* @param targetType 参数类型
* @param converterType 消息转换类型
* @return 真实的参数
*/
@SneakyThrows
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 获取request
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
if (servletRequestAttributes == null) {
throw new ParamException("request错误");
}
HttpServletRequest request = servletRequestAttributes.getRequest();
// 获取数据
ServletInputStream inputStream = request.getInputStream();
RequestData requestData = objectMapper.readValue(inputStream, RequestData.class);
if (requestData == null || StringUtils.isBlank(requestData.getText())) {
throw new ParamException("参数错误");
}
// 获取加密的数据
String text = requestData.getText();
// 放入解密之前的数据
request.setAttribute(CryptoConstant.INPUT_ORIGINAL_DATA, text);
// 解密
String decryptText = null;
try {
decryptText = AESUtil.decrypt(text);
} catch (Exception e) {
throw new ParamException("解密失败");
}
if (StringUtils.isBlank(decryptText)) {
throw new ParamException("解密失败");
}
// 放入解密之后的数据
request.setAttribute(CryptoConstant.INPUT_DECRYPT_DATA, decryptText);
// 获取结果
Object result = objectMapper.readValue(decryptText, body.getClass());
// 强制所有实体类必须继承RequestBase类,设置时间戳
if (result instanceof RequestBase) {
// 获取时间戳
Long currentTimeMillis = ((RequestBase) result).getCurrentTimeMillis();
// 有效期 60秒
long effective = 60*1000;
// 时间差
long expire = System.currentTimeMillis() - currentTimeMillis;
// 是否在有效期内
if (Math.abs(expire) > effective) {
throw new ParamException("时间戳不合法");
}
// 返回解密之后的数据
return result;
} else {
throw new ParamException(String.format("请求参数类型:%s 未继承:%s", result.getClass().getName(), RequestBase.class.getName()));
}
}
/**
* 如果body为空,转为空对象
* @param body spring解析完的参数
* @param inputMessage 输入参数
* @param parameter 参数对象
* @param targetType 参数类型
* @param converterType 消息转换类型
* @return 真实的参数
*/
@SneakyThrows
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
String typeName = targetType.getTypeName();
Class<?> bodyClass = Class.forName(typeName);
return bodyClass.newInstance();
}
}
EncryptResponseBodyAdvice
Automatically encrypt accordingly
package xyz.hlh.crypto.advice;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import xyz.hlh.crypto.annotation.EncryptionAnnotation;
import xyz.hlh.crypto.common.entity.Result;
import xyz.hlh.crypto.common.exception.CryptoException;
import xyz.hlh.crypto.entity.RequestBase;
import xyz.hlh.crypto.util.AESUtil;
import java.lang.reflect.Type;
/**
* @author HLH
* @description:
* @email [email protected]
* @date : Created in 2022/2/4 15:12
*/
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Result<?>> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
ParameterizedTypeImpl genericParameterType = (ParameterizedTypeImpl)returnType.getGenericParameterType();
// 如果直接是Result,则返回
if (genericParameterType.getRawType() == Result.class && returnType.hasMethodAnnotation(EncryptionAnnotation.class)) {
return true;
}
if (genericParameterType.getRawType() != ResponseEntity.class) {
return false;
}
// 如果是ResponseEntity<Result>
for (Type type : genericParameterType.getActualTypeArguments()) {
if (((ParameterizedTypeImpl) type).getRawType() == Result.class && returnType.hasMethodAnnotation(EncryptionAnnotation.class)) {
return true;
}
}
return false;
}
@SneakyThrows
@Override
public Result<?> beforeBodyWrite(Result<?> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 加密
Object data = body.getData();
// 如果data为空,直接返回
if (data == null) {
return body;
}
// 如果是实体,并且继承了Request,则放入时间戳
if (data instanceof RequestBase) {
((RequestBase)data).setCurrentTimeMillis(System.currentTimeMillis());
}
String dataText = JSONUtil.toJsonStr(data);
// 如果data为空,直接返回
if (StringUtils.isBlank(dataText)) {
return body;
}
// 如果位数小于16,报错
if (dataText.length() < 16) {
throw new CryptoException("加密失败,数据小于16位");
}
String encryptText = AESUtil.encryptHex(dataText);
return Result.builder()
.status(body.getStatus())
.data(encryptText)
.message(body.getMessage())
.build();
}
}
5.4 crypto-test
5.4.1 Structure
5.4.2 Important codes
application.yml configuration file
spring:
mvc:
format:
date-time: yyyy-MM-dd HH:mm:ss
date: yyyy-MM-dd
# 日期格式化
jackson:
date-format: yyyy-MM-dd HH:mm:ss
Teacher entity class
package xyz.hlh.crypto.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
* @author HLH
* @description: Teacher实体类,使用SpringBoot的validation校验
* @email [email protected]
* @date : Created in 2022/2/4 10:21
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Teacher extends RequestBase implements Serializable {
@NotBlank(message = "姓名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Range(min = 0, max = 150, message = "年龄不合法")
private Integer age;
@NotNull(message = "生日不能为空")
private Date birthday;
}
TestController Test Controller
package xyz.hlh.crypto.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import xyz.hlh.crypto.annotation.DecryptionAnnotation;
import xyz.hlh.crypto.annotation.EncryptionAnnotation;
import xyz.hlh.crypto.common.entity.Result;
import xyz.hlh.crypto.common.entity.ResultBuilder;
import xyz.hlh.crypto.entity.Teacher;
/**
* @author HLH
* @description: 测试Controller
* @email [email protected]
* @date : Created in 2022/2/4 9:16
*/
@RestController
public class TestController implements ResultBuilder {
/**
* 直接返回对象,不加密
* @param teacher Teacher对象
* @return 不加密的对象
*/
@PostMapping("/get")
public ResponseEntity<Result<?>> get(@Validated @RequestBody Teacher teacher) {
return success(teacher);
}
/**
* 返回加密后的数据
* @param teacher Teacher对象
* @return 返回加密后的数据 ResponseBody<Result>格式
*/
@PostMapping("/encrypt")
@EncryptionAnnotation
public ResponseEntity<Result<?>> encrypt(@Validated @RequestBody Teacher teacher) {
return success(teacher);
}
/**
* 返回加密后的数据
* @param teacher Teacher对象
* @return 返回加密后的数据 Result格式
*/
@PostMapping("/encrypt1")
@EncryptionAnnotation
public Result<?> encrypt1(@Validated @RequestBody Teacher teacher) {
return success(teacher).getBody();
}
/**
* 返回解密后的数据
* @param teacher Teacher对象
* @return 返回解密后的数据
*/
@PostMapping("/decrypt")
@DecryptionAnnotation
public ResponseEntity<Result<?>> decrypt(@Validated @RequestBody Teacher teacher) {
return success(teacher);
}
}
Copyright statement: This article is an original article of CSDN blogger "HLH_2021", following the CC 4.0 BY-SA copyright agreement, please attach the original source link and this statement for reprinting. Original link: https://blog.csdn.net/HLH_2021/article/details/122785888
Finally, I promoted my knowledge planet, and people in the circle of friends often asked me questions, but they couldn't answer them at all, so Brother R created a "Java Technology Small Circle" knowledge planet, and 2500+ friends joined.
Click the " Java Technology Small Circle " link to learn more.
The value is tens of thousands +, quickly scan the code to join:
The price is about to increase, it has long been an advantage!
End
Learn the correct posture of Spring Cloud microservices!
The blog garden is in a desperate situation to survive. .
Use ChatGPT, it's ridiculously strong!
How to delete duplicate elements in HashMap?
Java 20 is officially released, and it's amazing. .
The latest course on Spring Cloud microservices!