2627 字
约 8 分钟
0
博客系统演示账号与游客角色实现:Spring Security RBAC 基于角色的访问控制

博客系统演示账号与游客角色实现:Spring Security RBAC 基于角色的访问控制

本小节属于补充内容,因为我本地开发的代码,需要兼顾发布到演示环境。在演示环境中,除了超级管理员角色外,为了方便小伙伴们登录后台查看效果,所以,还额外添加了一个游客账号,该账号的角色为游客,仅能执行查询相关操作,对于新增,修改,删除操作都是不允许的,防止将演示环境的数据搞的一团糟。

如下图所示,当游客登录后台管理系统后,并执行删除文章分类操作时,会被后端服务的 Spring Security 安全框架鉴权不通过,并返回前端错误提示:演示账号仅支持查询操作!

功能背景

本小节介绍如何在博客系统中为演示环境添加一个特殊账号——演示账号(游客角色)。该账号仅支持查询操作,不允许新增,修改,删除数据,以防止演示环境被误操作。

演示账号登录后执行删除文章分类等操作时,会通过 Spring Security 框架鉴权失败,返回 “演示账号仅支持查询操作!” 的错误提示。

PS:后续上传的小节代码中,可能会带有该功能,某些小伙伴下载小节源码后,在本地运行时,会报错 t_user_role 角色表不存在等问题,所以,把这块的功能也讲解一下,防止后续下载小节源码,跑起来报错,搞得一头雾水。

实现思路

  1. 使用数据库表存储用户角色信息,实现 RBAC(基于角色的访问控制)。
  2. 通过 UserDetailsService 查询用户角色。
  3. 使用 Spring Security 的方法级注解 @PreAuthorize 进行细粒度控制。
  4. 自定义 AccessDeniedHandler 返回友好提示。

1. 写死的角色

还记得前面小节实现的用户认证鉴权功能吗?在 UserDetailServiceImpl 这个类中 loadUserByUsername() 方法中,我们通过用户名去数据库中查询对应的用户记录,当用户存在时,则返回用户详情对象,里面包括了用户名,密码,角色信息。

这里的 authorities 角色,我们写死的字符串 ADMIN,用于表示管理员角色。要想实现演示用户功能仅支持查询,就需要围绕它做文章。

2. 创建角色表

首先,在数据库中创建一张角色表,DDL 建表语句如下:

