项目为提高接口调用安全性和保护敏感数据,往往会选择将请求和响应采取加密方式来处理。本文分享基于自定义注解实现白盒接口的操作实践。
一、自定义注解类和切面类
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}后,即可测试和验证。