Skip to content
第 87 / 250 章后端⏱ 10 分钟阅读

第 87 章:角色与权限模块

学习目标

  • 实现角色 CRUD 与菜单分配
  • 实现菜单树查询与按钮权限校验
  • 完成 RBAC 权限闭环

一、模块结构

二、角色管理

实体

java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_role")
public class Role extends BaseEntity {
    private String name;
    private String code;
    private String description;
    private Integer sort;
    private Integer status;
    private Integer dataScope;
}

DTO

java
@Data
public class RoleCreateDTO {
    @NotBlank private String name;
    @NotBlank @Pattern(regexp = "^ROLE_[A-Z_]+$")
    private String code;
    private String description;
    private Integer sort;
    private Integer dataScope = 4;
    private List<Long> menuIds;
}

@Data
public class RoleUpdateDTO {
    @NotBlank private String name;
    private String description;
    private Integer sort;
    private Integer dataScope;
    private List<Long> menuIds;
}

@Data
public class RolePageQuery {
    @Min(1) private Long current = 1L;
    @Min(1) @Max(200) private Long size = 10L;
    private String keyword;
    private Integer status;
}

Service

java
public interface RoleService extends IService<Role> {
    Long createRole(RoleCreateDTO dto);
    void updateRole(Long id, RoleUpdateDTO dto);
    void deleteRole(Long id);
    void assignMenus(Long roleId, List<Long> menuIds);
    PageResult<RoleVO> page(RolePageQuery query);
    RoleVO getVOById(Long id);
    List<RoleVO> listAll();
}
java
@Service
@RequiredArgsConstructor
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {

    private final RoleMenuMapper roleMenuMapper;
    private final UserRoleMapper userRoleMapper;
    private final MenuMapper menuMapper;
    private final RoleConvert roleConvert;

    @Override
    @Transactional
    public Long createRole(RoleCreateDTO dto) {
        // ① 检查编码唯一
        if (baseMapper.selectByCode(dto.getCode()) != null) {
            throw new BusinessException("角色编码已存在");
        }
        Role role = roleConvert.toEntity(dto);
        role.setStatus(1);
        baseMapper.insert(role);

        // ② 分配菜单
        if (dto.getMenuIds() != null && !dto.getMenuIds().isEmpty()) {
            assignMenus(role.getId(), dto.getMenuIds());
        }
        return role.getId();
    }

    @Override
    @Transactional
    public void updateRole(Long id, RoleUpdateDTO dto) {
        Role role = baseMapper.selectById(id);
        Assert.notNull(role, "角色不存在");
        roleConvert.updateEntity(dto, role);
        baseMapper.updateById(role);

        if (dto.getMenuIds() != null) {
            assignMenus(id, dto.getMenuIds());
        }
    }

    @Override
    @Transactional
    public void deleteRole(Long id) {
        Assert.isTrue(!id.equals(1L), "超级管理员角色不可删除");
        // ① 检查是否有用户使用
        if (userRoleMapper.countByRoleId(id) > 0) {
            throw new BusinessException(ErrorCode.ROLE_IN_USE);
        }
        baseMapper.deleteById(id);
        roleMenuMapper.deleteByRoleId(id);
    }

    @Override
    @Transactional
    public void assignMenus(Long roleId, List<Long> menuIds) {
        roleMenuMapper.deleteByRoleId(roleId);
        if (menuIds != null) {
            for (Long menuId : menuIds) {
                roleMenuMapper.insert(new RoleMenu(roleId, menuId));
            }
        }
        // ① 清理所有该角色用户的权限缓存
        userRoleMapper.selectUserIdsByRoleId(roleId)
                .forEach(uid -> permissionService.clearCache(uid));
    }

    @Override
    public PageResult<RoleVO> page(RolePageQuery query) {
        Page<Role> page = new Page<>(query.getCurrent(), query.getSize());
        LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<Role>()
                .like(StringUtils.hasText(query.getKeyword()), Role::getName, query.getKeyword())
                .eq(query.getStatus() != null, Role::getStatus, query.getStatus())
                .orderByAsc(Role::getSort);
        IPage<Role> result = baseMapper.selectPage(page, wrapper);
        return PageResult.of(result, this::toVO);
    }

    @Override
    public RoleVO getVOById(Long id) {
        Role role = baseMapper.selectById(id);
        Assert.notNull(role, "角色不存在");
        RoleVO vo = roleConvert.toVO(role);
        vo.setMenuIds(roleMenuMapper.selectMenuIdsByRoleId(id));
        return vo;
    }

    @Override
    public List<RoleVO> listAll() {
        return baseMapper.selectList(
                new LambdaQueryWrapper<Role>()
                        .eq(Role::getStatus, 1)
                        .orderByAsc(Role::getSort))
                .stream().map(roleConvert::toVO).toList();
    }

    private RoleVO toVO(Role role) {
        return roleConvert.toVO(role);
    }
}

