一文上手SpringSecurity【八】

RBAC(Role-Based Access Control),基于角色的访问控制。通过用户关联角色,角色关联权限,来间接的为用户赋予权限。

一、RBAC介绍

RBAC(Role-Based Access Control),即基于角色的访问控制模型。

1.1 基本概念

  • 角色(Role):
    • 角色是 RBAC 模型的核心概念之一。它是一组权限的集合,代表了特定的工作职责或功能。例如,可以有 “管理员”“普通用户”“财务人员” 等不同的角色。
    • 角色将用户与权限进行解耦,使得权限管理更加清晰和易于维护。
  • 用户(User)
    • 用户是系统的使用者。在 RBAC 模型中,用户通过被分配到不同的角色来获得相应的权限。
    • 一个用户可以被分配多个角色,从而拥有多个角色所对应的权限。
  • 权限(Permission)
    • 权限定义了对系统资源的具体操作许可。例如,可以是对某个文件的读取权限、对数据库表的修改权限等。
    • 权限通常与系统中的具体资源和操作相关联。

1.2 基本工作原理

  • 用户分配角色:

    • 系统管理员将不同的角色分配给用户。用户在登录系统后,根据其被分配的角色来确定拥有的权限。
    • 例如,新员工入职时,管理员可以根据其工作职责为其分配 “普通员工” 角色。
  • 角色关联权限:

    • 管理员将各种权限分配给不同的角色。每个角色拥有一组特定的权限集合。
    • 比如,“管理员” 角色可能拥有对系统所有资源的读写和管理权限,而 “普通用户” 角色可能只有对部分资源的读取权限。
  • 权限控制:

    • 当用户在系统中进行操作时,系统会根据用户的角色来判断其是否具有执行该操作的权限。
    • 如果用户具有相应的权限,则操作被允许;否则,操作被拒绝。

1.3 数据库表

1

create database if not exists `rj-security-db`;
use `rj-security-db`;

