基于自定义注解实现白盒接口方案

        项目为提高接口调用安全性和保护敏感数据,往往会选择将请求和响应采取加密方式来处理。本文分享基于自定义注解实现白盒接口的操作实践。

        一、自定义注解类和切面类

package com.example.test.handler;

import com.example.test.common.WhiteBoxCommonVO;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface WhiteBox {

    // 参数类(用来传递加密数据,只有方法参数中有此类或此类的子类才会执行加解密)
    Class request() default WhiteBoxCommonVO.class;
    Class response() default WhiteBoxCommonVO.class;
}



package com.example.test.handler;

import com.alibaba.fastjson.JSONObject;
import com.example.test.common.BaseException;
import com.example.test.utils.ValidateUtil;
import com.example.test.utils.WhiteBoxUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

@Aspect
@Component
@Slf4j
public class WhiteBoxAspect {

    private static final String DATA = "data";
    private static final String GET_DATA = "getData";
    private static final String WHITE_BOX_COMMON = "WhiteBoxCommonVO";

    @Value("${white.box.enable:true}")
    private boolean whiteBoxEnable;
    /**
     * 定义切点,使用了@WhiteBox注解的类 或 使用了@WhiteBox注解的方法
     */
    @Pointcut("@within(com.example.test.handler.WhiteBox) || @annotation(com.example.test.handler.WhiteBox)")
    public void pointcut(){}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable{
            // 获取被代理方法参数
            Object[] args = point.getArgs();
            // 获取被代理对象
            Object target = point.getTarget();
            // 获取通知签名
            MethodSignature signature = (MethodSignature )point.getSignature();
            // 获取被代理方法
            Method pointMethod = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            // 获取被代理方法上面的注解@WhiteBox
            WhiteBox whiteBox = pointMethod.getAnnotation(WhiteBox.class);
            // 被代理方法上没有,则说明@WhiteBox注解在被代理类上
            if(null == whiteBox){
                whiteBox = target.getClass().getAnnotation(WhiteBox.class);
            }
            //处理请求
            handleRequest(whiteBox,args);
            Object result = point.proceed(args);
            //返回结果是否加密
            return handleResponse(whiteBox,result);
    }

    /**
     * 处理request
     * @param whiteBox
     * @param args
     * @throws Exception
     */
    private void handleRequest(WhiteBox whiteBox,Object[] args){
        try {
            // 获取注解上声明的加解密类
            Class requestClass = whiteBox.request();
            String simpleName = requestClass.newInstance().getClass().getSimpleName();
            //白盒开关打开
            if (whiteBoxEnable){
                //请求加密的:先解密再校验参数
                if (!WHITE_BOX_COMMON.equals(simpleName)){
                    for (int i = 0; i < args.length; i++) {
                        // 如果是clazz类型则说明请求加密
                        if(requestClass.isInstance(args[i])){
                            //将args[i]转换为clazz表示的类对象
                            Object cast = requestClass.cast(args[i]);
                            Method method = requestClass.getMethod(GET_DATA);
                            // 执行方法,获取加密数据
                            String encryptStr = (String) method.invoke(cast);
                            String json = WhiteBoxUtil.decode(encryptStr);
                            // 转换vo
                            args[i] = JSON.parseObject(json, (Type) args[i].getClass());
                            //参数校验
                            ValidateUtil.validate(args[i]);
                        }
                    }
                }else{
                    //请求不加密的:校验参数
                    validateArg(args);
                }
            }else {
                //白盒开关关闭 且使用@WhiteBox
                if (WHITE_BOX_COMMON.equals(simpleName)){
                    validateArg(args);
                }
            }
        }catch (BaseException e){
            throw e;
        }catch (Exception e) {
            log.error("处理代理对象请求参数异常",e);
            throw new BaseException(HttpStatus.BAD_REQUEST,"10005","处理代理对象请求参数异常",false);
        }
    }

    /**
     * 处理response
     * @param whiteBox
     * @param result
     * @return
     */
    private Object handleResponse(WhiteBox whiteBox,Object result){
        try {
            Class responseClass = whiteBox.response();
            String simpleName = responseClass.newInstance().getClass().getSimpleName();
            if (!WHITE_BOX_COMMON.equals(simpleName) && whiteBoxEnable){
                log.info("返回结果加密前:{}",JSONObject.toJSONString(result));
                String s = WhiteBoxUtil.encode(JSONObject.toJSONString(result));
                log.info("返回结果加密后:{}",s);
                //处理result对象数据返回
                return handleResult(responseClass,s);
            }
            return result;
        }catch (BaseException e){
            throw e;
        }catch (Exception e) {
            log.error("处理代理对象返回结果异常",e);
            throw new BaseException(HttpStatus.BAD_REQUEST,"10004","处理代理对象返回结果异常",false);
        }
    }

    /**
     * 校验参数对象
     * @param args
     */
    private void validateArg(Object[] args){
        if (null != args && args.length > 0){
            for (int i = 0; i < args.length; i++) {
                ValidateUtil.validate(args[i]);
            }
        }
    }

