若依脚手架-学习

本文最后更新于:1 年前

项目初始化

后端

  1. 前往 Gitee 下载页面(https://gitee.com/y_project/RuoYi-Vue (opens new window))下载解压到工作目录
  2. 创建数据库(默认使用 ry-vue 数据库,可自己修改),导入 sql 目录下的数据脚本 ry_2021xxxx.sqlquartz.sql
  3. 修改项目配置文件,如果前面修改了数据库名则需要修改数据库相关配置,位于 ruoyi-admin 模块下的配置文件 application-druid.yml 的数据库配置
  4. 启动 admin 应用程序即可

前端

  1. cd 进入 ruiyi-ui,执行 npm install 安装项目依赖,如果下载太慢可指定下载源 npm install --registry=https://registry.npmmirror.com
  2. 安装完依赖后可修改 vue.config.js 设置页面端口默认为 80
  3. npm run dev 启动项目

结构分析

模块结构

|500
1,2,3,4 前四个模块是必须的基本模块,5 和 6 分别是定时任务和代码生成器模块为可选模块,使用 maven 的 package 打包后会在 ruoyi-admin 模块的 target 文件夹下生成 jar 包,可直接部署运行

表结构

  1. 代码生成器表和系统管理表:

|500
2. 定时任务表:

|287

配置结构

ruoyi-admin 的 resources 目录树形结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
│  application-druid.yml
│ application.yml
│ banner.txt
│ logback-spring.xml

├─i18n
│ messages.properties
│ messages_en.properties

├─META-INF
│ spring-devtools.properties

├─mybatis
│ mybatis-config.xml

└─upload
└─avatar
└─2023
└─12
└─04
wallhaven-1pol63_20231204211235A001.png
  1. application-druid.yml:数据库配置文件,使用 druid 数据库连接池,并开启数据库监控
  2. application.yml:管理后台应用程序配置文件
  3. banner.txt:程序启动横幅配置
  4. logback-spring.xml (原本为 logback.xml,修改后可从 yml 文件中读取配置参数用于自定义日志):日志自定义配置文件(将原先的 logback.xml 改成 logback-spring.xml。原因是 springboot 先读取 logback.xml,然后加载 yml/properties,再加载 logback-spring.xml
  5. i18n:国际化文件夹,messages.properties 和 messages_en.properties 分别为中英文配置
  6. spring-devtools.properties:热启动配置文件
  7. mybatis-config.xml:mybatis 配置文件
  8. upload/**:自己配置的文件上传目录

返回值结构

RuoYi 脚手架使用的是前后端分离的版本,后端接口依旧是返回 json 格式数据,一共有 3 中返回值分别为:TableDatalnfo用于分页列表)、void用于导出/下载)、AjaxResult用于基本的增删改查)。
TableDatalnfo

1
2
3
4
5
6
7
8
9
10
11
12
public class TableDataInfo implements Serializable  
{
private static final long serialVersionUID = 1L;
/** 总记录数 */
private long total;
/** 列表数据 */
private List<?> rows;
/** 消息状态码 */
private int code;
/** 消息内容 */
private String msg;
}

AjaxResult

1
2
3
4
5
6
7
8
9
10
public class AjaxResult extends HashMap<String, Object>  
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
}

核心模块刨析

1. 解读模块

首先对一个开源项目,需要对各个项目业务模块进行分析理解,充分理解各个业务模块功能和关系之后,才能以此作为基座进行二次开发,一般分析从以下四个方面考虑:

  • 业务前提:了解模块功能,它在做什么,比如岗位 crud
  • 技术前提:了解前后端技术栈,掌握相对应的技术,如 springboot+vue 项目,那么前端 vue 基本操作必须会,以及后端 ssm 必须会
  • 开发技巧:前端会 F12 进入开发者模式进行调试,后端掌握 debug 调试
  • 常规项目特点:了解各种类型项目的特点,例如:管理后台类项目特点先页面、后接口,经典厂字形布局
  • web 项目组件技巧
    • ==业务 UI==:
      • 一般以树形结构层级关系布局,无论多复杂的前端页面,都先将整个页面标签收起来,再层层展开剥离分析各个子标签对应的 UI 和功能,由浅入深了解整个 UI 设计
    • ==业务逻辑==:
      1. 主线:发起请求—>接收请求—>处理请求—>响应请求
      2. 详细
        • 客户端发请求(关注:请求 url/请求方式/请求头/请求参数)
        • 接口接收请求(关注:接口定义/参数接收/参数封装/参数校验)
        • 接口处理请求(关注:业务逻辑实现)
        • 接口响应请求(关注:响应数据类型/数据格式/响应头)

2. 岗位模块

2.1 岗位列表


岗位模块的业务逻辑如下图所示:

