需求场景
附件内容都存在华为云OBS中,数据库中只保存了OBS文件的原始url和文件名,业务接口需要额外返回签名后的可访问url以供前端进行下载/显示文件内容,而在所有业务接口都自行做这个url签名的处理又比较麻烦,于是希望通过在VO类中,给需要签名的url字段添加一个自定义注解,扫描到这个注解之后,自动添加一个新字段,新字段内容为签名后的url地址
实现方式:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于指定api接口是否需要进行url地址签名(避免不需要处理url签名的api接口进入AOP)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UrlNeedSign {
}
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 UrlSign {
/**
* 文件名的属性名,从这个属性中获取值设置为下载的文件名
* @return
*/
String fileNameProp() default "";
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.cglib.beans.BeanGenerator;
import org.springframework.cglib.beans.BeanMap;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public final class PropertyAppendUtil {
private static final class DynamicBean {
private Object target;
private BeanMap beanMap;
private DynamicBean(Class superclass, Map<String, Class> propertyMap) {
this.target = generateBean(superclass, propertyMap);
this.beanMap = BeanMap.create(this.target);
}
private void setValue(String property, Object value) {
beanMap.put(property, value);
}
private Object getValue(String property) {
return beanMap.get(property);
}
private Object getTarget() {
return this.target;
}
/**
* 根据属性生成对象
*/
private Object generateBean(Class superclass, Map<String, Class> propertyMap) {
BeanGenerator generator = new BeanGenerator();
if (null != superclass) {
generator.setSuperclass(superclass);
}
BeanGenerator.addProperties(generator, propertyMap);
return generator.create();
}
}
public static Object generate(Object dest, Map<String, Object> newValueMap) throws InvocationTargetException, IllegalAccessException {
//1.获取原对象的字段数组
PropertyDescriptor[] descriptorArr = BeanUtils.getPropertyDescriptors(dest.getClass());
//2.遍历原对象的字段数组,并将其封装到Map
Map<String, Class> oldKeyMap = new HashMap<>();
for (PropertyDescriptor it : descriptorArr) {
//排除掉.getClass方法
if (!"class".equalsIgnoreCase(it.getName())) {
oldKeyMap.put(it.getName(), it.getPropertyType());
newValueMap.put(it.getName(), it.getReadMethod().invoke(dest));
}
}
//3.将扩展字段Map合并到原字段Map中
newValueMap.forEach((k, v) -> {
if(v!=null){
oldKeyMap.putIfAbsent(k, v.getClass());
}else{
oldKeyMap.putIfAbsent(k,Object.class);
}
});
//4.根据新的字段组合生成子类对象
DynamicBean dynamicBean = new DynamicBean(dest.getClass(), oldKeyMap);
//5.放回合并后的属性集合
newValueMap.forEach((k, v) -> {
try {
dynamicBean.setValue(k, v);
} catch (Throwable e) {
log.error("动态添加字段【"+k+"】出错", e);
}
});
return dynamicBean.getTarget();
}
}
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.test.domain.BaseResult;
import com.test.domain.PageResult;
import com.test.utils.HuaweiObsUtil;
import com.test.utils.JSONUtil;
import com.test.annotation.UrlSign;
import com.test.utils.PageUtil;
import com.test.utils.PropertyAppendUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
/**
* url鉴权切面
* 1:支持单个实体:属性上加@UrlAuthentication注解,返回值中包含url
* 2:支持list:list和属性值都需要加@UrlAuthentication注解,返回值中包含url
* 3:支持嵌套类:同list
* 4:支持嵌套list类:同list
*/
@Slf4j
@Aspect
@Component
public class UrlAuthenticationAspect {
private static final String EXT_URL_FIELD_NAME = "SignUrl";
private static final String EXT_URL_FIELD_NAME_LIST = "SignUrlList";
private static final long expireSecond = 3600L;
@Around("@annotation(com.test.annotation.NeedAuthentication)")
public Object addUrlSignature(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("进入切面");
Object result = joinPoint.proceed();
if (result instanceof BaseResult == false) {
log.warn("接口返回值不是BaseResult类型,无法进行url鉴权处理");
return result;
}
BaseResult baseResult = (BaseResult) result;
try {
Object data = baseResult.getData();
if (data instanceof PageResult) {
PageResult<?> pr = ((PageResult<?>) data);
List<Object> newRecords = new ArrayList<>();
for (Object o : pr.getList()) {
try {
newRecords.add(processObjectFields(o));
} catch (Exception e) {
log.error(StrUtil.format("解析异常:{}", JSONUtil.toJsonStr(baseResult)), e);
throw e;
}
}
baseResult.setData(PageUtil.convertPageResultForList(pr, sourceList -> newRecords));
} else if (data instanceof Collection) {
Collection<?> cdata = (Collection<?>) data;
Collection<Object> newData = new ArrayList<>();
for (Object item : cdata) {
newData.add(processObjectFields(item));
}
baseResult.setData(newData);
} else {
// 单个对象处理
baseResult.setData(processObjectFields(data));
}
} catch (Exception e) {
log.error("解析异常:{}", JSONUtil.toJsonStr(baseResult));
throw e;
}
log.info("切面结束");
return result;
}
private Object processObjectFields(Object obj) throws IllegalAccessException, InvocationTargetException {
Map<String, Object> addValue = new HashMap<>();
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(UrlSign.class)) {
UrlSign urlSign = field.getAnnotation(UrlSign.class);
String fileNameProp = urlSign.fileNameProp();
field.setAccessible(true);
Object value = field.get(obj);
if (value == null) {
addValue.put(field.getName() + EXT_URL_FIELD_NAME, null);
} else if (value instanceof String) {
String url4GetUrl = null;
if (StrUtil.isNotBlank(fileNameProp)) {
Object fileName = ReflectUtil.getFieldValue(obj, fileNameProp);
String fileNameStr = fileName == null ? "" : String.valueOf(fileName);
url4GetUrl = HuaweiObsUtil.buildUrlCustomFileName4GetUrl((String) value, fileNameStr, expireSecond);
} else {
url4GetUrl = HuaweiObsUtil.buildUrl4GetUrl((String) value, expireSecond);
}
addValue.put(field.getName() + EXT_URL_FIELD_NAME, url4GetUrl);
} else if (value instanceof Collection) {
Collection oldColl = (Collection) value;
if (!CollUtil.isEmpty(oldColl)) {
boolean isString = oldColl.stream().findFirst().get() instanceof String;
if (isString) {
List<String> fileNameColl = new ArrayList<>();
if (StrUtil.isNotBlank(fileNameProp)) {
Object fileName = ReflectUtil.getFieldValue(obj, fileNameProp);
if (fileName instanceof Collection) {
fileNameColl.addAll((Collection<String>) fileName);
} else if (fileName instanceof String) {
String fileNameStr = (String) fileName;
for (int i = 0; i < oldColl.size(); i++) {
fileNameColl.add(fileNameStr);
}
} else {
log.warn("无法处理的文件名类型" + fileName.getClass().getName());
for (int i = 0; i < oldColl.size(); i++) {
fileNameColl.add("");
}
}
}
List<String> newUrlColl = new ArrayList<>();
int i = 0;
for (Object nestedItem : (Collection<?>) value) {
String sourceUrl = (String) nestedItem;
String sourceFileName = null;
if (CollUtil.isNotEmpty(fileNameColl)) {
sourceFileName = fileNameColl.get(i);
}
if (StrUtil.isNotBlank(sourceFileName)) {
newUrlColl.add(HuaweiObsUtil.buildUrlCustomFileName4GetUrl(sourceUrl, sourceFileName, expireSecond));
} else {
newUrlColl.add(HuaweiObsUtil.buildUrl4GetUrl(sourceUrl, expireSecond));
}
i++;
}
addValue.put(field.getName() + EXT_URL_FIELD_NAME_LIST, newUrlColl);
} else {
List newColl = new ArrayList();
for (Object nestedItem : (Collection<?>) value) {
newColl.add(processObjectFields(nestedItem));
}
oldColl.clear();
oldColl.addAll(newColl);
}
}
} else if (!isPrimitiveOrWrapper(value.getClass())) {
Object newValue = processObjectFields(value);
field.set(obj, newValue);
}
}
}
return PropertyAppendUtil.generate(obj, addValue);
}
/**
* 判断是否为基本类型、包装类或标准库类,避免递归进入这些类型
*/
private boolean isPrimitiveOrWrapper(Class<?> clazz) {
return clazz.isPrimitive() || clazz.getName().startsWith("java.");
}
}
使用示例:
import com.test.annotation.UrlSign;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class TestVO {
@UrlSign
private String testFile1Url;
@UrlSign(fileNameProp = "testFile2Name")
private String testFile2Url;
private String testFile2Name;
@UrlSign
private List<String> testFileList1;
@UrlSign(fileNameProp = "testFileNameList2")
private List<String> testFileList2;
private List<String> testFileNameList2;
@UrlSign
private List<TestInnerVO> innerVOList;
@UrlSign
private TestInner2VO inner2VO;
@Getter
@Setter
public static class TestInnerVO{
@UrlSign(fileNameProp = "innerFile1UrlName")
private String innerFile1Url;
private String innerFile1UrlName;
@UrlSign(fileNameProp = "innerFileNameList")
private List<String> innerFileUrlList;
private List<String> innerFileNameList;
}
@Getter
@Setter
public static class TestInner2VO{
@UrlSign(fileNameProp = "innerFileName")
private String innerFileUrl;
private String innerFileName;
}
}
import com.test.domain.BaseResult;
import com.test.annotation.UrlNeedSign;
import com.test.domain.vo.TestVO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/inner/test")
public class TestUrlController {
@PostMapping("test")
@UrlNeedSign
public BaseResult<TestVO> test() {
TestVO testVO=new TestVO();
testVO.setTestFile1Url("https://test.obs.cn-south-1.myhuaweicloud.com/testFile1");
testVO.setTestFile2Url("https://test.obs.cn-south-1.myhuaweicloud.com/testFile2");
testVO.setTestFile2Name("测试文件2.txt");
testVO.setTestFileList1(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testFile3","https://test.obs.cn-south-1.myhuaweicloud.com/testFile4"));
testVO.setTestFileList2(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testFile5","https://test.obs.cn-south-1.myhuaweicloud.com/testFile6"));
testVO.setTestFileNameList2(Arrays.asList("测试文件5.txt","测试文件6.txt"));
List<TestVO.TestInnerVO> innerVOList=new ArrayList<>();
TestVO.TestInnerVO innerVO1=new TestVO.TestInnerVO();
innerVO1.setInnerFile1Url("https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File1");
innerVO1.setInnerFile1UrlName("测试内部VO1文件1.txt");
innerVO1.setInnerFileUrlList(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File2","https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File3"));
innerVO1.setInnerFileNameList(Arrays.asList("测试内部VO1文件2.txt","测试内部VO1文件3.txt"));
TestVO.TestInnerVO innerVO2=new TestVO.TestInnerVO();
innerVO2.setInnerFile1Url("https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File1");
innerVO2.setInnerFile1UrlName("测试内部VO2文件1.txt");
innerVO2.setInnerFileUrlList(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File2","https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File3"));
innerVO2.setInnerFileNameList(Arrays.asList("测试内部VO2文件2.txt","测试内部VO2文件3.txt"));
innerVOList.add(innerVO1);
innerVOList.add(innerVO2);
testVO.setInnerVOList(innerVOList);
TestVO.TestInner2VO inner2VO=new TestVO.TestInner2VO();
inner2VO.setInnerFileUrl("https://test.obs.cn-south-1.myhuaweicloud.com/testInnerVO2File1");
inner2VO.setInnerFileName("测试内部单VO文件.txt");
testVO.setInner2VO(inner2VO);
return BaseResult.success(testVO);
}
}
测试结果:
{
"code": "success",
"message": null,
"data": {
"testFile1Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile1",
"testFile2Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile2",
"testFile2Name": "测试文件2.txt",
"testFileList1": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile3",
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile4"
],
"testFileList2": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile5",
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile6"
],
"testFileNameList2": [
"测试文件5.txt",
"测试文件6.txt"
],
"innerVOList": [
{
"innerFile1Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File1",
"innerFile1UrlName": "测试内部VO1文件1.txt",
"innerFileUrlList": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File2",
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File3"
],
"innerFileNameList": [
"测试内部VO1文件2.txt",
"测试内部VO1文件3.txt"
],
"innerFileUrlListSignUrlList": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File2?attname=测试内部VO1文件2.txt&AccessKeyId=testak&Expires=1725096532&Signature=KogXKMbij%2FF4MfV%2FOJdIe7Lq9a4%3D",
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File3?attname=测试内部VO1文件3.txt&AccessKeyId=testak&Expires=1725096532&Signature=XU1TbeJxpKFHP8x7w%2BlP14sm7lw%3D"
],
"innerFile1UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File1?attname=测试内部VO1文件1.txt&AccessKeyId=testak&Expires=1725096532&Signature=VdzxcyqI3fYc1o30eLeYrRIUdS8%3D"
},
{
"innerFile1Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File1",
"innerFile1UrlName": "测试内部VO2文件1.txt",
"innerFileUrlList": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File2",
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File3"
],
"innerFileNameList": [
"测试内部VO2文件2.txt",
"测试内部VO2文件3.txt"
],
"innerFileUrlListSignUrlList": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File2?attname=测试内部VO2文件2.txt&AccessKeyId=testak&Expires=1725096532&Signature=Tnr7LaigTrx%2FDMlzPOG%2BvFfcXsE%3D",
"https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File3?attname=测试内部VO2文件3.txt&AccessKeyId=testak&Expires=1725096532&Signature=uNK6V8V3ZKnvtHOXqZpWy%2FoCvxc%3D"
],
"innerFile1UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File1?attname=测试内部VO2文件1.txt&AccessKeyId=testak&Expires=1725096532&Signature=PWxOUP1WuLYFaYZyx%2BVso1%2B%2BE%2Bg%3D"
}
],
"inner2VO": {
"innerFileUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInnerVO2File1",
"innerFileName": "测试内部单VO文件.txt",
"innerFileUrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInnerVO2File1?attname=测试内部单VO文件.txt&AccessKeyId=testak&Expires=1725096532&Signature=X3uEPTw1LUj3VsrX9MJlUyOrPjo%3D"
},
"testFileList1SignUrlList": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile3?AccessKeyId=testak&Expires=1725096532&Signature=txZ80twXwEQJg9b29lpYliLulX8%3D",
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile4?AccessKeyId=testak&Expires=1725096532&Signature=mLFWq%2Fuelh3439fqi%2BqIFodB52U%3D"
],
"testFile2UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile2?attname=测试文件2.txt&AccessKeyId=testak&Expires=1725096532&Signature=BDFn6ikqAQfAL8n4MHiRXQzosBk%3D",
"testFileList2SignUrlList": [
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile5?attname=测试文件5.txt&AccessKeyId=testak&Expires=1725096532&Signature=1Zap0vq5xQXXK%2FfRjnMuf4d9a88%3D",
"https://test.obs.cn-south-1.myhuaweicloud.com/testFile6?attname=测试文件6.txt&AccessKeyId=testak&Expires=1725096532&Signature=DjgupXge07HOvv56bXOKxf7T%2FNw%3D"
],
"testFile1UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile1?AccessKeyId=testak&Expires=1725096532&Signature=tE057WHVooZpz3K8AipDXrZmWvQ%3D"
}
}