    /**
     * 构造加密返回对象数据
     * @param result
     * @return
     */
    private Object handleResult(Class result,String s) {
        try {
            Object obj = result.newInstance();
            Field field = result.getSuperclass().getDeclaredField(DATA);
            field.setAccessible(true);
            field.set(obj, s);
            return obj;
        } catch (Exception e) {
            log.error("handleResult exception",e);
            throw new BaseException(HttpStatus.BAD_REQUEST,"10001","构造加密返回对象数据",false);
        }
    }
}

        二、关于Request和Response的说明

        请求与响应类TestDriveFormRequest和TestDriveFormResponse需继承WhiteBoxCommonVO,用于接收和响应加密后的数据。

        其中WhiteBoxCommonVO编码如下:

package com.example.test.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class WhiteBoxCommonVO {
    @Pattern(regexp = "^.{0,5000}$")
    @Size(max = 5000, message = "data长度不能超过5000个字符")
    private String data;
}

        三、注解启动开关(默认开启)

white.box.enable=true

         四、自定义注解使用方式(可作用在类上或方法上)

①request不加密,response加密:

@WhiteBox(response = TestDriveFormResponse.class)

②request加密,response不加密

@WhiteBox(request = TestDriveFormRequest.class)

③request和response都加密的

@WhiteBox(request = TestDriveFormRequest.class,response = TestDriveFormResponse.class)

④request和response都不加密的

        无需做任何处理。

        五、添加手动校验类ValidateUtil

package com.example.test.utils;

import com.example.test.common.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

@Slf4j
public class ValidateUtil {

    private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    /**
     * 校验实体类
     */
    public static <T> void validate(T t){
            Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);
            if (constraintViolations.size() > 0) {
                StringBuilder validateError = new StringBuilder();
                for (ConstraintViolation<T> constraintViolation : constraintViolations) {
                    validateError.append(constraintViolation.getMessage()).append(";");
                }
                log.error("ValidationException,{}",validateError.toString());
                throw new BaseException(HttpStatus.BAD_REQUEST,"10000",validateError.toString(),false);
            }
    }
}

        六、添加全局异常处理类

package com.example.test.common;

import org.springframework.http.HttpStatus;

public class BaseException extends RuntimeException {

    private String code;
    private String message;
    private HttpStatus httpStatus;
    private boolean notification = false;

    public BaseException(HttpStatus httpStatus,String code, String message,boolean notification) {
        super(message);
        this.httpStatus = httpStatus;
        this.code = code;
        this.message = message;
        this.notification = notification;
    }
}

        七、白盒工具类源码

package com.example.test.utils;

import com.example.test.common.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;

/**
 * 该工具加/解密规则:(Rule of decrypt and encrypt)
 * 算法 (algorithm):                 AES
 * 模式 (model):                     CBC
 * 填充 (Padding):                   PKCS7Padding
 * 输出 (output):                    String
 * 编码 (encode):                    utf-8
 * 密钥类型/长度 (key type/length):   hex / 64
 * 偏移量类型/长度 (iv type/length):  byte / 16
 */
public class WhiteBoxUtil {

    public static final Logger log = LoggerFactory.getLogger(WhiteBoxUtil.class);

    /**
     * 密钥, 256 hex string 64字节(需解析处理)
     */
    public static String DEFAULT_SECRET_KEY = "";

    @Value("${aes.secret.key}")
    public void setCocoCompanyId(String secretKey){
        DEFAULT_SECRET_KEY = secretKey;
    }

    private static final String AES = "AES";
    /**
     * 初始向量IV, 初始向量IV的长度规定为128位16个字节, 初始向量的来源于前端定义一致.
     */
    private static final byte[] KEY_VI = new byte[]{0x79, 0x60, 0x07, 0x67, 0x39, 0x5f, 0x3d, 0x7c, 0x79, 0x60, 0x07, 0x67,0x39, 0x5f, 0x3d, 0x7c};

    /**
     * 加密解密算法/加密模式/填充方式
     */
    private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";

    private static java.util.Base64.Encoder base64Encoder = java.util.Base64.getEncoder();
    private static java.util.Base64.Decoder base64Decoder = java.util.Base64.getDecoder();

    /**
     * 初始化加密provider等
     */
    static {
        Security.setProperty("crypto.policy", "unlimited");
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    }

    /**
     * AES加密
     */
    public static String encode(String content) {
        try {
            javax.crypto.SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(parseHexStr2Byte(DEFAULT_SECRET_KEY), AES);
            javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM);
            cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKey, new javax.crypto.spec.IvParameterSpec(KEY_VI));

            // 获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
            byte[] byteEncode = content.getBytes(java.nio.charset.StandardCharsets.UTF_8);

            // 根据密码器的初始化方式加密
            byte[] byteAES = cipher.doFinal(byteEncode);

