统一接口解密封装校验参数

需求

  最近做了一个跟合作方数据同步的接口,接口是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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值