若依框架学习笔记

1. 学习路线图

image-20240122093531148

2. 框架运行

若依框架官网

2.1 后端运行

0⃣️ 数据库配置

配置文件位置:ruoyi-adimn/src/main/resources/application-druid.yml

配置步骤:

1⃣️ 本地创建数据库ry-vue(通过配置文件得知)

image-20240122094833492

2⃣️ 运行sql脚本 (位置:sql/ry_20231130.sql)

✅ 运行完成后数据库表即可建立完成

3⃣️ 查看redis配置 (位置:ruoyi-adimn/src/main/resources/application.yml)

image-20240122095233790

本地启动redis

4⃣️ 启动项目

启动类的位置:ruoyi-adimn/src/main/java/com/ruoyi/RuoYiApplication

2.2 前端运行

前端目录:RUOYI-UI

下载依赖:npm install

运行前端代码:npm run dev

3. 项目分析

RuoYi-Vue 是一个单体项目,但是根据多模块进行管理,将不同的模块进行隔离

其中, ruoyi-quartz ruoyi-quartz可以进行删除,其余模块对其没有任何依赖

3.1.文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
com.ruoyi     
├── common // 工具类
│ └── annotation // 自定义注解
│ └── config // 全局配置
│ └── constant // 通用常量
│ └── core // 核心控制
│ └── enums // 通用枚举
│ └── exception // 通用异常
│ └── filter // 过滤器处理
│ └── utils // 通用类处理
├── framework // 框架核心
│ └── aspectj // 注解实现
│ └── config // 系统配置
│ └── datasource // 数据权限
│ └── interceptor // 拦截器
│ └── manager // 异步处理
│ └── security // 权限控制
│ └── web // 前端控制
├── ruoyi-generator // 代码生成(可移除)
├── ruoyi-quartz // 定时任务(可移除)
├── ruoyi-system // 系统代码
├── ruoyi-admin // 后台服务

PS:

1.代码分开存放:controller(位置:ruoyi-admin/src/main/java/com/ruoyi/web/controller/),controller调用的service、mapper、domain在ruoyi-system目录下

2.ruoyi-admin使用到framework内容

ruoyi-adminpom.xml中引入

image-20240122103339887

同样在frameworkpom.xml中引入ruoyi-system

image-20240122103701409

4.RBAC模型

  • 若依框架的权限管理功能是基于RBAC来实现的,即:系统中所有的权限,都是基于角色来控制的
  • 框架对权限的控制,不仅支持菜单的功能,还支持菜单中的每一个按钮的权限控制

RBAC(基于角色的访问控制)模型包含的表有下面五张:

  1. 用户表

  2. 角色表

  3. 菜单表

  4. 用户角色关联表

  5. 角色菜单关联表

image-20240122110117943

查询当前登录用户的所拥有的权限菜单(登录人ID:2)

1
2
3
4
select * from sys_menu t1
left join sys_role_menu t2 on t1.menu_id = t2.menu_id
left join sys_user_role t3 on t2.role_id = t3.role_id
where t3.user_id = 2;

5.数据字典

数据字典的功能由两张表组成:

  1. sys_dict_type:字典类型表
  2. sys_dict_data:字典数量表

两者之间的关系:sys_dict_type表的dict_type字段关联sys_dict_datadict_type字段

image-20240122112919670

6.拦截器

6.1 前端拦截器

前端拦截器分为 前置拦截器 和 响应拦截器

前置拦截器

前置拦截器的代码写在request.js文件中,路径为 src - utils - request.js

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
return config;
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})

前置拦截器完成功能:

  1. 在请求头中添加token
  2. get请求映射params参数
  3. 阻止重复请求重复提交(虽然前端做了,但一般后台也需要做借口幂等性校验)

6.2 响应拦截器

响应拦截器也在request.js文件中。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
Message({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
Message({ message: msg, type: 'warning' })
return Promise.reject('error')
} else if (code !== 200) {
Notification.error({ title: msg })
return Promise.reject('error')
} else {
return res.data
}
},error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)

6.2 后端拦截器

路径:ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.ruoyi.framework.interceptor;

import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ServletUtils;

/**
* 防止重复提交拦截器
*
* @author ruoyi
*/
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null)
{
if (this.isRepeatSubmit(request, annotation))
{
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
}
return true;
}
else
{
return true;
}
}

/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request 请求信息
* @param annotation 防重复注解参数
* @return 结果
* @throws Exception
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

7.登录流程

登录流程图:

image-20240123090704845

7.1 登录技术栈分析

上述流程中,登录成功后,最后会给前端返回一个token,会调用TokenService的createToken方法,如下:

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
35
36
/*位置: ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java */

/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);

Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}

---

/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}

实际上createToken方法就是采用JWT的方式生成令牌返回给前端

7.2 token校验

登录成功后,我们每一个请求到后台时,都需要对token进行权限校验,若依框架对token的校验是用过滤器实现的

过滤器位置:ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java

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
35
36
37
38
39
40
41
42
43
44
45
package com.ruoyi.framework.security.filter;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.TokenService;

/**
* token过滤器 验证token有效性
*
* @author ruoyi
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}

getLoginUser方法作用:

  1. 从请求头中获取到token,然后解析得到我们设置进去的UUID
  2. 然后再以UUID作为keyRedis中获取到登录账号信息
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
/* 位置:ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java */
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
log.error("获取用户信息异常'{}'", e.getMessage());
}
}
return null;
}

