1. 框架结构
拦截器
aop切面
注解
登录逻辑工具
异常处理handler
令牌服务
配置
拦截器
HeaderInterceptor: 自定义头拦截器,将header 数据封装到线程变量中方便获得。 同时会验证当前用户有效期自动刷新有效 期
public class HeaderInterceptor implements AsyncHandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (!(handler instanceof HandlerMethod))
{
return true;
}
//对应网关中的AuthFilter中将jwt令牌中的claims信息存入request header的操作,这里把用户信息从header中取出来,并且存入该线程的SecurityContextHolder
//SecurityContextHolder内部的核心储存是一个TransmittableThreadLocal(ThreadLocal的特殊形式)
SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
//这一步的底层操作,仍然是利用ServletUtils从requestHeader中获取token
String token = SecurityUtils.getToken();
//开始进入权限系统的操作流程,首先判断令牌是否为空
if (StringUtils.isNotEmpty(token))
{
//利用token,解析出当前的登录用户是谁
LoginUser loginUser = AuthUtil.getLoginUser(token);
if (StringUtils.isNotNull(loginUser))
{
//这一步是要判断该用户的登陆状态是否已经失效了
AuthUtil.verifyLoginUserExpire(loginUser);
//最后把登录用户信息,存入本线程的上下文环境
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception
{
SecurityContextHolder.remove();
}
}
public class SecurityContextHolder
{
private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void set(String key, Object value)
{
Map<String, Object> map = getLocalMap();
map.put(key, value == null ? StringUtils.EMPTY : value);
}
public static String get(String key)
{
Map<String, Object> map = getLocalMap();
return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY));
}
public static <T> T get(String key, Class<T> clazz)
{
Map<String, Object> map = getLocalMap();
return StringUtils.cast(map.getOrDefault(key, null));
}
public static Map<String, Object> getLocalMap()
{
Map<String, Object> map = THREAD_LOCAL.get();
if (map == null)
{
map = new ConcurrentHashMap<String, Object>();
THREAD_LOCAL.set(map);
}
return map;
}
public static void setLocalMap(Map<String, Object> threadLocalMap)
{
THREAD_LOCAL.set(threadLocalMap);
}
public static Long getUserId()
{
return Convert.toLong(get(SecurityConstants.DETAILS_USER_ID), 0L);
}
public static void setUserId(String account)
{
set(SecurityConstants.DETAILS_USER_ID, account);
}
public static String getUserName()
{
return get(SecurityConstants.DETAILS_USERNAME);
}
}
SecurityContextHolder中有一个静态的线程变量,其类型为TransmittableThreadLocal,这是阿里巴巴开源的一个类型,主要作用是处理父子线程变量不能共用的情况。ThreadLocal是跟当前线程挂钩的,所以脱离当前线程它就起不了作用。
ThreadLocal和TransmittableThreadLocal的使用场景区别
ThreadLocal场景:它的应用就是比如当前用户特有的一些属性,不能跟其他线程,用户共用。
TransmittableThreadLocal场景: 就是父子线程或者不同线程需要共用一些变量。
这个拦截器的功能并不是很复杂,主要是以下几点
-
对应网关中的AuthFilter中将jwt令牌中的claims信息存入request header的操作,这里把用户信息从header中取出来,并且存入该线程的SecurityContextHolder
-
获取requestHeader中的token
-
判断令牌是否为空,解析出当前的登录用户是谁,并判断用户的token以及登录状态是否已经过期
-
最后把用户登录信息也存入SecurityContextHolder
以上是preHandle方法中的流程
在afterCompletion方法中,就一件事情,清除SecurityContextHolder,这一点也很好理解,当当前用户结束操作的时候,清楚本线程中的信息,给下一个用户执行腾空间。
切面
-
系统中有两个aspect,分别是: InnerAuthAspect: 内部服务调用验证处理
@Aspect
@Component
public class InnerAuthAspect implements Ordered
{
@Around("@annotation(innerAuth)")
public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable
{
//这一步是查请求来源
String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
// 内部请求验证
// 如果不是来源于内部的请求,则抛出异常
if (!StringUtils.equals(SecurityConstants.INNER, source))
{
throw new InnerAuthException("没有内部访问权限,不允许访问");
}
//这里获取用户信息
String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
// 用户信息验证
// 未设置用户信息的,以及其他异常数据,同样抛出异常
if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)))
{
throw new InnerAuthException("没有设置用户信息,不允许访问 ");
}
//在切入点执行
return point.proceed();
}
/**
* 确保在权限认证aop执行前执行
*/
@Override
public int getOrder()
{
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
这个切面实现了Ordered接口,其作用是确保在权限认证AOP执行前执行
主逻辑方法使用@Around注解修饰,作用目标是内部注解@innerAuth
该切面的主要功能是,
-
验证请求是否是内部请求,
-
验证用户信息是否设置
这个切面很明确地说明了,对于用户请求,必须携带用户名和id,才会被视作是合法请求。
-
PreAuthorizeAspect:基于spring AOP的注解鉴权
@Aspect
@Component
public class PreAuthorizeAspect
{
/**
* 构建
*/
public PreAuthorizeAspect()
{
}
/**
* 定义AOP签名 (切入所有使用鉴权注解的方法)
*/
public static final String POINTCUT_SIGN = " @annotation(com.ruoyi.common.security.annotation.RequiresLogin) || "
+ "@annotation(com.ruoyi.common.security.annotation.RequiresPermissions) || "
+ "@annotation(com.ruoyi.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();
//这一步的作用是方法增强,对一个Method对象进行注解检查,即判断被目标注解修饰的方法,当前请求是否有足够权限访问
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);
}
}
}
此切面的作用是提供注解鉴权
切面中声明了AOP签名,并且使用@Pointcut注解限定了该切面的切入点。 这里使用了环绕切入。
在around方法中提供注解鉴权:
获取方法签名
方法增强,对一个Method对象进行注解检查,即判断被目标注解修饰的方法,当前请求是否有足够权限访问。checkMethodAnnotation方法中会对三大注解进行校验,只要有一个不符合条件,就会抛出NotPermissionException异常。
执行原有逻辑
public class NotPermissionException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public NotPermissionException(String permission)
{
super(permission);
}
public NotPermissionException(String[] permissions)
{
super(StringUtils.join(permissions, ","));
}
}
注解
-
总的来看,ruoyi权限系统中的注解分为权限注解和功能注解,权限注解+aop功能增强,实现了注解鉴权,功能注解则是决定此方法是否能够被远程调用等。
-
注解详解:
-
@RequiresLogin:登录认证:只有登录之后才能进入该方法
-
权限注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresLogin
{
}
@RequiresPermissions: 权限认证:必须具有指定权限才能进入该方法
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresPermissions
{
/**
* 需要校验的权限码
*/
String[] value() default {};
/**
* 验证模式:AND | OR,默认AND
*/
Logical logical() default Logical.AND;
}
-
此注解有两个参数:权限码和验证逻辑
-
权限码是一个数组,用于记录拥有哪些权限才能访问本接口
-
验证逻辑是说明,以上的访问权限之间是或还是与的关系。
-
@RequiresRoles:角色认证:必须具有指定角色标识才能进入该方法
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresRoles
{
/**
* 需要校验的角色标识
*/
String[] value() default {};
/**
* 验证逻辑:AND | OR,默认AND
*/
Logical logical() default Logical.AND;
}
-
此注解有两个参数:角色标识和验证逻辑
-
角色标识也是一个数组,记录访问该接口需要的角色
-
验证逻辑:说明是或还是与的关系
功能注解
-
@EnableCustomConfig
-
这个注解是一个合并注解,具体作用是什么并不是很明确。但至少可以简化代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 表示通过aop框架暴露该代理对象,AopContext能够访问
@EnableAspectJAutoProxy(exposeProxy = true)
// 指定要扫描的Mapper类的包的路径
@MapperScan("com.ruoyi.**.mapper")
// 开启线程异步执行
@EnableAsync
// 自动加载类
@Import({ ApplicationConfig.class, FeignAutoConfiguration.class })
public @interface EnableCustomConfig
{
}
-
@EnableRyFeignClients
-
这个注解是自定义feign注解,用于远程调用,添加basePackages的路径
-
这个注解其实是对@EnableFeignClients注解的增强,给basePackages设置了默认值"com.ruoyi"
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableFeignClients
public @interface EnableRyFeignClients
{
String[] value() default {};
String[] basePackages() default { "com.ruoyi" };
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
补充