一、自定義注解
先聊聊這個需求,我需要根據使用者的權限對資料進行一些處理,但是痛點在哪裡呢?使用者的權限是在請求的時候知道的,我怎麼把使用者的權限傳遞給處理規則呢?想了以下幾種方案:
- Mybatis 攔截器:如果你的權限參數可以滲透到 Dao 層,那麼這是最好的處理方式,直接在 Dao 層資料傳回的時候,根據權限做資料處理。
- Dubbo 過濾器:如果 Dao 層沒辦法實作的話,隻好考慮在 service 層做資料處理了。
- ResponseBodyAdvice :要是 service 層也沒辦法做到,隻能在通路層資料傳回的時候,根據權限做資料處理。(以下介紹的正是這種方式)
那麼現在有個難點就是:我怎麼把 request 的權限參數傳遞到 response 中呢?當然可以在 Spring 攔截器中處理,但是我不想把這段代碼侵入到完整的鑒權邏輯中。突然想到,我能不能像 spring-data-redis 中 @Cacheable 一樣,利用注解和 SpEL 表達式動态的傳遞權限參數呢?然後在 ResponseBodyAdvice 讀取這個注解的權限參數,進而對資料進行處理。
首先,我們需要有個自定義注解,它有兩個參數:key 表示 SpEL 表達式;userType 表示權限參數。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseSensitiveOverride {
/**
* SPEL 表達式
*
* @return
*/
String key() default "";
/**
* 1:主賬号、2:子賬号
*/
int userType() default 1;
}
然後,把這個注解放在路由位址上,key 寫入擷取權限參數的 SpEL 表達式:
@ResponseSensitiveOverride(key = "#driverPageParam.getUserType()")
@RequestMapping(value = "/queryPage", method = RequestMethod.POST)
public ResponseData<PageVo<AdminDriverVo>> queryPage(@RequestBody AdminDriverPageParam driverPageParam) {
return driverService.queryPageAdmin(driverPageParam);
}
二、SpEl + AOP 注解指派
現在 SpEL 表達式是有了,怎麼把 SpEL 表達式的結果指派給注解的 userType 參數呢?這就需要用 Spring AOP 、Java 反射 和 動态代理 的知識。
@Aspect
@Component
public class SensitiveAspect {
private SpelExpressionParser spelParser = new SpelExpressionParser();
/**
* 傳回通知
*/
@AfterReturning("@annotation(com.yungu.swift.base.model.annotation.ResponseSensitiveOverride) && @annotation(sensitiveOverride)")
public void doAfter(JoinPoint joinPoint, ResponseSensitiveOverride sensitiveOverride) throws Exception {
//擷取方法的參數名和參數值
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
List<String> paramNameList = Arrays.asList(methodSignature.getParameterNames());
List<Object> paramList = Arrays.asList(joinPoint.getArgs());
//将方法的參數名和參數值一一對應的放入上下文中
EvaluationContext ctx = new StandardEvaluationContext();
for (int i = 0; i < paramNameList.size(); i++) {
ctx.setVariable(paramNameList.get(i), paramList.get(i));
}
// 解析SpEL表達式擷取結果
String value = spelParser.parseExpression(sensitiveOverride.key()).getValue(ctx).toString();
//擷取 sensitiveOverride 這個代理執行個體所持有的 InvocationHandler
InvocationHandler invocationHandler = Proxy.getInvocationHandler(sensitiveOverride);
// 擷取 invocationHandler 的 memberValues 字段
Field hField = invocationHandler.getClass().getDeclaredField("memberValues");
// 因為這個字段是 private final 修飾,是以要打開權限
hField.setAccessible(true);
// 擷取 memberValues
Map memberValues = (Map) hField.get(invocationHandler);
// 修改 value 屬性值
memberValues.put("userType", Integer.parseInt(value));
}
}
通過這種方式,我們就實作了為注解動态指派。
三、ResponseBodyAdvice 處理資料
現在要做的事情就是在 ResponseBody 資料傳回前,對資料進行攔截,然後讀取注解上的權限參數,進而對資料進行處理,這裡使用的是 SpringMVC 的 ResponseBodyAdvice 來實作:
@Slf4j
@RestControllerAdvice
@Order(-1)
public class ResponseBodyAdvice implements org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return SysUserDto.USER_TYPE_PRIMARY;
}
};
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
if (returnType.hasMethodAnnotation(ResponseSensitiveOverride.class)) {
ResponseSensitiveOverride sensitiveOverride = returnType.getMethodAnnotation(ResponseSensitiveOverride.class);
threadLocal.set(sensitiveOverride.userType());
return true;
}
return false;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body != null && SysUserDto.USER_TYPE_SUB.equals(threadLocal.get())) {
// 業務處理
}
return body;
}
}
題外話,其實我最後還是擯棄了這個方案,選擇了 Dubbo 過濾器的處理方式,為什麼呢?因為在做資料導出的時候,這種方式沒辦法對二進制流進行處理呀!汗~ 但是該方案畢竟耗費了我一個下午的心血,還是在此記錄一下,可能有它更好的适用場景!