CREATE TABLE `t_user_role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(60) NOT NULL COMMENT '用户名',
  `role` varchar(60) NOT NULL COMMENT '角色',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户角色表';

该表中,我们通过 username 作为关联字段,和 t_user 用户记录关联起来。role 字段用来表示角色名称。用户表和角色表的关系为多对多,一个用户可能有多个角色,而某个角色可能又被多位用户拥有。

2.1 新增两个用户

这里给 t_user 用户表保留了两条记录,admin 表示管理员账号,test 表示演示账号:

TIP:对应的加密密码,可以通过 PasswordEncoderConfig 配置类中的 main 方法来生成。

2.2 新增对应的角色记录

接下来,新增每个账号对应的角色记录,INSERT 语句如下:

INSERT INTO `weblog`.`t_user_role` (`id`, `username`, `role`, `create_time`) VALUES (1, 'admin', 'ROLE_ADMIN', '2023-07-07 01:21:15');
INSERT INTO `weblog`.`t_user_role` (`id`, `username`, `role`, `create_time`) VALUES (2, 'test', 'ROLE_VISITOR', '2023-07-07 01:23:33');

这里,admin 账号对应的是 ROLE_ADMIN 管理员角色,test 账号对应的是 ROLE_VISITOR 演示用户。

3. 创建 DO 类和 Mapper 接口

表、记录搞定后,我们来创建对应的 DO 类和 Mapper 接口:

weblog-module-common 模块下的 /domain/dos 包中,创建对应的 DO 类,代码如下:

@Data
@Builder
@TableName("t_user_role")
public class UserRoleDO {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String role;
    private Date createTime;
}

/mapper 包下,创建接口,代码如下:

public interface UserRoleMapper extends BaseMapper<UserRoleDO> {
    /**
     * 根据用户名查询
     * @param username
     * @return
     */
    default List<UserRoleDO> selectByUsername(String username) {
        LambdaQueryWrapper<UserRoleDO> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(UserRoleDO::getUsername, username);

        return selectList(wrapper);
    }
}

在接口中,我们添加了一个默认方法 selectByUsername(),通过用户名查询角色列表。

4. 添加查询角色逻辑

修改 UserDetailServiceImpl 类中的代码,添加通过用户名查询角色的逻辑,代码如下:

@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {

	// 省略...
    @Autowired
    private UserRoleMapper userRoleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 省略...

        // 判断用户是否存在
        if (Objects.isNull(userDO)) {
            throw new UsernameNotFoundException("该用户不存在");
        }

        // 用户角色
        List<UserRoleDO> roleDOS = userRoleMapper.selectByUsername(username);

        String[] roleArr = null;

        // 转数组
        if (!CollectionUtils.isEmpty(roleDOS)) {
            List<String> roles = roleDOS.stream().map(p -> p.getRole()).collect(Collectors.toList());
            roleArr = roles.toArray(new String[roles.size()]);
        }

        // authorities 用于指定角色
        return User.withUsername(userDO.getUsername())
                .password(userDO.getPassword())
                .authorities(roleArr)
                .build();
    }
}

这里的 authorities 角色字段,我们不再写死,而是改从数据库中查询,当角色不为空时,将其转换为数组,并传入到 authorities 字段中。

5. 鉴权不通过响应提示

RestAccessDeniedHandler 专门用于处理用户登录成功,但是访问的是受保护的资源,之前,我们仅仅打印了一行警告级别的日志,现在我们给它一个错误返参,提示用户 “演示账号仅支持查询操作!”,代码如下:

@Slf4j
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.warn("登录成功访问受保护的资源,但是权限不够: ", accessDeniedException);
        // 预留,后面引入多角色时会用到
        ResultUtil.fail(response, Response.fail(ResponseCodeEnum.FORBIDDEN));
    }
}

5.1 添加 FORBIDDEN 枚举值

还需要在全局枚举类 ResponseCodeEnum 中添加 FORBIDDEN 枚举值,代码如下:

FORBIDDEN("20004", "演示账号仅支持查询操作!"),

6. 启用 Spring Security 方法级注解

编辑 WebSecurityConfig 配置类,添加 @EnableGlobalMethodSecurity 注解:

// 省略...
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
		// 省略...
}

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true):这是一个 Spring Security 注解,用于启用方法级别的安全性设置。prePostEnabled = true 表示启用 @PreAuthorize@PostAuthorize 注解,securedEnabled = true 表示启用 @Secured 注解。这意味着您可以在方法级别使用这些注解来定义访问控制规则。

7. 全局异常捕获

以上还没完,我们还需要在全局异常捕取类中将 AccessDeniedException 手动抛出,否则就被它给吞了。这里需要统一交给 RestAccessDeniedHandler 去处理:

package com.quanxiaoha.weblog.common.exception;

import com.quanxiaoha.weblog.common.enums.ResponseCodeEnum;
import com.quanxiaoha.weblog.common.utils.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation@ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 省略...
	
    @ExceptionHandler({ AccessDeniedException.class })
    public void throwAccessDeniedException(AccessDeniedException e) throws AccessDeniedException {
    	// 捕获到鉴权失败异常,主动抛出,交给 RestAccessDeniedHandler 去处理
        log.info("============= 捕获到 AccessDeniedException");
        throw e;
    }
}

注意AccessDeniedException 的包路径为 org.springframework.security.access.AccessDeniedException,千万别导错包了!!!

TIP:如果 AccessDeniedException.class 爆红,因为它属于 Spring Security 这个框架,你还需要在 weblog-module-common 模块的 pom.xml 文件中,添加 Spring Security 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

8. 添加鉴权注解

OK,到上面为止核心代码都添加完毕了,接下来我们在 TestController 控制器中,添加一个新的接口 /admin/update,并对其添加 @PreAuthorize("hasRole('ROLE_ADMIN')") 注解,表示在调用接口之前进行鉴权,必须是拥有 ROLE_ADMIN 管理员角色的账号发来的请求,才允许正常执行,否则提示鉴权不通过提示,代码如下:

@RestController
@Slf4j
@Api(tags = "首页模块")
public class TestController {

	// 省略...

    @PostMapping("/admin/update")
    @ApiOperationLog(description = "测试更新接口")
    @ApiOperation(value = "测试更新接口")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public Response testUpdate() {
        log.info("更新成功...");
        return Response.success();
    }

}

9. 测试一下

最后,我们测试一下,分别用两种不同角色的账户进行登录,请求接口,看看演示账号是否真的鉴权不通过,管理员全部放行:

9.1 用管理员账号登录测试

首先登录管理员账号,拿到对应的 Token 令牌:

[插入截图]

9.2 全局参数设置

knife4j 提供了全局参数设置功能。让我们可以全局设置 Token 令牌,这样后续每次接口测试中,都会带上它,无需每次单独设置。操作步骤如下:

[插入截图1]

点击 文档管理 | 全局参数设置 | 添加参数:

[插入截图2]

按照后端登录接口约定的格式,添加请求头,名称为 Authorization,值的格式为 Bearer + 空格 + Token 令牌,点击确定。

然后,我们刷新页面,应用新的令牌,通过该令牌分别访问 /admin/test/admin/update 两个接口,看看是否能够正常请求:

[插入截图3]

可以看到有校验提示信息,说明鉴权是通过的,接口能够正常执行。再来测试一下带有鉴权注解的 /admin/update 接口,图示如下:

[插入截图4]

返参提示成功,说明鉴权通过。

7.2 用游客账号登录测试

再来用游客账号登录,重新获取 Token 令牌,并设置到全局参数中,记得刷新页面。

[插入截图5]

分别再次请求两个接口,看看效果:

[插入截图6]

当我们请求 /admin/update 接口时,因为该接口必须拥有 ROLE_ADMIN 角色的用户才能正常访问,所以,后端返参提示 “演示账号仅支持查询操作!”。

10. 本小节源码下载

https://t.zsxq.com/13sP38IBy

11. 结语

本小节中,带着大家为博客添加了一个演示账号功能,通过对不同用户分配不同的角色,以及开启 Spring Security 框架的方法级别的安全性设置,手动对非查询接口添加 @PreAuthorize 鉴权注解,指定必须是拥有 ROLE_ADMIN 管理员角色才允许操作。基于这个思路,我们实现了一个通用的 RBAC 演示账号管理方案,适用于任何需要区分管理员与只读游客角色的前后端项目。

如果你有更好的 RBAC 实现方式,欢迎在评论区分享!

博客系统演示账号与游客角色实现:Spring Security RBAC 基于角色的访问控制
http://www.clxhxhhr.top/posts/371/
作者
clxstart
发布于
2026-08-22
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。