若依的权限校验实现原理就是AOP+自定义注解,代码并不多,debug跟一遍之后很容易理解,详细流程整理如下
用到的类
- 注解类RequiresLogin、RequiresPermissions、RequiresRoles,分别用于登录认证、权限认证和角色认证
- 切面类PreAuthorizeAspect,基于 Spring Aop 的注解鉴权
- 被代理类SysJobController,被代理类就是添加注解的方法所在的类,可以是任意一个类
链路跟踪
在需要鉴权的方法上添加对应的注解
@RequiresPermissions(“monitor:job:list”)表示需要在该方法执行前进行权限认证,参数即具体权限
/**
* 查询定时任务列表
*/
@RequiresPermissions("monitor:job:list")
@GetMapping("/list")
public TableDataInfo list(SysJob sysJob) {
startPage();
List<SysJob> list = jobService.selectJobList(sysJob);
return getDataTable(list);
}
执行切面类
package com.zhy.common.security.aspect;
import java.lang.reflect.Method;
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.stereotype.Component;
import com.zhy.common.security.annotation.RequiresLogin;
import com.zhy.common.security.annotation.RequiresPermissions;
import com.zhy.common.security.annotation.RequiresRoles;
import com.zhy.common.security.auth.AuthUtil;
/**
* 基于 Spring Aop 的注解鉴权
*
* @author kong
*/
@Aspect
@Component
public class PreAuthorizeAspect
{
/**
* 构建
*/
public PreAuthorizeAspect()
{
}
/**
* 定义AOP签名 (切入所有使用鉴权注解的方法)
*/
public static final String POINTCUT_SIGN = " @annotation(com.zhy.common.security.annotation.RequiresLogin) || "
+ "@annotation(com.zhy.common.security.annotation.RequiresPermissions) || "
+ "@annotation(com.zhy.common.security.annotation.RequiresRoles)";
/**
* 声明AOP签名
*/
@Pointcut(POINTCUT_SIGN)
public void pointcut()
{
}
/**
* 环绕切入
*
* @param joinPoint 切面对象
* @return 底层方法执行后的返回值
* @throws Throwable 底层方法抛出的异常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable
{
// 注解鉴权
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
checkMethodAnnotation(signature.getMethod());
try
{
// 执行原有逻辑
Object obj = joinPoint.proceed();
return obj;
}
catch (Throwable e)
{
throw e;
}
}
/**
* 对一个Method对象进行注解检查
*/
public void checkMethodAnnotation(Method method)
{
// 校验 @RequiresLogin 注解
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if (requiresLogin != null)
{
AuthUtil.checkLogin();
}
// 校验 @RequiresRoles 注解
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
if (requiresRoles != null)
{
AuthUtil.checkRole(requiresRoles);
}
// 校验 @RequiresPermissions 注解
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if (requiresPermissions != null)
{
AuthUtil.checkPermi(requiresPermissions);
}
}
}
debug进到around方法
signature.getMethod()获取到添加注解的方法
进到checkMethodAnnotation方法
拿到注解所在方法后首先判断该方法上添加了哪些注解,可以有一个或多个,此处只添加了@RequiresPermissions注解
进入校验权限认证的方法
权限验证工具类跟进到权限验证逻辑实现类
首先是调用SecurityContextHolder.setPermission
debug进入set方法
这里ROLE_PERMISSION是在SecurityConstants即权限相关通用常量类中定义的,该类中的其他属性如DETAILS_USER_ID、DETAILS_USERNAME、USER_KEY、LOGIN_USER等都是在用户登录时进行赋值
进入set方法,这里的getLocalMap最终获取的是TransmittableThreadLocal类型的THREAD_LOCAL集合,该集合中也存放了上面说的SecurityConstants类中的其他属性。TransmittableThreadLocal相比ThreadLocal而言优点在于能够处理处理父子线程变量不能共用的情况,ThreadLocal是跟当前线程挂钩的,所以脱离当前线程它就起不了作用
回到checkPermi方法中,这里判断为true,因为@RequiresPermissions注解中的logical默认值就是Logical.AND枚举字段,Logical枚举类有两个属性,AND和OR,表示验证用户所有的权限还是部分权限,继续执行checkPermiAnd方法
checkPermiAnd方法进来后首先获取当前账号的权限列表,debug进去
可以看到这里获取到admin账号的权限是"* : * : *",这个值也是登录流程中进行赋值
现在账号权限有了,执行业务逻辑需要的权限也有了,接下来就是进行比对了,如果通过那么AOP的任务就完成了,可以继续执行业务逻辑,不通过的会会抛出异常
debug进入hasPermi方法,来看看是怎么进行比对的,这里传进来的两个参数authorities和permission就是需要比对的值,值分别为"* : * : *“和"monitor:job:list”,可以看到这里用到了java8Stream中提供的filter过滤器,StringUtils::hasText过滤条件使用了双冒号运算符,表示遍历authorities集合中的元素作为参数调用 StringUtils工具类中的 hasText方法。anyMatch()表示进行匹配,其中x表示authorities权限列表中的元素,ALL_PERMISSION.contains(x)表示在 ALL_PERMISSION字符串中是否存在字符串x,PatternMatchUtils.simpleMatch(x, permission)) 表示 permission 中是否存在与 x 相匹配的子字符串。这里用到了或运算,由于ALL_PERMISSION是个字符串,值就是"* : * : *",所以ALL_PERMISSION.contains(x)永远为true,此处会发生或运算短路,意味着PatternMatchUtils.simpleMatch(x, permission)) 不会执行,所以return true
到此整个权限验证逻辑执行完毕