目录
1、若依-权限控制
1.1、介绍
权限控制
主要目的是保护系统的安全性和完整性,防止未经授权的用户获取敏感信息、执行非法操作或对系统进行恶意操作 。
常见的权限控制框架有SpringSecurity和Shiro。
若依的权限管理是通过RBAC
(Role-based Access Control 基于角色的访问控制)模型自己设计的。
RBAC模型将权限控制分为角色管理和权限管理两个部分。在若依中,角色是指对系统的一类用户或操作者的定义,而权限是指对系统中某个资源或操作的访问控制。通过为每个角色分配相应的权限,可以实现对系统的全面管理和控制。
具体来说,若依的权限管理包括以下几个方面:
-
菜单管理
:通过对系统菜单进行管理,可以控制用户在系统中能够访问的页面和功能。 -
按钮权限
:在系统中,某些操作需要特定的权限才能进行,例如删除、修改等操作。通过对按钮权限的控制,可以限制用户对系统的访问和操作。 -
数据权限
:在某些情况下,需要根据用户的角色或部门来限制其对数据的访问。通过数据权限的设置,可以实现对数据的细粒度控制。 -
API接口权限
:在若依中,API也可以通过权限的方式进行控制。通过对API的权限进行管理,可以限制用户对API的访问和使用。
1.2、数据库表结构设计
在数据库表结构方面,若依采用了RBAC模型的设计。其中,主要包括以下表:
-
sys_menu
:存储系统菜单信息,包括菜单ID、菜单名称、访问路径、菜单类型等字段。 -
sys_role
:存储系统角色信息,包括角色ID、角色名称、角色标识、角色描述等字段。 -
sys_user
:存储系统用户信息,包括用户ID、用户名、密码、昵称、邮箱、电话等字段。 -
sys_role_menu
:存储角色和菜单之间的关联关系,包括角色ID和菜单ID两个字段。 -
sys_user_role
:存储用户和角色之间的关联关系,包括用户ID和角色ID两个字段。
通过这些表的设计,可以实现对系统中菜单、角色和用户的管理。同时,通过角色和菜单之间的关联关系,可以实现对菜单访问权限的控制。通过用户和角色之间的关联关系,可以实现对用户访问权限的控制。
1.3、菜单管理
目录、菜单和按钮的区别
在若依(RuoYi)中,菜单和目录是两个不同的概念,它们之间的区别如下:
1.目录(Directory):
目录是用来组织和分类菜单的容器。目录本身没有功能,它只是一个容器,可以包含若干个菜单。目录通常是一个抽象的概念,用于将一组相关的菜单组织在一起。
在若依中,目录是以“系统管理”、“系统监控”等大模块的方式组织菜单的,用于区分不同的功能模块。目录通常以左侧的菜单树的形式展现,用户可以通过点击不同的目录来展开或收缩对应的菜单列表。
2.菜单(Menu):
菜单是具有一定功能的操作项,通常是一组具有相同功能的页面或功能点的集合。每个菜单通常对应一个页面或者一个功能模块。
在若依中,菜单通常是以左侧的树形菜单的形式展现,用户可以通过点击不同的菜单来跳转到对应的页面或功能模块。每个菜单都有一个唯一的标识符,通常以URL的形式表示。
3.按钮(Button):
按钮是指菜单中的操作按钮,用于触发一些具体的操作。在若依中,按钮通常是与表格或表单等组件配合使用的,用于进行数据的增删改查等操作。按钮通常会与权限控制结合起来,只有拥有相应权限的用户才能看到并使用该按钮。
总的来说,目录、菜单、按钮是若依系统中的三种不同的概念。目录是为了方便管理菜单和模块,菜单是系统的核心功能模块,按钮是菜单中的具体操作按钮。在实际应用中,它们通常会结合起来,形成一个完整的用户界面和操作流程。
1.4、菜单权限
在若依中,实现不同用户看到不同的菜单可以通过以下步骤实现:
-
在数据库中维护菜单的权限信息,可以为每个菜单设置一个权限标识。
-
在用户登录系统时,将该用户所拥有的菜单权限信息从数据库中获取出来。
-
根据用户的菜单权限信息动态生成菜单,使用户只能看到其拥有权限的菜单。
用户登录之后会请求后端的SysMenuController#getRouters接口获取登录用户可访问的菜单数据:
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
from sys_menu m
left join sys_role_menu rm on m.menu_id = rm.menu_id
left join sys_user_role ur on rm.role_id = ur.role_id
left join sys_role ro on ur.role_id = ro.role_id
left join sys_user u on ur.user_id = u.user_id
where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0
order by m.parent_id, m.order_num
菜单类型(M目录 C菜单 F按钮);菜单状态(0显示 1隐藏)
前端会根据该接口返回的数据渲染出不同的菜单。
1.5、api接口权限
配置方法
每一个按钮基本上都会对应着一个后端的接口,前端会根据权限标志显示或者隐藏按钮,但是如果用户不点击按钮,直接通过http请求工具请求后端咋办?所以接口权限也是要有的,该权限和按钮上权限完全一致。
若依系统实现了这部分功能,实现模块:spzx-common-security
,比如,用户管理页面中的修改用户按钮对应的后端接口长这个样子。
@RequiresPermissions("system:user:edit")
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysUser user) {
...
return toAjax();
}
和其对应的前端按钮权限标志一样
前端控制
v-hasPermi="['system:user:edit']"
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
1.5.1、后台权限注解
@RequiresLogin
@RequiresLogin
注解用于配置接口要求用户必须登录才可访问
示例1: 以下代码表示必须登录才可访问
@RequiresLogin
public AjaxResult allCheckCart(){
return success();
}
@RequiresPermissions
@RequiresPermissions
注解用于配置接口要求用户拥有某(些)权限才可访问,它拥有两个参数
参数 | 类型 | 描述 |
---|---|---|
value | String[] | 权限列表 |
logical | Logical | 权限之间的判断关系,默认为Logical.AND |
示例1: 以下代码表示必须拥有system:user:add
权限才可访问
@RequiresPermissions("system:user:add")
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
示例2: 以下代码表示必须拥有system:user:add
和system:user:edit
权限才可访问
@RequiresPermissions({"system:user:add", "system:user:edit"})
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
示例3: 以下代码表示需要拥有system:user:add
或system:user:edit
权限才可访问
@RequiresPermissions(value = {"system:user:add", "system:user:edit"}, logical = Logical.OR)
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
@RequiresRoles
@RequiresRoles
注解用于配置接口要求用户拥有某(些)角色才可访问,它拥有两个参数
参数 | 类型 | 描述 |
---|---|---|
value | String[] | 角色列表 |
logical | Logical | 角色之间的判断关系,默认为Logical.AND |
示例1: 以下代码表示必须拥有admin
角色才可访问
@RequiresRoles("admin")
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
示例2: 以下代码表示必须拥有admin
和common
角色才可访问
@RequiresRoles({"admin", "common"})
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
示例3: 以下代码表示需要拥有admin
或common
角色才可访问
@RequiresRoles(value = {"admin", "common"}, logical = Logical.OR)
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
1.5.2、实现原理
实现原理:一个拦截器【HeaderInterceptor
】 + 一个AOP【PreAuthorizeAspect
】
springboot 拦截器与AOP执行顺序:
拦截器HeaderInterceptor负责解析用户token信息,并将解析出的用户信息存入ThreadLocal
中,然后PreAuthorizeAspect负责解析权限注解,判断当前用户是否拥有注解权限。
1.6、数据权限
实现模块:spzx-common-datascope
数据权限实现的关键在于DataScopeAspect这个AOP类。该类是一个切面类,凡是加上DataScope注解的方法,在执行的时候都会被它拦截。
该切面定义了五种权限范围
该切面的核心逻辑是“拼SQL”,方法执行之前,会给参数的一个params属性添加一个dataScope键值对,key为"dataScope",值为AND (" + sqlString.substring(4) + ")"样式的一段SQL,这段SQL会根据当前用户所在的部门以及当前用户角色的权限范围发生变化。
以用户列表查询为例,执行sql为
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
<if test="userId != null and userId != 0">
AND u.user_id = #{userId}
</if>
<if test="userName != null and userName != ''">
AND u.user_name like concat('%', #{userName}, '%')
</if>
<if test="status != null and status != ''">
AND u.status = #{status}
</if>
<if test="phonenumber != null and phonenumber != ''">
AND u.phonenumber like concat('%', #{phonenumber}, '%')
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(u.create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(u.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
</if>
<if test="deptId != null and deptId != 0">
AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
</select>
1.7、完善系统权限功能
我们以品牌管理为例,其他模块类似,自行完善
1.7.1、添加按钮权限数据
在菜单管理添加品牌管理对应的增删改查权限数据。
1.7.2、Controller类权限控制
@Tag(name = "品牌接口管理")
@RestController
@RequestMapping("/brand")
public class BrandController extends BaseController {
@Autowired
private IBrandService brandService;
/**
* 查询品牌列表
*/
@RequiresPermissions("product:brand:list")
@Operation(summary = "查询品牌列表")
@GetMapping("/list")
public TableDataInfo list(Brand brand) {
startPage();
List<Brand> list = brandService.selectBrandList(brand);
return getDataTable(list);
}
/**
* 获取品牌详细信息
*/
@RequiresPermissions("product:brand:query")
@Operation(summary = "获取品牌详细信息")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return success(brandService.selectBrandById(id));
}
/**
* 新增品牌
*/
@RequiresPermissions("product:brand:add")
@Operation(summary = "新增品牌")
@PostMapping
public AjaxResult add(@RequestBody @Validated Brand brand) {
brand.setCreateBy(SecurityUtils.getUsername());
return toAjax(brandService.insertBrand(brand));
}
/**
* 修改品牌
*/
@RequiresPermissions("product:brand:edit")
@Operation(summary = "修改品牌")
@PutMapping
public AjaxResult edit(@RequestBody @Validated Brand brand) {
brand.setUpdateBy(SecurityUtils.getUsername());
return toAjax(brandService.updateBrand(brand));
}
/**
* 删除品牌
*/
@RequiresPermissions("product:brand:remove")
@Operation(summary = "删除品牌")
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(brandService.deleteBrandByIds(ids));
}
@RequiresPermissions("product:brand:query")
@Operation(summary = "获取全部品牌")
@GetMapping("getBrandAll")
public AjaxResult getBrandAll() {
return success(brandService.selectBrandAll());
}
}
1.7.3、vue页面按钮控制
<template>
<div class="app-container">
<!-- 搜索表单 -->
...
<!-- 功能按钮栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['product:brand:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['product:brand:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['product:brand:remove']"
>删除</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 数据展示表格 -->
<el-table v-loading="loading" :data="brandList" @selection-change="handleSelectionChange">
...
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:brand:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:brand:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
...
</div>
</template>
2、若依-系统日志
实现模块:spzx-common-log
在实际开发中,对于某些关键业务,我们通常需要记录该操作的内容,一个操作调一次记录方法,每次还得去收集参数等等,会造成大量代码重复。 我们希望代码中只有业务相关的操作,在项目中使用注解来完成此项功能。
在需要被记录日志的controller
方法上添加@Log
注解,使用方法如下:
@Log(title = "用户管理", businessType = BusinessType.INSERT)
public AjaxResult addSave(...)
{
return success(...);
}
2.1、注解参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
title | String | 空 | 操作模块 |
businessType | BusinessType | OTHER | 操作功能(OTHER 其他、INSERT 新增、UPDATE 修改、DELETE 删除、GRANT 授权、EXPORT 导出、IMPORT 导入、FORCE 强退、GENCODE 生成代码、CLEAN 清空数据) |
operatorType | OperatorType | MANAGE | 操作人类别(OTHER 其他、MANAGE 后台用户、MOBILE 手机端用户) |
isSaveRequestData | boolean | true | 是否保存请求的参数 |
isSaveResponseData | boolean | true | 是否保存响应的参数 |
excludeParamNames | String[] | {} | 排除指定的请求参数 |
2.2、自定义操作功能
1、在BusinessType
中新增业务操作类型如:
/**
* 测试
*/
TEST,
2、在sys_dict_data
字典数据表中初始化操作业务类型
insert into sys_dict_data values(25, 10, '测试', '10', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', '2018-03-16 11-33-00', 'ry', '2018-03-16 11-33-00', '测试操作');
3、在Controller
中使用注解
@Log(title = "测试标题", businessType = BusinessType.TEST)
public AjaxResult test(...)
{
return success(...);
}
操作日志记录逻辑实现代码 LogAspect.java
登录系统(系统管理-操作日志)可以查询操作日志列表和详细信息。
2.3、完善系统日志
我们以品牌管理为例,其他模块类似,自行完善
/**
* 新增品牌
*/
@Log(title = "品牌管理", businessType = BusinessType.INSERT)
@RequiresPermissions("product:brand:add")
@Operation(summary = "新增品牌")
@PostMapping
public AjaxResult add(@RequestBody @Validated Brand brand) {
brand.setCreateBy(SecurityUtils.getUsername());
return toAjax(brandService.insertBrand(brand));
}
/**
* 修改品牌
*/
@Log(title = "品牌管理", businessType = BusinessType.UPDATE)
@RequiresPermissions("product:brand:edit")
@Operation(summary = "修改品牌")
@PutMapping
public AjaxResult edit(@RequestBody @Validated Brand brand) {
brand.setUpdateBy(SecurityUtils.getUsername());
return toAjax(brandService.updateBrand(brand));
}
/**
* 删除品牌
*/
@Log(title = "品牌管理", businessType = BusinessType.DELETE)
@RequiresPermissions("product:brand:remove")
@Operation(summary = "删除品牌")
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(brandService.deleteBrandByIds(ids));
}
3、若依-代码生成
大部分项目里其实有很多代码都是重复的,几乎每个基础模块的代码都有增删改查的功能,而这些功能都是大同小异, 如果这些功能都要自己去写,将会大大浪费我们的精力降低效率。所以这种重复性的代码可以使用代码生成。
实现模块:spzx-gen
3.1、介绍
若依(Ruoyi)项目的代码生成模块(spzx-gen)
中,使用了Apache Velocity作为模板引擎,根据模板(spzx-gen模块模板文件在resources/vm下面,我们可以根据需要自行调整模板。)
来生成具体的代码。
Velocity根据模板文件中的占位符和变量替换规则
,将元数据信息嵌入到生成的代码中,生成具体的代码文件。通过导入表结构和生成代码两个后端接口,实现了快速导入数据库表结构和生成代码的功能。导入表结构会从information_schema
数据库的tables和columns
表中查询表和列的信息,并插入到ruoyi数据库的gen_table
和gen_table_column
表中。生成代码时,会根据查询到的表和列信息,初始化Velocity模板引擎,并准备上下文信息,包括变量值信息。然后,读取模板文件,渲染模板,并将渲染后的内容添加到压缩流中生成zip压缩文件,供前后端下载使用。ruoyi-vue代码生成器大大提高了开发效率,使得开发人员能够快速生成符合规范的代码文件。
3.2、代码生成器的使用
通过以下步骤,若依可以根据数据库表的设计信息自动生成相应的代码文件,极大地提高了开发效率。开发人员可以根据生成的代码文件进行进一步的开发和定制。
以下是若依实现代码自动生成的一般流程:
3.2.1、修改配置文件
登录nacos:http://localhost:8848/nacos
在spzx-gen-dev.yml 修改以下两个配置:
1、数据库连接配置
我们需要对哪个数据库表生成代码,那么就连接哪个数据库,当前我们以"商品库"为例
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spzx-product?characterEncoding=utf-8&useSSL=false
username: root
password: root
2、代码生成器配置
# 代码生成
gen:
# 作者
author: atguigu
# 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
packageName: com.spzx.product
# 自动去除表前缀,默认是false
autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix:
3.2.2、引入数据库表
代码生成依赖gen_table和gen_table_column这两张表,spzx-product库中创建这两张表
-- ----------------------------
-- 代码生成业务表
-- ----------------------------
drop table if exists gen_table;
create table gen_table (
table_id bigint(20) not null auto_increment comment '编号',
table_name varchar(200) default '' comment '表名称',
table_comment varchar(500) default '' comment '表描述',
sub_table_name varchar(64) default null comment '关联子表的表名',
sub_table_fk_name varchar(64) default null comment '子表关联的外键名',
class_name varchar(100) default '' comment '实体类名称',
tpl_category varchar(200) default 'crud' comment '使用的模板(crud单表操作 tree树表操作)',
tpl_web_type varchar(30) default '' comment '前端模板类型(element-ui模版 element-plus模版)',
package_name varchar(100) comment '生成包路径',
module_name varchar(30) comment '生成模块名',
business_name varchar(30) comment '生成业务名',
function_name varchar(50) comment '生成功能名',
function_author varchar(50) comment '生成功能作者',
gen_type char(1) default '0' comment '生成代码方式(0zip压缩包 1自定义路径)',
gen_path varchar(200) default '/' comment '生成路径(不填默认项目路径)',
options varchar(1000) comment '其它生成选项',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (table_id)
) engine=innodb auto_increment=1 comment = '代码生成业务表';
-- ----------------------------
-- 代码生成业务表字段
-- ----------------------------
drop table if exists gen_table_column;
create table gen_table_column (
column_id bigint(20) not null auto_increment comment '编号',
table_id bigint(20) comment '归属表编号',
column_name varchar(200) comment '列名称',
column_comment varchar(500) comment '列描述',
column_type varchar(100) comment '列类型',
java_type varchar(500) comment 'JAVA类型',
java_field varchar(200) comment 'JAVA字段名',
is_pk char(1) comment '是否主键(1是)',
is_increment char(1) comment '是否自增(1是)',
is_required char(1) comment '是否必填(1是)',
is_insert char(1) comment '是否为插入字段(1是)',
is_edit char(1) comment '是否编辑字段(1是)',
is_list char(1) comment '是否列表字段(1是)',
is_query char(1) comment '是否查询字段(1是)',
query_type varchar(200) default 'EQ' comment '查询方式(等于、不等于、大于、小于、范围)',
html_type varchar(200) comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)',
dict_type varchar(200) default '' comment '字典类型',
sort int comment '排序',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
primary key (column_id)
) engine=innodb auto_increment=1 comment = '代码生成业务表字段';
3.2.3、启动spzx-gen模块
启动spzx-gen模块
3.2.4、代码生成
进入系统工具-代码生成页面,点击导入按钮,找到product表并导入,如下图所示
若依根据配置和模板,通过解析数据库表的元数据信息,自动生成对应的Java类、Mapper接口、Service类、Controller类等代码文件。
点击编辑按钮之后,跳转修改生成配置页面
点击预览按钮,可以查看生成的代码,点击“生成代码”,可以下载生成文件
此处生成的代码我们只作为测试,不使用。