            // 将加密后的数据转换为字符串
            return base64Encoder.encodeToString(byteAES);
        } catch (Exception e) {
            log.error("白盒工具类加密异常",e);
            throw new BaseException(HttpStatus.BAD_REQUEST,"10002","白盒工具类加密异常",false);
        }
    }

    /**
     * AES解密
     */
    public static String decode(String content){
        try {
            javax.crypto.SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(parseHexStr2Byte(DEFAULT_SECRET_KEY), AES);
            javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM);
            cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, new javax.crypto.spec.IvParameterSpec(KEY_VI));

            // 将加密并编码后的内容解码成字节数组
            byte[] byteContent = base64Decoder.decode(content);
            // 解密
            byte[] byteDecode = cipher.doFinal(byteContent);
            return new String(byteDecode, java.nio.charset.StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("白盒工具类解密异常",e);
            throw new BaseException(HttpStatus.BAD_REQUEST,"10003","白盒工具类解密异常",false);
        }
    }

    /**
     * 密钥为hex字符串,在使用前需要重新处理为byte[]
     * @param hexStr
     * @return
     */
    public static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }
}

        八、编写Controller类

package com.example.test.controller;

import com.example.test.common.*;
import com.example.test.handler.WhiteBox;
import com.example.test.utils.WhiteBoxUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.Valid;

@Validated
@RestController
@Api(tags = "demo测试")
public class TestController {

    private final static Logger logger = LoggerFactory.getLogger(TestController.class);
    /**
     * request不加密,response加密
     * @param request
     * @return
     */
    @WhiteBox(response = TestDriveFormResponse.class)
    @GetMapping(value = "/test1")
    @ApiOperation("request不加密,response加密")
    public TestDriveFormResponse test1(@RequestBody TestDriveFormRequest request){
        TestDriveFormResponse response = TestDriveFormResponse.builder()
                .id("1")
                .name("abc")
                .build();
        logger.info("response加密后:{}", WhiteBoxUtil.encode("abc"));
        return response;
    }

    /**
     * request加密,response不加密
     * @param request   注释掉,在白盒切面类处理
     * @return
     */
    @GetMapping(value = "/test2")
    @WhiteBox(request = TestDriveFormRequest.class)
    @ApiOperation("request加密,response不加密")
    public TestDriveFormResponse test2(@RequestBody TestDriveFormRequest request){
        //TODO 业务逻辑
        TestDriveFormResponse response = TestDriveFormResponse.builder()
                .id("1")
                .name("abc")
                .build();
        return response;
    }

    /**
     * request和response都加密的
     * @param request
     * @return
     */
    @GetMapping(value = "/test3")
    @WhiteBox(request = TestDriveFormRequest.class,response = TestDriveFormResponse.class)
    @ApiOperation("request和response都加密的")
    public TestDriveFormResponse test(@RequestBody TestDriveFormRequest request){
        //TODO 业务逻辑
        TestDriveFormResponse response = TestDriveFormResponse.builder()
                .id("1")
                .name("abc")
                .build();
        return response;
    }

    /**
     * request和response都不加密
     * @param request
     * @return
     */
    @GetMapping(value = "/test4")
    @ApiOperation("request和response都不加密")
    public TestDriveFormResponse test4(@RequestBody @Valid TestDriveFormRequest request){
        //TODO 业务逻辑
        TestDriveFormResponse response = TestDriveFormResponse.builder()
                .id("1")
                .name("abc")
                .build();
        return response;
    }

}

        九、编写HTTP Client测试文件

### 1.request不加密,response加密
GET http://localhost:8080/my/test1
Accept: */*
Cache-Control: no-cache
Content-Type: application/json

{
  "name":"abc",
  "age":10
}

### 2.request加密,response不加密
GET http://localhost:8080/my/test2
Accept: */*
Cache-Control: no-cache
Content-Type: application/json

{
  "data": "NwCtijSisVq2JKrlfj2PyKjggCKAupwyfYT+ZZcnJczhBBZ6Gwu5XXTRSDcignp5"
}


### 3.request和response都加密
GET http://localhost:8080/my/test3
Accept: */*
Cache-Control: no-cache
Content-Type: application/json

{
  "data": "NwCtijSisVq2JKrlfj2PyKjggCKAupwyfYT+ZZcnJczhBBZ6Gwu5XXTRSDcignp5"
}

### 4.request和response都不加密
GET http://localhost:8080/my/test4
Accept: */*
Cache-Control: no-cache
Content-Type: application/json

{
  "name":"abc",
  "age":10
}

### 5.request和response都加密 ==>>注解加在类上
GET http://localhost:8080/my/test5
Accept: */*
Cache-Control: no-cache
Content-Type: application/json

{
  "data": "NwCtijSisVq2JKrlfj2PyKjggCKAupwyfYT+ZZcnJczhBBZ6Gwu5XXTRSDcignp5"
}

###

         自行配置加解密秘钥串${white.box.enable:true}后,即可测试和验证。

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值