用户请求浏览器进入前端页面,进入过程中前端页面发起请求向后端请求数据,得到返回的 json 数据后将数据渲染在前端页面上呈现给用户。
其后端业务调用链路如下图所示:

2.2 岗位添加/修改

页面设计:依据表中必须填/选填字段,根据内容决定选择框架-输入框
前端代码:

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
  <!-- 添加或修改岗位对话框 -->  
<el-dialog :title="title" :visible.sync="open" append-to-body width="500px">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="岗位名称" prop="postName">
<el-input v-model="form.postName" placeholder="请输入岗位名称" />
</el-form-item> <el-form-item label="岗位编码" prop="postCode">
<el-input v-model="form.postCode" placeholder="请输入编码名称" />
</el-form-item> <el-form-item label="岗位顺序" prop="postSort">
<el-input-number v-model="form.postSort" :min="0" controls-position="right" />
</el-form-item> <el-form-item label="岗位状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}
</el-radio>
</el-radio-group> </el-form-item> <el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入内容" type="textarea" />
</el-form-item> </el-form> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div> </el-dialog></div>
<script>
/** 提交按钮 */
submitForm: function () {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.postId != undefined) {
updatePost(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
});
} else {
addPost(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
});
}
this.getList();
}
});
},
</script>

后端代码:

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
/**  
* 新增岗位
*/
@PreAuthorize("@ss.hasPermi('system:post:add')")
@Log(title = "岗位管理", businessType = BusinessType.INSERT)
@PostMapping
//Spring-validate 校验框架
public AjaxResult add(@Validated @RequestBody SysPost post) {
if (!postService.checkPostNameUnique(post)) {
return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
} else if (!postService.checkPostCodeUnique(post)) {
return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
}
post.setCreateBy(getUsername());
return toAjax(postService.insertPost(post));
}
/**
* 修改岗位
*/
@PreAuthorize("@ss.hasPermi('system:post:edit')")
@Log(title = "岗位管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysPost post) {
if (!postService.checkPostNameUnique(post)) {
return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
} else if (!postService.checkPostCodeUnique(post)) {
return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
}
post.setUpdateBy(getUsername());
return toAjax(postService.updatePost(post));
}
  1. 前端通过 data 发送 json 数据后端就要用 @RequestBody 接收
  2. 前端通过 Params 发送 xxx-form-url 编码的参数后端就要用 @RequestParam 接收

服务调用链路如下图所示:
|650

2.3 岗位删除


前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
/** 删除按钮操作 */
handleDelete (row) {
const postIds = row.postId || this.ids;
this.$modal.confirm('是否确认删除岗位编号为"' + postIds + '"的数据项?').then(function () {
// 点击确认删除后的操作
return delPost(postIds);
}).then(() => {
// 执行完删除后的操作
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
});
},
</script>

后端代码:

1
2
3
4
5
6
7
8
9
/**  
* 删除岗位
*/
@PreAuthorize("@ss.hasPermi('system:post:remove')")
@Log(title = "岗位管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{postIds}")
public AjaxResult remove(@PathVariable Long[] postIds) {
return toAjax(postService.deletePostByIds(postIds));
}

2.4 Excel 导出功能

若依脚手架使用的是 POI,但是占用内存大,不适合高并发,但是定制灵活。

  • 如果操作 Excel 复杂度高,建议使用 POI,编程灵活。
  • 如果操作 Excel 数据量大,对性能有一定要求的情况,建议使用 EasyExcel。
  • 如果操作 Excel 数据量小,而且追求编程效率,建议使用 Hutool 的 ExcelUtil。


前端代码:

1
2
3
4
5
6
7
8
<script>
/** 导出按钮操作 */
handleExport () {
this.download('system/post/export', {
...this.queryParams
}, `post_${new Date().getTime()}.xlsx`)
}
</script>

后端代码:

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
/**  
* 导出岗位列表
*/
@Log(title = "岗位管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:post:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysPost post) {
List<SysPost> list = postService.selectPostList(post); //获取导出数据
ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);//获取导出excel模板
util.exportExcel(response, list, "岗位数据");//传入数据和模板导出数据
}
//实体类需要添加excel字段注解
public class SysPost extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 岗位序号 */
@Excel(name = "岗位序号", cellType = ColumnType.NUMERIC)
private Long postId;
/** 岗位编码 */
@Excel(name = "岗位编码")
private String postCode;
/** 岗位名称 */
@Excel(name = "岗位名称")
private String postName;
/** 岗位排序 */
@Excel(name = "岗位排序")
private Integer postSort;
/** 状态(0正常 1停用) */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status;
}

扩展自定义模块-客户 CURD

使用 mybatis-generator 实现

  1. 建表-customer:
1
2
3
4
5
6
7
8
CREATE TABLE customer(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(255) NOT NULL COMMENT '名称',
`phone` varchar(255) DEFAULT NULL COMMENT '手机号',
`age` int(11) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='客户表';
SET FOREIGN_KEY_CHECKS = 1;
  1. 拷贝配置文件 generatorConfig.xml 到 ruoyi-systemi 中:
    ==在 pom 文件中加入依赖==
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <build>
    <plugins>
    <!--MyBatis的generator插件-->
    <plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.3.2</version>
    <configuration>
    <verbose>true</verbose>
    <overwrite>false</overwrite>
    </configuration>
    <dependencies>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.45</version>
    </dependency>
    </dependencies>
    </plugin>
    </plugins>
    </build>
  2. 使用插件生成 domain,mapper,xml 文件
  3. 添加 selectForList()方法
  4. 拷贝岗位的 service 和 impl 实现类和 controller,修改
  5. 拷贝前端界面到对应界面中
  6. 创建客户的菜单

代码生成器生成

  1. 导入创建的 customer 表

  2. 编辑修改生成的配置信息

|700
3. 下载生成的代码,导入 sql 文件到数据表

  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
    +---main
    | +---java
    | | \---com
    | | \---ruoyi
    | | \---system
    | | +---controller
    | | | CustomerController.java
    | | +---domain
    | | | Customer.java
    | | +---mapper
    | | | CustomerMapper.java
    | | \---service
    | | | ICustomerService.java
    | | \---impl
    | | CustomerServiceImpl.java
    | \---resources
    | \---mapper
    | \---system
    | CustomerMapper.xml
    \---vue
    +---api
    | \---system
    | customer.js
    \---views
    \---system
    \---customer
    index.vue

    基本的增删改查(基本条件查询和列表查询)逻辑已经实现,后续根据业务需求修改补充对应逻辑代码

数据字典

数据字典是程序中全局经常改变的一些变量,为了方便维护管理采用数据字典进行交互式配置管理。
背景:我们在项目中会有很多的下拉框,这些下拉框都有一个特点就是键值对的存在实现下拉框方式:

  • 界面写死—>有硬编码问题
  • 给下拉框创建张表—>要创建很多张表
  • 使用数据字典实现—>借助字典类型表和数据表实现下拉框等常用逻辑值的动态更新

概念

数据字典(data dictionary)是对于数据模型中的数据对象或者项目的描述的集合,这样做有利于程序员和其他需要参考的人。分析一个用户交换的对象系统的第一步就是去辨别每一个对象,以及它与其他对象之间的关系。这个过程称为数据建模,结果产生一个对象关系图。当每个数据对象和项目都给出了一个描述性的名字之后,它的关系再进行描述(或者是成为潜在描述关系的结构中的一部分),然后再描述数据的类型(例如文本还是图像,或者是二进制数值),列出所有可能预先定义的数值,以及提供简单的文字性描述。这个集合被组织成书的形式用来参考,就叫做数据字典。
简而言之,它是一种数据组织形式,方便使用者维护数据,管理数据。

数据字典表

脚手架中的数据字典组成数据结构由 2 张表共同维护

  • sys_dict_type: 字典类型表

  • sys_dict_data: 字典数据表

|825

字典界面



通过在线维护管理数据字典数据进行实现常见数据的软编码动态更新,大大提高系统的可维护性。

逻辑实现

前端:

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
//获取字典数据
export function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((dictType, index) => {
res.value[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
} else {
getDicts(dictType).then(resp => {
res.value[dictType] = resp.data.map(p => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass
}))
useDictStore().setDict(dictType, res.value[dictType]);
})
}
})
return toRefs(res.value);
})()
}
//main.js全局方法挂载
app.config.globalProperties.useDict = useDict
//组件中传入字典类型获取对应类型的字典数据
const {proxy} = getCurrentInstance();
const {sys_normal_disable} = proxy.useDict("sys_normal_disable");
//使用对应类型的字典数据
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="岗位状态" style="width: 200px">
<el-option v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select></el-form-item>

认证与授权

认证和授权采用是 SpringSecurity 实现 RBAC ,这里分析 RuoYi 是如何借助 SpringSecurity 实现认证与授权的。

认证

  1. 导入依赖
    1
    2
    3
    4
    5
    <!-- spring security 安全认证,版本通过父类模块进行统一版本管理 -->  
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  2. SecurityConfig 配置
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package com.ruoyi.framework.config;  
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
/**
* spring security配置
*
* @author ruoyi
*/@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 解决 无法直接注入 AuthenticationManager
* * @return
* @throws Exception
*/ @Bean
@Override public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
// 注解标记允许匿名访问的url
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
  1. 使用 jwt 认证(SysLoginService):
