在常用的后台管理系统中,通常都会有权限系统设计,以用于给对应人员分配不同权限,控制其对后管系统中的某些菜单、按钮以及列表数据的可见性。
本文将用 waynboot-mall 项目举例,给大家介绍常见后管系统的权限控制该如何设计。大纲如下,
权限模型
要理解权限控制,我们需要先了解什么是权限模型。
权限模型是指用于描述用户、角色和权限之间关系的一种抽象模型。不同的权限模型有不同的优缺点,适用于不同的场景和需求。在本项目中,我们采用了 RBAC(Role-Based Access Control)模型,即基于角色的访问控制模型。
RBAC 模型的基本思想是将用户和权限分离,通过角色作为中间层来连接用户和权限。一个角色可以关联多个权限,一个用户可以拥有多个角色。这样可以实现灵活的权限配置和管理,避免直接给用户分配权限带来的复杂性和冗余性。
RBAC 模型有多个扩展版本,如 RBAC0、RBAC1、RBAC2 等。在本项目中,我们使用了 RBAC0 模型,即最基本的 RBAC 模型。RBAC0 模型包含三个要素:用户(User)、角色(Role)和权限(Permission)。用户是指使用系统的主体,角色是指一组相关的权限的集合,权限是指对系统资源的访问或操作能力。
在 waynboto-mall 项目中,RBAC0 中的权限对应的就是菜单。菜单权限包含菜单页面对用户是否可见、页面按钮对用户是否可见、页面列表数据根据用户进行过滤等。
权限要素
在 RBAC0 模型中,我们需要对用户、角色和权限进行定义和梳理。具体来说,我们需要确定以下几个方面:
- 用户的来源和属性:用户是从哪里获取的?用户有哪些属性?如用户名、密码、昵称、手机号、邮箱等。
- 角色的命名和分类:角色是如何命名的?角色有哪些分类?如按照部门、职位、功能等进行划分。
- 权限的类型和范围:权限有哪些类型?权限涉及哪些资源?如页面权限、操作权限、数据权限等。
- 用户、角色和权限之间的关联方式:用户如何与角色关联?角色如何与权限关联?如一对一、一对多、多对多等。
在 waynboto-mall 项目中,我做了以下的定义和梳理:
用户设计
用户来源于系统内部注册或外部导入,用户有用户名、密码、姓名、手机号、邮箱等属性。表结构如下,
CREATE TABLE `sys_user` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
`user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户账号',
`nick_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户昵称',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '用户邮箱',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '手机号码',
`sex` tinyint DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '头像地址',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '密码',
`user_status` tinyint DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
`del_flag` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE KEY `user_name_uqi` (`user_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息表';
角色设计
角色按照功能模块进行命名,如商品管理、订单管理、营销管理等。角色可以分为普通角色和超级管理员角色,普通角色可以拥有部分或全部功能模块的权限,超级管理员角色可以拥有所有功能模块的权限,并且可以管理其他用户和角色。表结构如下
CREATE TABLE `sys_role` (
`role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名称',
`role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色权限字符串',
`sort` int NOT NULL COMMENT '显示顺序',
`role_status` tinyint NOT NULL COMMENT '角色状态(0正常 1停用)',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
`del_flag` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='角色信息表';
超级管理员也就是 rule_key 为 admin 角色,赋予了 admin 角色用户拥有系统的绝对控制能力。
权限(菜单)设计
权限分为页面权限、操作权限和数据权限。页面权限控制用户可以看到哪些页面或菜单,操作权限控制用户可以在页面上执行哪些操作或按钮,数据权限控制用户可以查看或修改哪些数据或范围。表结构如下,
CREATE TABLE `sys_menu` (
`menu_id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '菜单名称',
`parent_id` bigint DEFAULT '0' COMMENT '父菜单ID',
`sort` int DEFAULT '0' COMMENT '显示顺序',
`path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '路由地址',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '组件路径',
`is_frame` tinyint DEFAULT '1' COMMENT '是否为外链(0是 1否)',
`menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`menu_status` tinyint DEFAULT NULL COMMENT '菜单状态(0启用 1禁用)',
`visible` tinyint DEFAULT '0' COMMENT '显示状态(0显示 1隐藏)',
`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '#' COMMENT '菜单图标',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '备注',
PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2055 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='菜单权限表';
用户角色关联设计
用户与角色之间是多对多的关联方式,即一个用户可以拥有多个角色,一个角色可以分配给多个用户。角色与权限之间也是多对多的关联方式,即一个角色可以拥有多个权限,一个权限可以分配给多个角色。用户角色关联表、角色菜单关联表结构如下,
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户和角色关联表';
CREATE TABLE `sys_role_menu` (
`role_id` bigint NOT NULL COMMENT '角色ID',
`menu_id` bigint NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`role_id`,`menu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='角色和菜单关联表';
后管权限设计 ER 图
waynboot-mall 项目的后台权限系统设计相关 er 图如下,
权限分配
当我们理解清楚后权限模型后,就该进行具体的权限分配了。在进行权限分配时,我们需要遵循以下几个原则:
- 权限分配要合理:不同的角色应该拥有与其职责相符合的权限,不应该给予过多或过少的权限。
- 权限分配要灵活:不同的场景和需求可能需要调整权限配置,应该提供方便和快捷的方式来进行权限变更。
- 权限分配要安全:权限变更应该有明确的审批和记录流程,避免因为权限错误或滥用导致系统风险(也就是权限相关的操作需要有日志记录)。
在 waynboto-mall 项目中,我们采用了以下几种方式来进行权限分配:
用户角色分配
通过给用户分配角色来实现权限分配:这是最常见和最基本的方式,通过勾选用户拥有的角色来控制用户拥有的权限。
角色权限分配
通过给角色分配权限来实现权限分配:这是最灵活和最细粒度的方式,通过勾选角色拥有的权限来控制角色拥有的权限。
超管角色定义
通过设置超级管理员角色(role_key 为 admin)来实现全局权限管理:这是最简单和最高效的方式,通过设置一个超级管理员来控制所有功能模块和数据范围的访问和操作。
waynboot-mall 项目的访问控制框架是采用的 Spring Security 3.0版本。我在这里给大家先介绍 Spring Security 的相关知识以及 3.0 版本中的配置类代码编写。
Spring Security
一、什么是 Spring Security
Spring Security 是一个基于 Spring 框架的开源项目,旨在为 Java 应用程序提供强大和灵活的安全性解决方案。Spring Security 提供了以下特性:
- 认证:支持多种认证机制,如表单登录、HTTP 基本认证、OAuth2、OpenID 等。
- 授权:支持基于角色或权限的访问控制,以及基于表达式的细粒度控制。
- 防护:提供了多种防护措施,如防止会话固定、点击劫持、跨站请求伪造等攻击。
- 集成:与 Spring 框架和其他第三方库和框架进行无缝集成,如 Spring MVC、Thymeleaf、Hibernate 等。
二、如何引入 Spring Security
在 waynboot-mall 项目中直接引入 spring-boot-starter-security 依赖,
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
三、如何配置 Spring Security
在 Spring Security 3.0 中要配置 Spring Security 跟以往是有些不同的,比如不在继承 WebSecurityConfigurerAdapter。在 waynboot-mall 项目中,具体配置如下,
@Configuration
@EnableWebSecurity
@AllArgsConstructor
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
private UserDetailsServiceImpl userDetailsService;
private AuthenticationEntryPointImpl unauthorizedHandler;
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private LogoutSuccessHandlerImpl logoutSuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// cors启用
.cors(httpSecurityCorsConfigurer -> {})
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(httpSecuritySessionManagementConfigurer -> {
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(unauthorizedHandler);
})
// 过滤请求
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
authorizationManagerRequestMatcherRegistry
.requestMatchers("/favicon.ico", "/login", "/favicon.ico", "/actuator/**").anonymous()
.requestMatchers("/slider/**").anonymous()
.requestMatchers("/captcha/**").anonymous()
.requestMatchers("/upload/**").anonymous()
.requestMatchers("/common/download**").anonymous()
.requestMatchers("/doc.html").anonymous()
.requestMatchers("/swagger-ui/**").anonymous()
.requestMatchers("/swagger-resources/**").anonymous()
.requestMatchers("/webjars/**").anonymous()
.requestMatchers("/*/api-docs").anonymous()
.requestMatchers("/druid/**").anonymous()
.requestMatchers("/elastic/**").anonymous()
.requestMatchers("/message/**").anonymous()
.requestMatchers("/ws/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})
.headers(httpSecurityHeadersConfigurer -> {
httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);
});
// 处理跨域请求中的Preflight请求(cors),设置corsConfigurationSource后无需使用
// .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 对于登录login 验证码captchaImage 允许匿名访问
httpSecurity.logout(httpSecurityLogoutConfigurer -> {
httpSecurityLogoutConfigurer.logoutUrl("/logout");
httpSecurityLogoutConfigurer.logoutSuccessHandler(logoutSuccessHandler);
});
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 认证用户时用户信息加载配置,注入springAuthUserService
httpSecurity.userDetailsService(userDetailsService);
return httpSecurity.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
这里详细介绍下 SecurityConfig 配置类,
- filterChain(HttpSecurity httpSecurity) 方法是访问控制的核心方法,这里面可以针对 url 设置是否需要权限认证、cors配置、csrf配置、用户信息加载配置、jwt过滤器拦截配置等众多功能。
- authenticationManager(AuthenticationConfiguration authenticationConfiguration) 方法适用于启用认证接口,需要手动声明,否则启动报错。
- bCryptPasswordEncoder() 方法用户定义用户登录时的密码加密策略,需要手动声明,否则启动报错。
四、如何使用 Spring Security
要使用 Spring Security,只需要在需要控制访问权限的方法或类上添加相应的 @PreAuthorize 注解即可,如下,
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("system/role")
public class RoleController extends BaseController {
private IRoleService iRoleService;
@PreAuthorize("@ss.hasPermi('system:role:list')")
@GetMapping("/list")
public R list(Role role) {
Page<Role> page = getPage();
return R.success().add("page", iRoleService.listPage(page, role));
}
}
我们在 list 方法上加了 @PreAuthorize("@ss.hasPermi('system:role:list')") 注解表示当前登录用户拥有 system:role:list 权限才能访问 list 方法,否则返回权限错误。
五、当前登录用户权限
在 SecurityConfig 配置类中我们定义了 UserDetailsServiceImpl 作为我们的用户信息加载的实现类,从而通过读取数据库中用户的账号、密码与前端传入的账号、密码进行比对。代码如下,
@Slf4j
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private IUserService iUserService;
private IDeptService iDeptService;
private PermissionService permissionService;
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
System.out.println(bCryptPasswordEncoder.encode("123456"));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 读取数据库中当前用户信息
User user = iUserService.getOne(new QueryWrapper<User>().eq("user_name", username));
// 2. 判断该用户是否存在
if (user == null) {
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
// 3. 判断是否禁用
if (Objects.equals(UserStatusEnum.DISABLE.getCode(), user.getUserStatus())) {
log.info("登录用户:{} 已经被停用.", username);
throw new DisabledException("登录用户:" + username + " 不存在");
}
user.setDept(iDeptService.getById(user.getDeptId()));
// 4. 获取当前用户的角色信息
Set<String> rolePermission = permissionService.getRolePermission(user);
// 5. 根据角色获取权限信息
Set<String> menuPermission = permissionService.getMenuPermission(rolePermission);
return new LoginUserDetail(user, menuPermission);
}
}
针对 UserDetailsServiceImpl 的代码逻辑进行一个讲解。
- 读取数据库中当前用户信息
- 判断该用户是否存在
- 判断是否禁用
- 获取当前用户的角色信息
- 根据角色获取权限信息
总结一下
本文给大家讲解了常见后管系统的权限控制系统该如何设计以及 Spring Security 实战,在常用的 RBAC0 权限模型下,权限要素包含用户、角色、权限(菜单)三要素,只要大家能理解用户、角色、权限(菜单)三要素的设计理念以及表结构后,相信就能轻松掌握后管权限控制系统的设计精髓。
想要获取 waynboot-mall 项目源码的同学,可以关注我公众号【程序员wayn】,回复 waynboot-mall 即可获得。