需求
最近做了一个跟合作方数据同步的接口,接口是HTTP post请求,加密方法是将json字符串AES加密然后转成base64字符串。有两种情况,一种是Content-type=text/plain
,这时,整个body是一个经过加密的json字符串,另一种情况是Content-type=multipart/form-data
,这时候,每一个表单字段都是独立加密,每个文件的每一行是单独加密的json字符串。
思路
- 定义注解@Decrypt,写在controller层的方法上,类似于spring的@RequestMapping,表示这个方法需要解码
- 定义注解@DecryptPojo,@DecryptSimple,写在方法的参数上,类似于spring的@RequestParam,表示参数需要解码,@DecryptPojo表示把参数解密封装到pojo,pojo的字段名和参数名必须相同,@DecryptSimple表示把参数解密并赋值到同名参数上
- 通过springAOP拦截带有@Decrypt的方法,处理参数
代码实现
aop拦截器
package com.aaa.bbb.aspect;
import java.io.BufferedReader;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.http.HttpHeaders;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSONObject;
import com.aaa.bbb.annotation.Decrypt;
import com.aaa.bbb.annotation.DecryptPojo;
import com.aaa.bbb.annotation.DecryptSimple;
import com.aaa.bbb.annotation.Valid;
import com.aaa.bbb.exception.DecryptException;
import com.aaa.bbb.stream.DecryptLineInputStream;
import com.aaa.bbb.utils.ValidationUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
@Aspect
@Component
public class DecryptAspect{
private final ServletFileUpload servletFileUpload;
@Autowired
public DecryptAspect(ServletFileUpload servletFileUpload) {
this.servletFileUpload = servletFileUpload;
}
@Around("@annotation(decryptAnnotation)")
public Object doBefore(ProceedingJoinPoint joinPoint, Decrypt decryptAnnotation) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
Object[] params = joinPoint.getArgs();//所有参数
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();//所有参数名
Class<?>[] parameterTypes = ((CodeSignature) joinPoint.getSignature()).getParameterTypes();//所有参数的类型
if (params.length == 0) {
return joinPoint.proceed(params);
}
//获取方法,此处可将signature强转为MethodSignature
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//参数注解,1维是参数,2维是注解
Annotation[][] annotations = method.getParameterAnnotations();
Optional<AES> aesOptional = Optional.ofNullable(SecureUtil.aes(StrUtil.utf8Bytes("passwordpassword")));
if (aesOptional.isEmpty()) {
throw new DecryptException();
}
JSONObject parseObject = new JSONObject();//把解密的参数缓存到jsonObject
/*
解密参数
*/
if (ServletFileUpload.isMultipartContent(request)) {
List<FileItem> itemList = servletFileUpload.parseRequest(request);
for (FileItem fileItem : itemList) {
if (fileItem.isFormField()) {
//表单字段
String value;
try {
value = aesOptional.get().decryptStr(IoUtil.readUtf8(fileItem.getInputStream()));
} catch (Exception e) {
throw new DecryptException();
}
parseObject.put(fileItem.getFieldName(), value);
} else {
//文件
DecryptLineInputStream decryptLineInputStream = new DecryptLineInputStream(fileItem.getInputStream(), aesOptional.get());
parseObject.put(fileItem.getFieldName(), decryptLineInputStream);
}
}
} else {
BufferedReader utf8Reader = IoUtil.getUtf8Reader(request.getInputStream());
String s = utf8Reader.readLine();
if (StrUtil.isBlank(s)) {
return joinPoint.proceed(params);
}
String decryptStr;
try {
decryptStr = aesOptional.get().decryptStr(s);
} catch (Exception exception) {
throw new DecryptException();
}
parseObject = JSONObject.parseObject(decryptStr);
}
/*
绑定参数
*/
for (int i = 0; i < annotations.length; i++) {
Annotation[] paramAnn = annotations[i];
//该参数没有注解,直接下一个参数
if (paramAnn.length == 0) {
continue;
}
for (Annotation annotation : paramAnn) {
//annotation就是参数上的注解
if (annotation.annotationType().equals(DecryptPojo.class)) {
params[i] = parseObject.toJavaObject(params[i].getClass());
} else if (annotation.annotationType().equals(DecryptSimple.class)) {
params[i] = Convert.convert(parameterTypes[i], parseObject.get(paramNames[i]));
}
}
}
/*
校验参数
*/
for (int i = 0; i < annotations.length; i++) {
Annotation[] paramAnn = annotations[i];
//该参数没有注解,直接下一个参数
if (paramAnn.length == 0) {
continue;
}
for (Annotation annotation : paramAnn) {
//annotation就是参数上的注解
if (annotation.annotationType().equals(Valid.class)) {
List<String> validate = ValidationUtil.validate(params[i]);
if (!validate.isEmpty()) {
throw new BusinessException(String.join(";", validate));
}
}
}
}
return joinPoint.proceed(params);
}
}
Apache Common Upload配置
package com.aaa.bbb.config;
import cn.hutool.core.io.FileUtil;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.FileCleanerCleanup;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.File;
import java.nio.charset.StandardCharsets;
@Configuration
public class BeanConfig {
@Bean
public ServletListenerRegistrationBean<FileCleanerCleanup> resourceCleanupBean() {
return new ServletListenerRegistrationBean<>(new FileCleanerCleanup());
}
@Bean
public ServletFileUpload servletFileUpload() {
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(FileUtil.mkdir(FileUtil.getTmpDirPath() + File.separator + "uploadTemp"));
factory.setDefaultCharset(StandardCharsets.UTF_8.name());
ServletFileUpload servletFileUpload = new ServletFileUpload(factory);
servletFileUpload.setFileSizeMax(52428800L);//#最大上传大小50MB,单位Byte
return servletFileUpload;
}
}
注解
package com.aaa.bbb.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Decrypt {
}
package com.aaa.bbb.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptPojo {
}
package com.aaa.bbb.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptSimple {
String value() default "";
}
package com.aaa.bbb.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* CharSequence (length of character sequence is evaluated)
* Collection (collection size is evaluated)
* Map (map size is evaluated)
* Array (array length is evaluated)
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmpty {
String message() default "";
}
package com.aaa.bbb.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "";
}
package com.aaa.bbb.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Valid {
}
封装带解密校验功能的输入流
package com.aaa.bbb.stream;
import cn.hutool.core.io.IoUtil;
import cn.hutool.crypto.symmetric.AES;
import com.alibaba.fastjson.JSONObject;
import com.aaa.bbb.exception.DecryptException;
import com.aaa.bbb.utils.ValidationUtil;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
public class DecryptLineInputStream extends InputStream {
private final BufferedReader bufferedReader;
private final AES aes;
public DecryptLineInputStream(InputStream inputStream, AES aes) {
bufferedReader = IoUtil.getUtf8Reader(inputStream);
this.aes = aes;
}
@Override
public int read() {
throw new RuntimeException("请使用其他read方法");
}
public String readLine() throws IOException {
String line = bufferedReader.readLine();
Optional<String> optional = Optional.ofNullable(line);
String result = null;
if (optional.isPresent()) {
try {
result = aes.decryptStr(line);
} catch (Exception exception) {
throw new DecryptException();
}
}
return result;
}
public JSONObject readJsonLine() throws IOException {
String line = readLine();
Optional<String> optional = Optional.ofNullable(line);
return optional.isPresent() ? JSONObject.parseObject(line) : null;
}
public <T> T readJavaObjLine(Class<T> clazz) throws IOException {
JSONObject jsonObject = readJsonLine();
Optional<JSONObject> optional = Optional.ofNullable(jsonObject);
return optional.isPresent() ? jsonObject.toJavaObject(clazz) : null;
}
public <T> T readJavaObjLineWithValid(Class<T> clazz, StringBuilder failMsg) {
T javaObj = readJavaObjLine(clazz);
Optional<T> optional = Optional.ofNullable(javaObj);
boolean present = optional.isPresent();
if (present) {
List<String> validate = ValidationUtil.validate(javaObj);
for (String s : validate) {
failMsg.append(s);
}
return javaObj;
} else {
return null;
}
}
}
AES解密异常
package com.aaa.bbb.exception;
public class DecryptException extends RuntimeException {
}
校验工具
package com.aaa.bbb.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.aaa.bbb.annotation.NotEmpty;
import com.aaa.bbb.annotation.NotNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.*;
public class ValidationUtil {
@SuppressWarnings("ConstantConditions")
public static List<String> validate(Object obj) {
List<String> result = new LinkedList<>();
Field[] fields = ReflectUtil.getFields(obj.getClass());
for (Field field : fields) {
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
if (Objects.equals(annotation.annotationType(), NotEmpty.class)) {
Object fieldValue = ReflectUtil.getFieldValue(obj, field);
String msg = ((NotEmpty) annotation).message();
if (Objects.isNull(fieldValue)) {
result.add(msg);
}
if (StrUtil.isEmpty(msg)) {
msg = StrUtil.format("{}不能为空", field.getName());
}
if (fieldValue instanceof String) {
if (StrUtil.isEmpty((String) fieldValue)) {
result.add(msg);
}
} else if (fieldValue instanceof Collection) {
if (CollUtil.isEmpty((Collection<?>) fieldValue)) {
result.add(msg);
}
} else if (fieldValue instanceof Map) {
if (MapUtil.isEmpty((Map<?, ?>) fieldValue)) {
result.add(msg);
}
} else if (fieldValue instanceof Array) {
if (ArrayUtil.isEmpty((Object[]) fieldValue)) {
result.add(msg);
}
}
} else if (Objects.equals(annotation.annotationType(), NotNull.class)) {
Object fieldValue = ReflectUtil.getFieldValue(obj, field);
String msg = ((NotNull) annotation).message();
if (StrUtil.isEmpty(msg)) {
msg = StrUtil.format("{}不能为空", field.getName());
}
if (Objects.isNull(fieldValue)) {
result.add(msg);
}
}
}
}
return result;
}
}
pojo
package com.aaa.bbb.pojo;
import com.aaa.bbb.stream.DecryptLineInputStream;
import com.aaa.bbb.annotation.NotEmpty;
import com.aaa.bbb.annotation.NotNull;
import lombok.Data;
@Data
public class UserPojo {
@NotEmpty
private String name;
@NotEmpty
private String age;
@NotNull
private DecryptLineInputStream file;
}
测试
text/plain测试用例E6i/TvyevlNqxtmEBsXJbdb9yHjgyWTAG5eveQbd5CY=
multipart/form-data测试文件:https://download.csdn.net/download/qq_28807077/16658608
@PostMapping("/testAes")
@Decrypt
public String testAes(@DecryptSimple String name, @DecryptSimple(type = Integer.class) Integer age, @DecryptPojo UserPojo userPojo) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", name);
jsonObject.put("age", age);
jsonObject.put("userPojo", userPojo);
System.out.println(jsonObject);
return "ok";
}
@PostMapping("/testMultipart")
@Decrypt
public String testMultipart(@DecryptSimple String name, @DecryptSimple String age, @DecryptPojo UserPojo userPojo) throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", name);
jsonObject.put("age", age);
jsonObject.put("userPojo", userPojo);
DecryptLineInputStream inputStream = userPojo.getFile();
String line;
while ((line = inputStream.readLine()) != null) {
jsonObject.put(IdUtil.objectId(), line);
}
System.out.println(jsonObject);
return "ok";
}
spring framework xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
xmlns:tx="http://www.springframework.org/schema/tx">
<aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
<context:component-scan base-package="com.aaa.bbb" />
</beans>
maven配置
spring framework版
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
springboot版
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
可能会遇到的问题
如果是springboot,默认的上传机制会导致Apache Commons FileUpload servletFileUpload.parseRequest(request)返回的List长度为0,关闭springboot默认上传机制即可。
spring:
servlet:
multipart:
enabled: false
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/73840.html