若依
管理员:admin,admin123
普通用户:ry,admin123
Ruoyi-Vue
1 权限管理
ruoyi 的权限管理是用 @PreAuthorize 来实现的,它是 SpringSecurity 的一个注解
这个注解用在方法上,当这个注解里的值是 true 的时候代表可以访问
注解支持 EL 表达式
1 |
|
其中,@ss 表示作者自定义的一个 Service 方法,取了别名
1 |
|
其中 @PreAuthorize("@ss.hasPermi('monitor:server:list')")
调用了 PermissionService 的 hasPermi 方法,传入了一个权限列表
ruoyi 的权限管理是在数据表 sys_menu 里的,其中有一列 perms 定义了各种各样的权限
然后在代码里有一个类 LoginUser 里的一个属性 Set<String> permissions
对权限做了封装,判断一个用户是否有目标资源的权限,就看这个 Set 集合里有没有对应的权限列表
ruoyi 的用户对应的菜单权限是在:系统管理 / 角色管理里修改的,对应的表是 sys_role_menu
2 事务管理
@Transactional 注解只能应用到 public 可见度的方法上(因为 @Transactional 基于 AOP 实现的事务管理,AOP 只会代理 public 的方法)
- 检查性异常,事务不会回滚;正确做法是,在 @Transactional 中加入 rollbackFor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@Transactional(rollbackFor = Exception.class)
public int insertUser(User user) throws Exception{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag){
throw new SQLException("发生异常了..");
}
return rows;
} - try-catch 自己手动捕获异常,不会回滚;正确做法是在业务层统一抛出异常,全局异常处理器处理
3 系统日志
定义自定义注解 @Log 添加到被记录日志的 Controller 方法上
1 |
|
其中 BusinessType 和 OperatorType 是枚举类型
1 |
|
使用方式
1 |
|
定义一个切面类,其中前置请求把当前时间存入到 ThreadLocal中,为了计算操作消耗时间
其中一个参数是 Log controllerLog
,这个参数相当于 Log 注解的一个实例,有了这个参数就可以用@Before(value = "@annotation(controllerLog)")
这种方式表示所有带 @Log 注解的方法
1 |
|
@AfterReturning 是正常执行完返回的情况,其中注解中的 returning 表示的是代理方法的返回值,写到了参数中
1 |
|
拦截异常操作,throwing 表示抛出的异常
1 |
|
4 数据校验
用@Validated 注解可以帮忙校验前端传过来的数据,引入以下的包
1 |
|
@NotNull 不能为null
@NotBlank 不能为空,常用于检查空字符串
@NotEmpty 不能为空,多用于检测list是否size是0
@Max 该字段的值只能小于或等于该值
@Min 该字段的值只能大于或等于该值
@Past 检查该字段的日期是在过去
@Future 检查该字段的日期是否是属于将来的日期
@Email 检查是否是一个有效的email地址
使用方法:
1 |
|
还可以自定义注解校验器
1 |
|
5 数据权限
用户只能看到自己部门的数据,这种情况一般被称为数据权限
在 SysRole 类中,有一个字段,dataScope 表示数据范围
- 所有数据权限
- 自定义数据权限
- 本部门数据权限
- 本部门及以下数据权限
- 仅本人数据权限
核心是利用自定义注解,@DataScope 和 处理 AOP 的类 DataScopeAspect 来实现
@DataScope 注解里定义了三个属性其中在 DataScopeAspect 类中可以收到这三个属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
/**
* 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来
*/
public String permission() default "";
}
这个注解一般放在 Service 层的方法上,传进来的一般是 pojo 对象传进来的 pojo 对象,比如 SysDept 集成了 BaseEntity,BaseEntity 中有一个属性1
2
3
4
5@Override
@DataScope(deptAlias = "d")
public List<SysDept> selectDeptList(SysDept dept){
return deptMapper.selectDeptList(dept);
}Map<String, Object> params;
在 AOP 的过程中,会取出来这个字段,根据不同角色的权限,往里面动态添加一些 SQL 语句最后在 mapper.xml 文件中,会把添加的内容拼接到查询语句最后1
2
3
4
5
6
7if (StringUtils.isNotBlank(sqlString.toString())){
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity){
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}${params.dataScope}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
<include refid="selectDeptVo"/>
where d.del_flag = '0'
<if test="deptId != null and deptId != 0">
AND dept_id = #{deptId}
</if>
<if test="parentId != null and parentId != 0">
AND parent_id = #{parentId}
</if>
<if test="deptName != null and deptName != ''">
AND dept_name like concat('%', #{deptName}, '%')
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
order by d.parent_id, d.order_num
</select>
6 异常处理
首先需要自定义异常,比如这里的登录异常,继承了 RuntimeException,保证 Spring 事务不失效,因为 Spring 事务默认处理的是运行时异常,如果抛出检查性异常就不会进行回滚
1 |
|
创建全局异常处理器,在类上加一个注解,@RestControllerAdvice = @ControllerAdvice + @ResponseBody
1 |
|
7 数据导入导出
7.1 导出
8 登录管理
8.1 SpringSecurity 前置知识
SpringSecurity 认证流程
Filter->构造Token->AuthenticationManager->转给Provider处理->认证处理成功后续操作或者不通过抛异常
登录逻辑
登录👉拿到token👉请求带上token👉JWT过滤器拦截👉校验token👉将从缓存中查出来的对象放到上下文中
重要概念
- SecurityContext:上下文对象,
Authentication
对象会放在里面。 - SecurityContextHolder:用于拿到上下文对象的静态工具类。
- Authentication:认证接口,定义了认证对象的数据形式。
- AuthenticationManager:用于校验
Authentication
,返回一个认证完成后的Authentication
对象- Authentication 是个接口, 常用的实现类是
UsernamePasswordAuthenticationToken
- 没认证的
UsernamePasswordAuthenticationToken
是this.setAuthenticated(false);
- 认证之后是
super.setAuthenticated(true);
1
2
3
4
5
6
7
8
9
10
11
12public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
- Authentication 是个接口, 常用的实现类是
SecurityContext
上下文对象, 认证后的数据就放在这里1
2
3
4
5
6
7public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication();
// 放入Authentication对象
void setAuthentication(Authentication authentication);
}SecurityContextHolder
可以说是SecurityContext的工具类,用于get or set or clear SecurityContext,默认会把数据都存储到当前线程中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class SecurityContextHolder {
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}AuthenticationManager
将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager1
2
3
4
5public interface AuthenticationManager {
// 认证方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
Controller 的请求到 SysLoginController#login
1 |
|
调用 Service 的 login 方法
其中 uuid 来验证输入的验证码与 Redis 中的验证码是否匹配
1 |
|
SpringSecurity 的验证方法鉴权,这个语句是核心语句
下面就 authenticationManager
对象创建authenticate
方法参数,来解析这个登录鉴权过程
- 传入用户名和密码创建了一个UsernamePasswordAuthenticationToken对象,这是我们前面说过的Authentication的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication对象。
- 使用我们先前已经声明过的Bean-authenticationManager调用它的authenticate方法进行认证,返回一个认证完成的Authentication对象。
- 认证完成没有出现异常,使用SecurityContextHolder获取SecurityContext之后,将认证完成之后的Authentication对象,放入上下文对象。
- 从Authentication对象中拿到我们的UserDetails对象,之前我们说过,认证后的Authentication对象调用它的getPrincipal()方法就可以拿到我们先前数据库查询后组装出来的UserDetails对象,然后创建token。
其中 authenticationManager 对象是配置类配置的 bean
定义了一个配置类 SecurityConfig 继承了 WebSecurityConfigurerAdapter 定义了 AuthenticationManager 对象
1 |
|
核心代码中调用的这个方法 authenticate
传入的参数是 Authentication
类型的,所以需要在前面创建出 Authentication
类的对象
1 |
|
其中 UsernamePasswordAuthenticationToken
是 Authentication
类的子类,需要把用户名和密码传递进去
这是 authenticate
方法传递的参数来源
之后 authenticate
方法会执行一个过滤器链(SpringSecurity 自动调用), 最终会调用到 UserDetailsService
这个接口的 loadUserByUsername
方法, 这里有一个实现类 UserDetailsServiceImpl
实现了这个接口接口重写了方法
所以最终 authenticate
会调用 UserDetailsServiceImpl.loadUserByUsername
在 SysLoginService.login
方法, 核心代码之前还有一个, 把 Authentication
对象存到 ThreadLocal 的操作
1 |
|
其中第二行把 Authentication
保存到了 ThreadLocal 中, AuthenticationContextHolder
底层封装了 ThreadLocal
在 UserDetailsServiceImpl.loadUserByUsername
中
1 |
|
根据 username 查询了数据库, 得到了 SysUser 对象
调用了 passwordService.validate() 方法, 进入 validate 方法内
这个方法内
1 |
|
这个方法首先从之前的 AuthenticationContextHolder
(本质上是一个 ThreadLocal) 从取出之前存的 UsernamePasswordAuthenticationToken
对象(里面是用户输入的用户名和密码)
username 和 password 是他从 UsernamePasswordAuthenticationToken
里取出来的
跟数据库查出来的密码进行比较(加密之后比较, 因为数据库里是存储的密文, SpringSecurity 自动加密后的, SpringSecurity 默认是用 BCryptPasswordEncoder 来进行加密的)
有一个 retryCount 变量, 如果
如果不匹配, 把 retryCount 放到 Redis 里默认设置 10 分钟, 如果超过最大重试次数设置的是 5 次, 就抛出异常
如果期间内匹配成功, 就从 Redis 中删除 key
最后 UserDetailsServiceImpl.loadUserByUsername
是 SpringSecurity 提供的方法, 它的返回值是 UserDetails
类型的, ruoyi 创建了一个 LoginUser
类继承了 UserDetails
所以 UserDetailsServiceImpl.loadUserByUsername
方法返回了 LoginUser
对象
权限字符是在 UserDetailsServiceImpl.createLoginUser
方法中通过构造方法存入 LoginUser 中的, 有个调用 permissionService
的行为
1 |
|
前面 UserDetailsServiceImpl.loadUserByUsername
执行成功之后会把 UserDetails
对象存储到 authentication
对象中
回到 SysLoginService
方法中, 第二行得到 getPrincipal
对象可以强转为 LoginUser
1 |
|
第一行是异步执行方法, todo
最后一行是记录用户信息, 登录的 ip 和登录的时间
9 异步任务管理器
他定义了一个类, 用来管理异步任务调用
其中这个成员变量获取 ThreadPoolTaskExecutor
对象
1 |
|
其中 SpringUtils 是 ruoyi 封装的一个工具类实现了 BeanFactoryPostProcessor 和 ApplicationContextAware
两个接口
- BeanFactoryPostProcessor 接口是为了拿到
ConfigurableListableBeanFactory
对象 - ApplicationContextAware 接口是为了拿到
ApplicationContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
{
/** Spring应用上下文环境 */
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
{
SpringUtils.beanFactory = beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
SpringUtils.applicationContext = applicationContext;
}
/**
* 获取对象
*
* @param name
* @return Object 一个以所给名字注册的bean的实例
* @throws org.springframework.beans.BeansException
*
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException
{
return (T) beanFactory.getBean(name);
}
// 下面还有方法, 感兴趣可以翻看 ruoyi 源码
}
回到 AsyncManager
对象里, 得到了 ScheduledExecutorService
这个 bean, 这个 bean 定义在ThreadPoolConfig
中
1 |
|
1 |
|
这里 new 了一个 ScheduledThreadPoolExecutor
对象
并且重写了一个 afterExecute
方法, 这个方法是 ScheduledThreadPoolExecutor
类继承自 ThreadPoolExecutor
类的
这种写法第一次见
回到 AsyncManager
采用单例模式生成对象
1 |
|
有个方法执行的函数, 传入任务, 延时时间, 延时时间的单位
1 |
|
10 异步工厂(产生任务用)
这个工厂是专门记录登录日志的, 存储到 sys_logininfor 表中
1 |
|
11 日志记录 LogAspect 类
首先创建一个日志记录对象, 其中 LogAspect.class 是为了标识这个日志对象, 相当于给这个日志对象起了个名字
1 |
|
getLogger 创建对象跟 @Slf4j 有什么区别
- getLogger 创建对象是直接使用 SLF4J 的 api 创建一个 Logger 对象, 用于记录日志, LogAspect.class 是这个 Logger 的名字, 通常用于区分不同类
- @Slf4j 是 lombok 提供的一个注解, 用于在编译时自动在类中声明一个名为 log 的 Logger 对象, 创建方式与上述代码相同
日志类中定义了一个 ThreadLocal, 用于计算操作消耗的时间, 在方法执行前 set 当前时间, 方法返回时用当前时间减去之前 set 的时间, 最后在 finally 代码块中 remove 掉
1 |
|
处理完请求执行
其中注解上的 returning = jsonResult
应该是为了跟方法参数中的 jsonResult
绑定, 为了获取目标方法的执行结果
注解上的 pointcut = "@annotation(controllerLog)"
应该是为了跟方法参数中 Log controllerLog
绑定, 只要是方法上有 @Log
注解的, 都要被代理
1 |
|
将切点, @Log 注解, 返回值都传递给 handleLog 方法
handleLog 方法接收 4 个参数
- 切点
- 注解
- 异常
- 返回值
1
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
创建数据库日志对象, 封装属性
先给操作状态设置成成功,
1 |
|
- BusinessStatus 是一个枚举类型, 里面有两个值
- 枚举类型本身里面有属性
- name 枚举的名字, 比如
BusinessStatus.SUCCESS.name()
得到的就是字符串SUCCESS
- ordinal 是枚举的序数, 从 0 开始
1
2
3
4
5
6
7
8
9
10
11
12public enum BusinessStatus
{
/**
* 成功
*/
SUCCESS,
/**
* 失败
*/
FAIL,
}
- name 枚举的名字, 比如
RequestContextHolder 是 Spring 的一个类, 用于存储当前线程的 RequestAttributes 对象
RequestAttributes 对象包含了与当前 HTTP 请求相关的属性,例如请求参数、请求头、会话属性等
1 |
|
他得到请求参数之后, 这条语句可以得到 ip
1 |
|
如果获取不到, 下面还有判断语句
1 |
|
通过这个获取 URI
1 |
|
后续的一些操作
1 |
|
最后异步插入数据库
1 |
|
调用异步工厂里的一个方法, 把日志记录到数据库里
他这个 TimerTask 继承了 Runnable 接口, 所以要重写里面的 run 方法
1 |
|
12 配置多数据源
首先 ruoyi 用的是 Druid 数据源
下面主从库是 Druid 主从库的配置方式
首先 Spring Boot 的 application.yml 中激活了 druid, 它的意思是把名为 application-druid.yml
的配置文件加载进来
1 |
|
其中 application-druid.yml
中可以配主从数据库
1 |
|
在 DruidConfig 中会有对应的 bean@ConfigurationProperties("spring.datasource.druid.slave")
,用于将配置文件中的属性值绑定到Java对象上
它将 application-druid.yml
配置文件中以 spring.datasource.druid.slave
为前缀的属性值绑定到了当前的Bean(即DataSource)上
1 |
|
DataSourceAspect
定义了一个切点
1 |
|
@within(com.ruoyi.common.annotation.DataSource)
将会匹配所有在类上标注了 @DataSource 注解的类中的所有方法
在这里把注解传入的 DataSourceType 设置到 ThreadLocal 中
1 |
|
通过 AbstractRoutingDataSource
用于动态路由数据源。这个类的主要作用是根据某种策略,动态地选择一个数据源,然后将当前的数据库操作路由到这个数据源上
这种机制在实现数据库读写分离、多数据源切换等场景时非常有用
AbstractRoutingDataSource
类本身并不知道应该选择哪个数据源,这个决定由子类通过实现 determineCurrentLookupKey()
方法来做出。这个方法应该返回一个能唯一标识数据源的 key,然后 AbstractRoutingDataSource
就会使用这个 key 来查找并选择对应的数据源
1 |
|
13 注解
@PostConstruct
在初始化之后会执行这里的代码, 通常用于执行一些初始化工作, 比如加载缓存, 启动某个后台线程等
1 |
|