1
2
3
4
5
6
7
8
9
10
11
12
13
/**  
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody) {
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
return AjaxResult.success().put(Constants.TOKEN, token);
}

自定义数据库验证

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
/**  
* 用户验证处理
*
* @author ruoyi
*/@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPasswordService passwordService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException(MessageUtils.message("user.not.exists"));
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException(MessageUtils.message("user.password.delete"));
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException(MessageUtils.message("user.blocked"));
}
passwordService.validate(user);
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}

授权

若依脚手架中权限是捆绑于菜单上的,用户-——>角色—>菜单—>权限
前端在用户登录后会向后端发起请求获取用户信息,其中包括权限字符串数组,以便于鉴权。

后端接口级别鉴权

  1. 开启注解支持
1
2
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)  
public class SecurityConfig extends WebSecurityConfigurerAdapter {···}
  1. 方法上添加权限注解
    1
    2
    3
    4
    5
    6
    7
    8
    @PreAuthorize("@ss.hasPermi('system:dict:list')")  
    @GetMapping("/list")
    public TableDataInfo list(SysDictType dictType)
    {
    startPage();
    List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
    return getDataTable(list);
    }
  2. 验证用户权限(返回 true 表示鉴权通过)
    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
    @Service("ss")  
    public class PermissionService
    {
    /**
    * 验证用户是否具备某权限
    *
    * @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);
    }
    /**
    * 判断是否包含权限
    *
    * @param permissions 权限列表
    * @param permission 权限字符串
    * @return 用户是否具备某权限
    */
    private boolean hasPermissions(Set<String> permissions, String permission)
    {
    return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }

前端组件级别鉴权

  1. 为组件添加定制化属性(v-hasPermi=['权限标签']
    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
    <el-row :gutter="10" class="mb8">  
    <el-col :span="1.5">
    <el-button v-hasPermi="['system:post:add']"
    icon="Plus"
    plain
    type="primary"
    @click="handleAdd"
    >新增
    </el-button>
    </el-col> <el-col :span="1.5">
    <el-button v-hasPermi="['system:post:edit']"
    :disabled="single"
    icon="Edit"
    plain
    type="success"
    @click="handleUpdate"
    >修改
    </el-button>
    </el-col> <el-col :span="1.5">
    <el-button v-hasPermi="['system:post:remove']"
    :disabled="multiple"
    icon="Delete"
    plain
    type="danger"
    @click="handleDelete"
    >删除
    </el-button>
    </el-col> <el-col :span="1.5">
    <el-button v-hasPermi="['system:post:export']"
    icon="Download"
    plain
    type="warning"
    @click="handleExport"
    >导出
    </el-button>
    </el-col> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
  2. 解析定制化权限属性
    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
    /**  
    * v-hasPermi 操作权限处理
    * Copyright (c) 2019 ruoyi
    */
    import useUserStore from '@/store/modules/user'
    export default {
    /**
    * 组件生命周期钩子函数,组件挂载后执行
    * @param {HTMLElement} el - 组件元素节点
    * @param {Object} binding - 绑定对象
    * @param {Object} vnode - VNode 节点对象
    */
    mounted(el, binding, vnode) {
    const {value} = binding
    const all_permission = "*:*:*";//声明所有权限标志
    const permissions = useUserStore().permissions//获取当前用户的权限
    if (value && value instanceof Array && value.length > 0) {
    const permissionFlag = value // 组件所需权限标识
    /**
    * 检查是否具有某些权限
    * @type {boolean}
    */
    const hasPermissions = permissions.some(permission => {
    return all_permission === permission || permissionFlag.includes(permission) //判断用户权限是否为超级管理员或者包含组件所需权限
    })
    if (!hasPermissions) {
    el.parentNode && el.parentNode.removeChild(el)
    }
    } else {
    throw new Error(`请设置操作权限标签值`)
    }
    }
    }
  3. vue app 全局使用组件权限属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //directive/index.js
    import hasRole from './permission/hasRole'
    import hasPermi from './permission/hasPermi'
    import copyText from './common/copyText'
    export default function directive(app){
    app.directive('hasRole', hasRole)
    app.directive('hasPermi', hasPermi) //app设置鉴权属性
    app.directive('copyText', copyText)
    }
    //main.js
    app.use(router)
    app.use(store)
    app.use(plugins)
    app.use(elementIcons)
    directive(app) //app使用全局属性
    // 使用element-plus 并且设置全局的大小
    app.use(ElementPlus, {
    locale: locale,
    // 支持 large、default、small
    size: Cookies.get('size') || 'default'
    })
    app.mount('#app')

参考

  1. 若依项目分析二次开发_哔哩哔哩_bilibili

若依脚手架-学习
https://alleyf.github.io/2023/12/3727d5849252.html
作者
fcs
发布于
2023年12月4日
更新于
2023年12月7日
许可协议