说明
在多数的项目中,开发人员习惯将表记录的主键自增id通过接口暴露出去,如:接口-列表,返回数据中包含每行数据的id,删除和修改接口依照此id做删除与修改的条件,由于id是有规律可猜测的,非法用户就可以利用这个漏洞进行非法操作了
方案
利用切面对方法做参数做出参加密,入参解密
AES加密工具
内容
- 定义字段加密类型
public enum EncryptFieldType {
/**
* 加密,解密类型
*/
ENCRYPT("加密"),
DECRYPT("解密");
private String value;
EncryptFieldType(String value) {
this.value = value;
}
/**
* <p>Getter for the field <code>value</code>.</p>
*
* @return a {@link java.lang.String} object.
*/
public String getValue() {
return value;
}
/**
* <p>Setter for the field <code>value</code>.</p>
*
* @param value a {@link java.lang.String} object.
*/
public void setValue(String value) {
this.value = value;
}
}
- 定义加密字段工具
@Service
public class EncryptFieldUtil {
private SymmetricCrypto aes;
/**
* <p>Constructor for EncryptFieldUtil.</p>
*
* @param secretKey a {@link java.lang.String} object.
*/
public EncryptFieldUtil(@Value("${jjche.filter.encryption.field.secret-key}") String secretKey) {
aes = new SymmetricCrypto(SymmetricAlgorithm.AES, secretKey.getBytes());
}
/**
* <p>
* 加密字符串
* </p>
*
* @param value 待加密值
* @return /
*/
private String encryptStr(String value) {
return aes.encryptHex(value);
}
/**
* <p>
* 解密字符串
* </p>
*
* @param value 待解密值
* @return /
*/
private String decryptStr(String value) {
return aes.decryptStr(value);
}
/**
* <p>
* 加/解密字符串
* </p>
*
* @param value 字符串
* @param encryptFieldType 类型
* @return /
*/
public String handleEncryptStr(String value, EncryptFieldType encryptFieldType) {
if (encryptFieldType == EncryptFieldType.ENCRYPT) {
value = encryptStr(value);
} else {
value = decryptStr(value);
}
return value;
}
/**
* <p>
* 加/解密字符串集合
* </p>
*
* @param strList 字符串集合
* @param encryptFieldType 类型
* @return /
*/
public List<String> handleEncryptCollectionStr(List<String> strList, EncryptFieldType encryptFieldType) {
for (int i = 0; i < strList.size(); i++) {
String value = strList.get(i);
if (encryptFieldType == EncryptFieldType.ENCRYPT) {
value = encryptStr(value);
} else {
value = decryptStr(value);
}
strList.set(i, value);
}
return strList;
}
/**
* <p>
* 加/解密对象字段
* </p>
*
* @param o 对象,非基本数据类型
* @param encryptFieldType 类型
* @throws java.lang.IllegalAccessException if any.
*/
public void setObjectFieldEncrypt(Object o, EncryptFieldType encryptFieldType) throws IllegalAccessException {
if (o != null && !ClassUtil.isBasicType(o.getClass())) {
Field[] fields = o.getClass().getDeclaredFields();
for (Field field : fields) {
boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
if (hasSecureField) {
field.setAccessible(true);
String value = StrUtil.toString(field.get(o));
String encryptValue = "";
//解密
if (encryptFieldType == EncryptFieldType.DECRYPT) {
encryptValue = this.decryptStr(value);
} else {
encryptValue = this.encryptStr(value);
}
field.set(o, encryptValue);
}
}
}
}
/**
* <p>
* 加/解密对象字段集合
* </p>
*
* @param pojoList 非基本数据类型集合
* @param encryptFieldType 类型
* @throws java.lang.IllegalAccessException if any.
*/
public void setCollectionObjectFieldEncrypt(Collection<Object> pojoList,
EncryptFieldType encryptFieldType) throws IllegalAccessException {
if (CollUtil.isNotEmpty(pojoList)) {
for (Object o : pojoList) {
this.setObjectFieldEncrypt(o, encryptFieldType);
}
}
}
/**
* <p>
* 加密分页集合
* </p>
*
* @param myPage 分页集合
* @throws java.lang.IllegalAccessException if any.
*/
public void setEncryptMyPageRecords(Object myPage) throws IllegalAccessException {
// ClassUtil.method
Method mh = ReflectionUtils.findMethod(myPage.getClass(), "getRecords");
if (mh != null) {
Object obj = ReflectionUtils.invokeMethod(mh, myPage);
boolean isCollection = ClassUtil.isAssignable(Collection.class, obj.getClass());
if (obj != null && isCollection) {
Collection collection = (Collection) obj;
if (CollUtil.isNotEmpty(collection)) {
setCollectionObjectFieldEncrypt(collection, EncryptFieldType.ENCRYPT);
}
}
}
}
}
- 安全字段加密解密切面
@Order(Ordered.HIGHEST_PRECEDENCE)
@Aspect
@Component
public class EncryptFieldAop {
@Autowired
private EncryptFieldUtil encryptFieldUtil;
/**
* <p>annotationPointCut.</p>
*/
@Pointcut("@annotation(org.jjche.common.annotation.EncryptMethod)")
public void annotationPointCut() {
}
/**
* <p>around.</p>
*
* @param joinPoint a {@link org.aspectj.lang.ProceedingJoinPoint} object.
* @return a {@link java.lang.Object} object.
* @throws java.lang.Throwable if any.
*/
@Around("annotationPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object responseObj = null;
Throwable throwable = null;
try {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
Method realMethod = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), method.getParameterTypes());
Annotation[][] parameterAnnotations = realMethod.getParameterAnnotations();
Object[] args = joinPoint.getArgs();
//是否存在参数注解
if (ArrayUtil.isNotEmpty(parameterAnnotations) && ArrayUtil.isNotEmpty(args)) {
for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] parameterAnnotation = parameterAnnotations[i];
//参数是否包含加密注解
boolean isParamEncryptField = ArrayUtil.isNotEmpty(parameterAnnotation) && ClassUtil.isAssignable(EncryptField.class, parameterAnnotation[0].getClass());
if (isParamEncryptField) {
Object arg = args[i];
args[i] = handleEncrypt(arg, EncryptFieldType.DECRYPT);
}
}
}
responseObj = joinPoint.proceed(args);
EncryptMethod encryptMethod = realMethod.getAnnotation(EncryptMethod.class);
//是否加密返回数据
if (encryptMethod != null && encryptMethod.returnVal()) {
responseObj = handleEncrypt(responseObj, EncryptFieldType.ENCRYPT);
}
} catch (Exception t) {
throwable = t;
//记录日志错误不要影响业务
StaticLog.error("log encryptField parse exception:{}", ThrowableUtil.getStackTrace(t));
}
if (throwable != null) {
throw throwable;
}
return responseObj;
}
/**
* <p>
* 处理出参加密,入参解密
* </p>
*
* @param requestObj 出参/入参
* @return /
*/
private Object handleEncrypt(Object requestObj, EncryptFieldType encryptFieldType) throws IllegalAccessException {
if (Objects.isNull(requestObj)) {
return null;
}
Class myPageClass = ClassUtil.loadClass("org.jjche.mybatis.param.MyPage");
boolean isMyPage = ClassUtil.isAssignable(myPageClass, requestObj.getClass());
//MyPage出参特殊处理
if (isMyPage && encryptFieldType == EncryptFieldType.ENCRYPT) {
Object myPage = Convert.convert(myPageClass, requestObj);
encryptFieldUtil.setEncryptMyPageRecords(myPage);
} else {
boolean isStr = ClassUtil.isAssignable(String.class, requestObj.getClass());
boolean isCollection = ClassUtil.isAssignable(Collection.class, requestObj.getClass());
boolean isCollectionStr = false;
boolean isCollectionPojo = false;
if (isCollection) {
Predicate<Object> strPredicate = new Predicate<Object>() {
@Override
public boolean test(Object o) {
return ClassUtil.isAssignable(String.class, o.getClass());
}
};
Predicate<Object> pojoPredicate = new Predicate<Object>() {
@Override
public boolean test(Object o) {
return !ClassUtil.isBasicType(o.getClass());
}
};
isCollectionStr = CollUtil.contains((Collection) requestObj, strPredicate);
isCollectionPojo = CollUtil.contains((Collection) requestObj, pojoPredicate);
}
boolean isPojo = !ClassUtil.isBasicType(requestObj.getClass()) && !isCollectionPojo;
//参数类型是String
if (isStr) {
String encryptValue = StrUtil.toString(requestObj);
requestObj = encryptFieldUtil.handleEncryptStr(encryptValue, encryptFieldType);
}//参数类型是Collection<String>
else if (isCollectionStr) {
List<String> strList = Convert.toList(String.class, requestObj);
requestObj = encryptFieldUtil.handleEncryptCollectionStr(strList, encryptFieldType);
}
//参数类型是POJO
else if (isPojo) {
encryptFieldUtil.setObjectFieldEncrypt(requestObj, encryptFieldType);
}//参数类型是Collection<POJO>
else if (isCollectionPojo) {
List<Object> pojoList = Convert.toList(Object.class, requestObj);
encryptFieldUtil.setCollectionObjectFieldEncrypt(pojoList, encryptFieldType);
requestObj = pojoList;
}
}
return requestObj;
}
}
注意事项
参与加解密的字段类型必须是String 支持的类型有:String、List、POJO、List MyPage仅支持出参类型 不会处理嵌套list
使用方法
@EncryptMethod,参与加/解密的方法上必须增加,参数returnVal默认false不加密出参,为true时加密出参
@EncryptField 方法的参数,对象的字段上增加入参示例
/**
* <p>
* 根据ID查询
* </p>
*
* @param id ID
* @return StudentVO 对象
*/
@EncryptMethod
public StudentVO getVoById(@EncryptField String id, @EncryptField List<String> ids, @EncryptField StudentDTO dto, @EncryptField List<StudentDTO> dtos) {
StudentDO studentDO = this.getById(id);
Assert.notNull(studentDO, "记录不存在或权限不足");
return this.studentMapStruct.toVO(studentDO);
}
@Data
public class StudentVO implements Serializable {
@ApiModelProperty(value = "姓名")
@EncryptField
private String name;
}
出参示例
@EncryptMethod(returnVal = true)
public MyPage<StudentVO> pageQuery(PageParam page, StudentSortEnum sort, StudentQueryCriteriaDTO query) {
LambdaQueryWrapper queryWrapper = MybatisUtil.assemblyLambdaQueryWrapper(query, SortEnum.ID_DESC);;
return this.baseMapper.pageQuery(page, queryWrapper);
}
非注解处理
可使用EncryptFieldUtil类单独使用
AES加密配置属性jjche.filter.encryption.field.secret-key