三、菜单管理

实体

java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_menu")
public class Menu extends BaseEntity {
    private Long parentId;
    private String name;
    private Integer type;          // 1=目录 2=菜单 3=按钮
    private String permission;
    private String path;
    private String component;
    private String icon;
    private Integer sort;
    private Integer visible;
    private Integer status;
}

DTO / VO

java
@Data
public class MenuCreateDTO {
    @NotNull private Long parentId;
    @NotBlank private String name;
    @NotNull @Min(1) @Max(3) private Integer type;
    private String permission;
    private String path;
    private String component;
    private String icon;
    private Integer sort;
    private Integer visible = 1;
}

@Data
public class MenuVO {
    private Long id;
    private Long parentId;
    private String name;
    private Integer type;
    private String permission;
    private String path;
    private String component;
    private String icon;
    private Integer sort;
    private List<MenuVO> children;
}

Service

java
public interface MenuService extends IService<Menu> {
    Long createMenu(MenuCreateDTO dto);
    void updateMenu(Long id, MenuUpdateDTO dto);
    void deleteMenu(Long id);
    List<MenuVO> tree();
    Set<String> currentUserPermissions();
    List<MenuVO> currentUserMenuTree();
}
java
@Service
@RequiredArgsConstructor
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {

    private final RoleMenuMapper roleMenuMapper;
    private final PermissionService permissionService;
    private final RedisTemplate<String, Object> redis;
    private final MenuConvert menuConvert;

    @Override
    @Transactional
    public Long createMenu(MenuCreateDTO dto) {
        Menu menu = menuConvert.toEntity(dto);
        menu.setStatus(1);
        baseMapper.insert(menu);
        return menu.getId();
    }

    @Override
    public void updateMenu(Long id, MenuUpdateDTO dto) {
        Menu menu = baseMapper.selectById(id);
        Assert.notNull(menu, "菜单不存在");
        menuConvert.updateEntity(dto, menu);
        baseMapper.updateById(menu);
        // ① 清理所有权限缓存
        permissionService.clearAllCache();
    }

    @Override
    @Transactional
    public void deleteMenu(Long id) {
        // ① 检查是否有子菜单
        Long childCount = baseMapper.selectCount(
                new LambdaQueryWrapper<Menu>().eq(Menu::getParentId, id));
        Assert.isTrue(childCount == 0, "请先删除子菜单");

        // ② 检查是否被角色引用
        Long refCount = roleMenuMapper.countByMenuId(id);
        Assert.isTrue(refCount == 0, "菜单已被角色使用,无法删除");

        baseMapper.deleteById(id);
        permissionService.clearAllCache();
    }

    @Override
    public List<MenuVO> tree() {
        List<Menu> all = baseMapper.selectList(
                new LambdaQueryWrapper<Menu>()
                        .orderByAsc(Menu::getSort));
        return buildTree(all, 0L);
    }

    @Override
    public Set<String> currentUserPermissions() {
        return permissionService.getUserPermissions(SecurityUtils.getCurrentUserId());
    }

    @Override
    public List<MenuVO> currentUserMenuTree() {
        Long userId = SecurityUtils.getCurrentUserId();
        List<Menu> menus = baseMapper.selectByUserId(userId);
        return buildTree(menus, 0L);
    }

    private List<MenuVO> buildTree(List<Menu> all, Long parentId) {
        return all.stream()
                .filter(m -> m.getParentId().equals(parentId))
                .map(m -> {
                    MenuVO vo = menuConvert.toVO(m);
                    vo.setChildren(buildTree(all, m.getId()));
                    return vo;
                })
                .toList();
    }
}

Controller

java
@RestController
@RequestMapping("/api/menu")
@RequiredArgsConstructor
@Tag(name = "菜单管理")
public class MenuController {

    private final MenuService menuService;

    @GetMapping("/tree")
    @Operation(summary = "菜单树")
    public Result<List<MenuVO>> tree() {
        return Result.ok(menuService.tree());
    }

    @GetMapping("/current")
    @Operation(summary = "当前用户菜单")
    public Result<List<MenuVO>> currentUserMenu() {
        return Result.ok(menuService.currentUserMenuTree());
    }

    @GetMapping("/permissions")
    @Operation(summary = "当前用户按钮权限")
    public Result<Set<String>> permissions() {
        return Result.ok(menuService.currentUserPermissions());
    }

    @PostMapping
    @RequiresPermission("menu:create")
    public Result<Long> create(@RequestBody @Valid MenuCreateDTO dto) {
        return Result.ok(menuService.createMenu(dto));
    }

    @PutMapping("/{id}")
    @RequiresPermission("menu:update")
    public Result<Void> update(@PathVariable Long id, @RequestBody MenuUpdateDTO dto) {
        menuService.updateMenu(id, dto);
        return Result.ok();
    }