-- 用户表
DROP TABLE IF EXISTS `tb_sys_user`;
CREATE TABLE `tb_sys_user`  (
                                `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
                                `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
                                `nick_name` varchar(150) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
                                `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
                                `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
                                PRIMARY KEY (`id`) USING BTREE,
                                UNIQUE INDEX `name`(`name` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户管理' ROW_FORMAT = DYNAMIC;
INSERT INTO `tb_sys_user` VALUES (4, 'zhangsan', '张三', '$2a$10$dCr8Skk7kLa2kNCms.23aeiYI2RS2vrdoSae6Jz3.0w.YCiu9lmT2', NULL);
INSERT INTO `tb_sys_user` VALUES (5, 'jack', '杰克', '$2a$10$dCr8Skk7kLa2kNCms.23aeiYI2RS2vrdoSae6Jz3.0w.YCiu9lmT2', NULL);

-- 角色表
DROP TABLE IF EXISTS `tb_sys_role`;
CREATE TABLE `tb_sys_role`  (
                         `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
                         `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名称',
                         `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
                         PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色管理' ROW_FORMAT = DYNAMIC;

INSERT INTO `tb_sys_role` VALUES (10, '超级管理员', NULL);
INSERT INTO `tb_sys_role` VALUES (11, '技术经理', NULL);
INSERT INTO `tb_sys_role` VALUES (12, '财务总监', NULL);
INSERT INTO `tb_sys_role` VALUES (13, '研发工程师', NULL);
INSERT INTO `tb_sys_role` VALUES (14, '人事专员', NULL);
INSERT INTO `tb_sys_role` VALUES (15, '产品经理', NULL);

-- 权限表【菜单表】
DROP TABLE IF EXISTS `tb_sys_menu`;
CREATE TABLE `tb_sys_menu`  (
                                `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
                                `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单名称',
                                `parent_id` bigint NULL DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
                                `url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单URL,类型:1.普通页面(如用户管理, /sys/user)2.嵌套完整外部页面,以http(s)开头的链接 ',
                                `perms` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:sys:user:add,sys:user:edit)',
                                `type` int NULL DEFAULT NULL COMMENT '类型   0:目录   1:菜单   2:按钮',
                                `icon` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单图标',
                                `order_num` int NULL DEFAULT NULL COMMENT '排序',
                                `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
                                PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 64 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单管理' ROW_FORMAT = DYNAMIC;

INSERT INTO `tb_sys_menu` VALUES (64, '用户管理', 0, '/user/list', 'sys:user:list', NULL, NULL, NULL, NULL);
INSERT INTO `tb_sys_menu` VALUES (65, '用户添加', 64, '/user/add', 'sys:user:add', NULL, NULL, NULL, NULL);
INSERT INTO `tb_sys_menu` VALUES (66, '用户删除', 64, '/user/delete', 'sys:user:delete', NULL, NULL, NULL, NULL);
INSERT INTO `tb_sys_menu` VALUES (67, '用户更新', 64, '/user/update', 'sys:user:update', NULL, NULL, NULL, NULL);
INSERT INTO `tb_sys_menu` VALUES (68, '商品管理', 0, '/goods/list', 'sys:goods:list', NULL, NULL, NULL, NULL);
INSERT INTO `tb_sys_menu` VALUES (69, '商品添加', 68, '/goods/add', 'sys:goods:add', NULL, NULL, NULL, NULL);
INSERT INTO `tb_sys_menu` VALUES (70, '商品删除', 68, '/goods/delete', 'sys:goods:delete', NULL, NULL, NULL, NULL);
INSERT INTO `tb_sys_menu` VALUES (71, '商品更新', 68, '/goods/update', 'sys:goods:update', NULL, NULL, NULL, NULL);

-- 用户-角色-中间表
DROP TABLE IF EXISTS `tb_sys_user_role`;
CREATE TABLE `tb_sys_user_role`  (
                                     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
                                     `user_id` bigint NULL DEFAULT NULL COMMENT '用户ID',
                                     `role_id` bigint NULL DEFAULT NULL COMMENT '角色ID',
                                     `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
                                     PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色' ROW_FORMAT = DYNAMIC;

INSERT INTO `tb_sys_user_role` VALUES (5, 4, 10, '2025-05-18 11:16:27');
INSERT INTO `tb_sys_user_role` VALUES (6, 4, 11, '2025-05-18 11:16:35');
INSERT INTO `tb_sys_user_role` VALUES (7, 5, 15, '2025-05-18 11:16:37');
INSERT INTO `tb_sys_user_role` VALUES (8, 5, 13, '2024-05-18 11:16:39');
INSERT INTO `tb_sys_user_role` VALUES (9, 5, 14, '2024-05-18 11:16:41');

-- 角色-权限【菜单】中间表
DROP TABLE IF EXISTS `tb_sys_role_menu`;
CREATE TABLE `tb_sys_role_menu`  (
                                     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
                                     `role_id` bigint NULL DEFAULT NULL COMMENT '角色ID',
                                     `menu_id` bigint NULL DEFAULT NULL COMMENT '菜单ID',
                                     `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
                                     PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 632 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色菜单' ROW_FORMAT = DYNAMIC;

INSERT INTO `tb_sys_role_menu` VALUES (632, 10, 64, '2023-05-18 11:17:50');
INSERT INTO `tb_sys_role_menu` VALUES (633, 10, 65, '2023-10-18 11:18:02');
INSERT INTO `tb_sys_role_menu` VALUES (634, 10, 66, '2024-09-18 11:18:18');
INSERT INTO `tb_sys_role_menu` VALUES (635, 10, 67, '2029-05-18 11:18:27');
INSERT INTO `tb_sys_role_menu` VALUES (636, 10, 68, '2024-05-23 11:18:37');
INSERT INTO `tb_sys_role_menu` VALUES (637, 10, 69, '2025-06-18 11:18:49');
INSERT INTO `tb_sys_role_menu` VALUES (638, 10, 70, '2033-09-18 11:19:01');
INSERT INTO `tb_sys_role_menu` VALUES (639, 11, 64, '2027-05-18 11:19:23');
INSERT INTO `tb_sys_role_menu` VALUES (640, 11, 65, '2036-06-18 11:19:39');
INSERT INTO `tb_sys_role_menu` VALUES (641, 15, 68, '2024-12-18 11:20:00');
INSERT INTO `tb_sys_role_menu` VALUES (642, 15, 71, '2022-05-18 11:20:14');
INSERT INTO `tb_sys_role_menu` VALUES (643, 14, 64, '2024-06-21 11:20:50');
INSERT INTO `tb_sys_role_menu` VALUES (644, 14, 65, '2028-05-18 11:21:10');

RBAC跟语言和框架没有啥关系,纯纯一种技术解决方案而已, 不同的安全框架都有不同的实现. 上述五张表,结合自己的实际需求,自行修改,这里我们只是为了做测试而已.

二、spring security 授权处理

2.1 授权的通俗解释

  • 张三是一个普通的个人, 这相当于普通用户
  • 村长、县长、市长这些相当于角色
  • 简单理解, 村长只能管理一个村子, 县长能管理一个县, 市长管理一个市…, 这是权限

张三可以成为一个【村长】, 可以【管理一个村子】, 从大的粒度考量, 因为张三是村长,所以能管理一个村子, 并不是它叫张三, 所以这里可以抽象出来【基于角色信息的权限控制】.

现在张三是一个村长了, 村长可以管理村里的纠纷, 管理村里的财政, 不管是管理纠纷还是村里的财政这些都是村长这个角色对应的一条一条的权限.

以上就是用户、角色、权限的通俗解释,当然它们用户-角色-权限【菜单】之间的关系都是多对多的.

2.2 授权处理

在接口UserDetails 当中

public interface UserDetails extends Serializable {
	// 权限集合
	Collection<? extends GrantedAuthority> getAuthorities();
}
public interface GrantedAuthority extends Serializable {
	String getAuthority();
}

通过以上的接口定义,可以看出来,所谓的权限就是一个字符串而已. 我们常用的一个实现类就是: SimpleGrantedAuthority, 可以查看一下它的类定义

public final class SimpleGrantedAuthority implements GrantedAuthority {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final String role;

	public SimpleGrantedAuthority(String role) {
		Assert.hasText(role, "A granted authority textual representation is required");
		this.role = role;
	}

	@Override
	public String getAuthority() {
		return this.role;
	}
 	// 略...
}

通过调用SimpleGrantedAuthority构建方法,可以将一个普通的字符串转换为SimpleGrantedAuthority对象被spring security所识别.
另外一个就是SimpleGrantedAuthority是没有无参构造的,这一点再执行它的对象的序列化、反序列化打时候要特别注意.

2.3 权限字符串

基于角色的授权规则包括 ROLE_ 作为前缀, 也可以通过如下的方式进行修改.

public final class GrantedAuthorityDefaults {

	private final String rolePrefix;

	public GrantedAuthorityDefaults(String rolePrefix) {
		this.rolePrefix = rolePrefix;
	}

	/**
	 * The default prefix used with role based authorization. Default is "ROLE_".
	 * @return the default role prefix
	 */
	public String getRolePrefix() {
		return this.rolePrefix;
	}
}
@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
	return new GrantedAuthorityDefaults("RJ_");
}

使用静态方法公开GrantedAuthorityDefaults,以确保 Spring 在初始化 Spring Security 的方法安全性@Configuration类之前发布它

权限字符串分隔符一般以冒号者分隔, 譬如说: sys:user:create, 表示系统模块下的用户服务的创建权限.当然这并不是必须的,一切结合公司规则为准则.

2.4 添加权限字符串

在UserDetailsServiceImpl中,也就是UserDetailsService的实现类,此处我们之前传递的是个空集合,现在传递一些权限字符串.
修改代码如下所示:
2
修改TokenAuthenticationFilter即token的校验过滤器
3

2.5 开启授权处理

前置条件: 授权的前端是当前用户必须经过认证

spring security当中,会使用默认的FilterSecrityInterceptor来进行权限的校验。

  • 从FitlerSecurityInterceptor当中会从SecurityContextHolder获取其中的Authentication, 然后获取权限信息,当前用户是否人的了该资源的访问的权限.
  • 我们需要把当前用户的具体权限也存储到Authentication当点,然后设置我们的资源所需要的权限即可.
  • 使用时候,配置类配置开启注解:@EnableMethodSecurity
  • @EnableMethodSecurity, 开启基于方法的授权
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MethodSecuritySelector.class)
public @interface EnableMethodSecurity {
}

这里有两个特别重要的属性:

  • boolean prePostEnabled() default true;表示注解@PreAuthorize、 @PostAuthorize、 @PreFilter和 @PostFilter是否开启,默认就是【开启】状态
  • boolean securedEnabled() default false; 表示注解@secured是否开启, true,表示开启, false,关闭

2.6 编写测试接口

在HomeController当中, 添加如下测试代码

 // 测试权限字符串
 // 表示只有当前用户有: ROLE_user这个角色才测出访问这个接口
 @Secured("ROLE_user")
 @GetMapping("/api/pub/v1/list")
 public Result menu(){
     List<String> menuList = new ArrayList<>();
     menuList.add("商品管理");
     menuList.add("用户管理");
     menuList.add("查看商品");
     menuList.add("查看客户信息");
     return Result.success(0, "获取列表", menuList);
 }

@Secured(“ROLE_user”), 于角色的权限控制注解。可以指定一个或多个角色,只有具有这些角色的用户才能执行被注解的方法。可以指定一个或者多个角色的字符串,写法简单,但是功能也十分简单,以上表示,当前认证的用户如果拥有ROLE_user这个角色, 那么接口可以访问,如果当前认证的用户没有ROLE_user角色,那么无法访问接口,会抛出异常.

2.7 前端测试工程添加代码并验证

// 测试接口,主要验证权限字符串
export const getMneuListTest = () => $http({url: '/list', method: 'get'})
const m1 = async () => {
    const {code, msg, data} = await getMneuListTest()
    if(code === 0){
        content.value = data
    }else {
        ElMessage.error({
            type: 'error',
            message: msg || '操作异常',
            showClose: true
        })
    }
}

3
启动服务器测试
5
在这里插入图片描述
再添加一个测试接口

// 测试只有admin能看的数据
@Secured("ROLE_admin")
@GetMapping("/api/pub/v1/admin")
public Result admin(){
    return Result.success(0, "操作成功", "数据只有admin角色能看");
}

前端写接口,访问即可,可以看到,由于当前认证的用户并没有User_admin这个角色,返回访问接口会返回403.
6

2.8 授权异常自定义处理

如果用户没有相关访问去访问资源,默认会抛出AccessDeniedException异常, 当该异常发生的时候,我们可以通过AccessDeniedHandler接口,进行自定义处理.在spring security配置文件当中进行配置, 如下所示:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity.authorizeHttpRequests(authorize -> {
                try {
                    authorize.requestMatchers("/api/pub/v1/login").permitAll()
                            .requestMatchers("/static/**", "/resources/**").permitAll()
                            .anyRequest().authenticated();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }).csrf(AbstractHttpConfigurer::disable)
            // 将我们自己定义的过滤器添加到 UsernamePasswordAuthenticationFilter过滤器之前
            .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .cors(c -> {
            	// 处理spring security跨域配置
                c.configurationSource(apiConfigurationSource());
            }).exceptionHandling(exception -> exception.accessDeniedHandler(new AccessDeniedHandler() { // 处理未授权的情况, 访问资源的情况.
                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException, IOException {
                    response.setContentType("application/json;charset=utf-8");
                    Result result = Result.error(403, "您没有权限操作", "权限不足,请联系管理员!");
                    PrintWriter writer = response.getWriter();
                    writer.write(JSON.toJSONString(result));
                }
            }))
            .build();
}

再次测试
5
异常信息
90

2.9 验证权限字符串

编写测试接口

// 只有拥有sys:user:list权限才能访问
@PreAuthorize(value = "hasAuthority('sys:user:list')")
@GetMapping("/api/pub/v1/user")
public Result userList(){
  return Result.success(0, "操作成功", "数据只有sys:user:list角色能看");
}

// 只有拥有sys:user:create权限才能访问
@PreAuthorize(value = "hasAuthority('sys:user:create')")
@GetMapping("/api/pub/v1/create")
public Result userCreate(){
  return Result.success(0, "操作成功", "数据只有sys:user:create角色能看");
}

@PreAuthorize(value = “hasAuthority(‘sys:user:create’)”), 该注解是我们最最常用的注解,没有之一.

  • @PreAuthorize是 Spring Security 中用于方法级别的访问控制的注解。
    • 权限检查:
      • 在方法执行之前,@PreAuthorize会根据指定的表达式来判断当前用户是否具有执行该方法的权限。如果表达式的结果为false,则会抛出一个访问拒绝异常,阻止方法的执行。
      • 这个注解可以用于控制对特定业务方法的访问,确保只有具有相应权限的用户才能执行这些方法。
    • 表达式语言支持
      • @PreAuthorize支持使用 SpEL(Spring Expression Language)表达式来定义权限检查逻辑。SpEL 提供了丰富的语法和功能,可以进行各种复杂的权限判断。
      • 例如,可以使用表达式检查用户的角色、权限、属性等信息,以确定是否允许执行方法。 ’

基于角色的权限检查使用示例:

  • @PreAuthorize(“hasRole(‘ADMIN’)”):表示只有具有 “ADMIN” 角色的用户才能执行该方法。
  • @PreAuthorize(“hasAnyRole(‘ADMIN’, ‘MANAGER’)”):表示具有 “ADMIN” 或 “MANAGER” 角色的用户可以执行该方法。

基于权限的权限检查

  • @PreAuthorize(“hasPermission(#order, ‘WRITE’)”):假设存在一个自定义的权限检查方法hasPermission,这个注解表示只有当当前用户对给定的订单对象order具有 “WRITE” 权限时,才能执行该方法

基于用户属性的权限检查

  • @PreAuthorize(“authentication.principal.username.equals(‘admin’)”):表示只有用户名是 “admin” 的用户才能执行该方法。

56
123

2.10 从表中查询出权限

根据用户id,查询出对应的角色信息

@Mapper
public interface TbSysUserMapper extends BaseMapper<TbSysUser> {
  @Select("select * from tb_sys_role where id in (select role_id from tb_sys_user_role where user_id = #{id})")
  List<TbSysRole> findSysRoleByUserId(Long id);

  @Select("select * from tb_sys_menu where id in (select menu_id from tb_sys_role_menu where role_id in (select role_id from tb_sys_user_role where user_id = #{id}))")
  List<TbSysMenu> findSysMenuByUserId(Long id);
}

单元测试一下
zhangsan账号id = 4, 角色信息如下所示
123
zhangsan账号id = 4的权限列表
345
jack的账号id=5, 角色信息如下所示
78
jack的账号id=5, 权限信息
222
修改UserDetailsServiceImp获取权限列表
1234
TokenAuthenticationFilter获取权限列表
21
编写一个测试接口

// 只有拥有sys:goods:delete权限才能访问
@PreAuthorize(value = "hasAuthority('sys:goods:delete')")
@GetMapping("/api/pub/v1/delete")
public Result userDelete(){
   return Result.success(0, "操作成功", "商品删除成功: 数据只有sys:goods:delete权限能看");
}

登录zhangsan账号
121
99
登录jack账号
543
1234
验证完成

三、总结

  • RBAC模型五张表
  • 权限校验的三个注解
    • @EnableMethodSecurity(securedEnabled = true)
    • @PreAuthorize
    • @Secured
  • MySQL多表查询

注意事项:

  • 在token校验的过滤器当中,频繁的操作数据库,可能会带来性能瓶颈, 我们可以引入Redis来存储认证对象,避免频繁的操作数据库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值