0x0 背景
在项目中,权限校验服务是一个独立的组件,因此采用传统的shiro框架或者spring security框架和我们的项目不太适配;
为了简化权限校验业务,本文使用spring的aop,利用注解和spel表达式,实现了精细的权限控制。
0x1 代码
1.注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
/**
* SPEL表达式
*/
String value() default "";
/**
* 权限码
*/
String[] permissions() default {};
/**
* 资源类型,默认为区域资源
*/
String resourceType() default "region";
}
2.切面
@Component
@Aspect
@Slf4j
public class PermissionAspect {
/**
* spel解析器
*/
private final SpelExpressionParser parser= new SpelExpressionParser();
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* spel缓存
*/
private final ConcurrentHashMap<Method, Expression> expressionMap = new ConcurrentHashMap<>(256);
private final Map<String, Method> methodMap = new HashMap<>(16);
private final BeanFactory beanFactory;
private final PermissionUtils permissionUtils;
/**
* 初始化
*
* @param beanFactory spring上下文
* @param permissionUtils 用户实现的权限校验工具类
*/
public PermissionAspect(BeanFactory beanFactory, PermissionUtils permissionUtils) {
this.beanFactory = beanFactory;
this.permissionUtils = permissionUtils;
//用于将用户自定义的静态方法注册到el上下文,注意方法不能重载!
Method[] methods = permissionUtils.getClass().getDeclaredMethods();
for (Method method : methods) {
int modifiers = method.getModifiers();
if (Modifier.isStatic(modifiers)) {
methodMap.put(method.getName(), method);
}
}
}
@Before("@annotation(permission)")
@SuppressWarnings("unchecked")
public void check(JoinPoint joinPoint, Permission permission) throws Throwable {
Method targetMethod = getTargetMethod(joinPoint);
Expression expression = getExpression(targetMethod, permission);
EvaluationContext evaluationContext = getEvaluationContext(joinPoint, targetMethod);
Object expressionValue = expression.getValue(evaluationContext);
if (expressionValue == null) {
throw new RuntimeException("expressionValue is null, please check your el expression");
}
boolean hasPermission = false;
//如果是函数校验,返回boolean
if (expressionValue instanceof Boolean) {
hasPermission = (Boolean) expressionValue;
}
//如果是传入了id或id list,采用其他方式校验
else if (expressionValue instanceof String) {
hasPermission = permissionUtils.hasPermission(expressionValue.toString(), permission.resourceType());
}
else if (expressionValue instanceof Collection) {
hasPermission = permissionUtils.hasAllPermission((Collection<String>) expressionValue, permission.resourceType());
}
if (!hasPermission) {
throw new PermissionException();
}
}
/**
* 从缓存中获取spel编译表达式
*
* @param method method
* @param permission 注解
* @return SpelExpression
*/
private Expression getExpression(Method method, Permission permission) {
Expression expression = expressionMap.get(method);
if (expression != null) {
return expression;
}
String value = permission.value();
return expressionMap.computeIfAbsent(method, k -> parser.parseRaw(value));
}
/**
* 获取Spel上下文
*
* @param joinPoint joinPoint
* @param method method
* @return spel上下文
*/
EvaluationContext getEvaluationContext(JoinPoint joinPoint, Method method) {
Object[] args = joinPoint.getArgs();
PermissionRootObject root = new PermissionRootObject(args, permissionUtils);
MethodBasedEvaluationContext evaluationContext =
new MethodBasedEvaluationContext(root, method, args, parameterNameDiscoverer);
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
if (methodMap.isEmpty()) {
return evaluationContext;
}
//将用户自定义的方法注册到上下文
methodMap.forEach(evaluationContext::registerFunction);
return evaluationContext;
}
/**
* 获取目标方法
*
* @param joinPoint join point
* @return 目标方法
*/
private Method getTargetMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
return methodSignature.getMethod();
}
}
3.SPEL根对象
public interface PermissionUtils {
boolean hasPermission(String resourceId, String resourceType, String... permissions);
boolean hasRegionPermission(String regionId);
boolean hasAllRegionPermission(Collection<String> idList);
boolean hasAllPermission(Collection<String> idList, String resourceType, String... permissions);
}
@Data
public class PermissionRootObject implements PermissionUtils {
private Object[] args;
private PermissionUtils permissionUtils;
public PermissionRootObject(Object[] args, PermissionUtils permissionUtils) {
this.args = args;
this.permissionUtils = permissionUtils;
}
@Override
public boolean hasPermission(String resourceId, String resourceType, String... permissions) {
return permissionUtils.hasPermission(resourceId, resourceType, permissions);
}
@Override
public boolean hasRegionPermission(String regionId) {
return permissionUtils.hasRegionPermission(regionId);
}
@Override
public boolean hasAllRegionPermission(Collection<String> idList) {
return permissionUtils.hasAllRegionPermission(idList);
}
@Override
public boolean hasAllPermission(Collection<String> idList, String resourceType, String... permissions) {
return permissionUtils.hasAllPermission(idList, resourceType, permissions);
}
}
0x2 使用案例
首先要实现一个PermissionUtils的spring bean,这个根据用户的实际情况来开发;
然后再对应的方法上加上我们自定义的注解,如下所示:
@RestController
@RequestMapping("security")
public class SecurityController {
/**
* 自定义静态方法校验
*/
@GetMapping("/test")
@Permission("#test(#id)")
public String test1(String id) {
return "query";
}
/**
* 校验某种资源
*/
@GetMapping("/query")
@Permission(value = "#idList", resourceType = "camera")
public String test3(List<String> idList) {
return "query";
}
/**
* 使用根对象的方法进行校验
*/
@GetMapping("/update")
@Permission("hasRegionPermission(#id)")
public String test2(String id, String name) {
return "update";
}
}