    @DeleteMapping("/{id}")
    @RequiresPermission("menu:delete")
    public Result<Void> delete(@PathVariable Long id) {
        menuService.deleteMenu(id);
        return Result.ok();
    }
}

四、PermissionService(权限缓存核心)

java
@Service
@RequiredArgsConstructor
public class PermissionService {

    private final RedisTemplate<String, Object> redis;
    private final UserMapper userMapper;
    private final RoleMapper roleMapper;
    private final MenuMapper menuMapper;

    private static final String PERM_KEY_PREFIX = "user:perms:";
    private static final Duration CACHE_TTL = Duration.ofMinutes(30);

    public Set<String> getUserPermissions(Long userId) {
        String key = PERM_KEY_PREFIX + userId;

        // ① 查缓存
        Object cached = redis.opsForValue().get(key);
        if (cached instanceof Set<?> set) {
            return set.stream().map(Object::toString).collect(Collectors.toSet());
        }

        // ② 查 DB
        Set<String> perms = new HashSet<>();
        List<Role> roles = roleMapper.selectByUserId(userId);
        for (Role role : roles) {
            List<Menu> menus = menuMapper.selectByRoleId(role.getId());
            for (Menu menu : menus) {
                if (menu.getType() == 3 && StringUtils.hasText(menu.getPermission())) {
                    perms.add(menu.getPermission());
                }
            }
        }

        // ③ 写缓存
        redis.opsForValue().set(key, perms, CACHE_TTL);
        return perms;
    }

    public void clearCache(Long userId) {
        redis.delete(PERM_KEY_PREFIX + userId);
    }

    public void clearAllCache() {
        Set<String> keys = redis.keys(PERM_KEY_PREFIX + "*");
        if (keys != null && !keys.isEmpty()) {
            redis.delete(keys);
        }
    }
}

五、权限注解

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('{value}')")
public @interface RequiresPermission {
    String value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface RequiresRole {
    String value();
}
java
// 用法
@RequiresPermission("user:create")
@PostMapping
public Result<Long> create(...) { ... }

@RequiresRole("ADMIN")
@DeleteMapping("/{id}")
public Result<Void> delete(...) { ... }

六、Controller:角色管理

java
@RestController
@RequestMapping("/api/role")
@RequiredArgsConstructor
@Tag(name = "角色管理")
public class RoleController {

    private final RoleService roleService;

    @GetMapping("/page")
    @Operation(summary = "角色分页")
    public Result<PageResult<RoleVO>> page(RolePageQuery query) {
        return Result.ok(roleService.page(query));
    }

    @GetMapping("/all")
    @Operation(summary = "所有角色")
    public Result<List<RoleVO>> all() {
        return Result.ok(roleService.listAll());
    }

    @GetMapping("/{id}")
    public Result<RoleVO> getById(@PathVariable Long id) {
        return Result.ok(roleService.getVOById(id));
    }

    @PostMapping
    @RequiresPermission("role:create")
    public Result<Long> create(@RequestBody @Valid RoleCreateDTO dto) {
        return Result.ok(roleService.createRole(dto));
    }

    @PutMapping("/{id}")
    @RequiresPermission("role:update")
    public Result<Void> update(@PathVariable Long id, @RequestBody @Valid RoleUpdateDTO dto) {
        roleService.updateRole(id, dto);
        return Result.ok();
    }

    @DeleteMapping("/{id}")
    @RequiresPermission("role:delete")
    public Result<Void> delete(@PathVariable Long id) {
        roleService.deleteRole(id);
        return Result.ok();
    }

    @PutMapping("/{id}/menus")
    @RequiresPermission("role:assign")
    public Result<Void> assignMenus(@PathVariable Long id, @RequestBody List<Long> menuIds) {
        roleService.assignMenus(id, menuIds);
        return Result.ok();
    }
}

七、完整 RBAC 闭环

八、本章小结

要点关键
RoleCRUD + 分配菜单
Menu三级菜单(目录/菜单/按钮)
权限按钮级 + 菜单级 + 路径级
注解@RequiresPermission + @RequiresRole
缓存Redis 缓存用户权限(30 分钟)
失效角色/菜单变更时主动清理
数据权限role.data_scope:1-5

动手练习

练习 1:基础题

实现角色管理 CRUD:新增角色、修改、删除、分页查询、分配菜单。

练习 2:进阶题

实现完整的权限校验:用 @RequiresPermission 控制接口访问,未登录返回 401,无权限返回 403。

练习 3:思考题

权限变更时如何让所有用户立即生效?除了 Redis 失效,还有什么方案?


下一章第 88 章:部门与字典模块

本站基于 VitePress 构建 · 由 Codebook 团队维护