verifyToken方法作用:校验令牌超时时间和当前时间的差值,如果小于20分钟的话,就会刷新令牌的超时时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
{
refreshToken(loginUser);
}
}

8.按钮权限控制

若依框架不仅对菜单实现了权限控制,还对按钮实现了权限控制。

8.1 前端

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:user:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['system:user:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:user:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-upload2"
size="mini"
@click="handleImport"
v-hasPermi="['system:user:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:user:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>

很多按钮都有v-hasPermi="['xxx:xxx:xxx']"v-hasPermivue的自定义指令,属性值就是创建按钮时定义的权限标志。

其定义在ruoyi-ui/src/directive/permission/hasPermi.js文件中。

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
 /**
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/

import store from '@/store'

export default {
inserted(el, binding, vnode) {
const { value } = binding
const all_permission = "*:*:*";
const permissions = store.getters && store.getters.permissions

if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value

const hasPermissions = permissions.some(permission => {
return all_permission === permission || permissionFlag.includes(permission)
})

if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置操作权限标签值`)
}
}
}

8.2 后端

8.2.1 接口权限

controller中随便找一个接口为例:

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取菜单列表
*/
@PreAuthorize("@ss.hasPermi('system:menu:list')")
@GetMapping("/list")
public AjaxResult list(SysMenu menu)
{
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
return success(menus);
}

发现其中有这样一行代码:

1
@PreAuthorize("@ss.hasPermi('system:menu:list')")

进入hasPermi方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 路径:ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java */
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission)
{
if (StringUtils.isEmpty(permission))
{
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
{
return false;
}
PermissionContextHolder.setContext(permission);
return hasPermissions(loginUser.getPermissions(), permission);
}

上面代码逻辑如下:

  1. 通过SecurityUtils工具类从Security上下文中获取到登录用户信息
  2. 然后从用户信息中获取到该用户所拥有的权限字符串集合
  3. 然后做对比,看其中是否包含【@ss.hasPermi(‘system:menu:list’)】中的权限字符串
  4. 包含就可以访问该方法,不包含就不可以访问该方法

权限方法

@PreAuthorize注解用于配置接口要求用户拥有某些权限才可访问,它拥有如下方法

方法 参数 描述
hasPermi String 验证用户是否具备某权限
lacksPermi String 验证用户是否不具备某权限,与 hasPermi逻辑相反
hasAnyPermi String 验证用户是否具有以下任意一个权限
hasRole String 判断用户是否拥有某个角色
lacksRole String 验证用户是否不具备某角色,与 isRole逻辑相反
hasAnyRoles String 验证用户是否具有以下任意一个角色,多个逗号分隔

详细信息

8.2.1 数据权限

文档内容

9. 项目实战

项目背景和需求说明

学生成绩管理系统

  • 对学校的【课程信息】和【分数信息】进行管理

  • 老师角色的人登录系统后,可以对【课程信息】和【分数信息】进行维护,进行增删改查

  • 学生角色的人登录系统后,可以查看【课程信息】和【分数信息】,但是不能进行修改和删除操作就这么一个简单的需求,接下来,我们看看利用若依框架,如何快速的进行开发

项目表结构分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DROP TABLE IF EXISTS `t_score`;
CREATE TABLE `t_score` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`create_user_name` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人姓名',
`course_id` BIGINT(2) NULL DEFAULT NULL COMMENT '课程ID',
`user_id` BIGINT(1) NULL DEFAULT NULL COMMENT '用户ID',
`score` int(11) NULL DEFAULT NULL COMMENT '分数',
PRIMARY key(`id`) USING BTREE
)ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分数表' ROW_FORMAT = Compact;

---------

DROP TABLE IF EXISTS `t_course`;
CREATE TABLE `t_course` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`create_user_name` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人姓名',
`course_name` VARCHAR(50) NULL DEFAULT NULL COMMENT '课程名称',
`course_status` INT(1) NULL DEFAULT NULL COMMENT '课程状态(1:不可用;2:可用)',
PRIMARY key(`id`) USING BTREE
)ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '课程表' ROW_FORMAT = Compact;

代码生成

image-20240123143822327

选择 “生成代码” 按钮,即可下载生成好的代码。

拷贝代码到项目中并测试

① 后端代码 - 位置:main/java

分为四个文件夹controllerdomainmapperservice

  1. domain实体类,拷贝放置位置ruoyi-system/src/main/java/com/ruoyi/system/domain

  2. mapper,拷贝放置位置ruoyi-system/src/main/java/com/ruoyi/system/mapper

  3. service,拷贝放置位置ruoyi-system/src/main/java/com/ruoyi/system/service

  4. mapper.xml,拷贝放置位置ruoyi-system/src/main/resources/mapper/system

  5. controller,拷贝放置位置ruoyi-admin/src/main/java/com/ruoyi/web/controller/system

② 前端代码 - 位置:vue

分为两个文件夹apiviews

  1. api下的js文件,拷贝放置位置RUOYI-UI/src/api/system
  2. views下的vue文件,拷贝放置位置RUOYI-UI/src/views/system,一般需要带着文件夹,例如hourdb/index.vue

③ 执行sql文件,完成菜单生成

ry-vue下执行即可,执行完成后,可在sys_menu下查看生成的菜单数据

④ 执行maven - compile

文章作者: qinwei
文章链接: https://qw-null.github.io/2024/02/19/若依框架学习笔记/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 QW's Blog