Build a Button‑Level User Permission System Step‑by‑Step with Spring Boot & MyBatisPlus
This tutorial walks through designing a button‑level user permission system—including a five‑table database schema, Spring Boot + MyBatisPlus project setup, code generation, menu CRUD operations, recursive menu tree construction, role‑based permission queries, and annotation‑driven request authorization—complete with runnable code snippets and sample data.
Database Design
A MySQL database menu_auth_db is created with five tables: tb_user, tb_role, tb_user_role, tb_menu, and tb_role_menu. tb_user stores user id, mobile, name, password and a soft‑delete flag. tb_role stores role id, name, code and a soft‑delete flag. tb_user_role links users to roles (user_id, role_id). tb_role_menu links roles to menus (role_id, menu_id). tb_menu implements a parent‑child tree and contains the fields: name – menu name menu_code – unique code used for permission checks parent_id – id of the parent node (null for root) node_type – 1 = folder, 2 = page, 3 = button icon_url – icon address sort – ordering number link_url – front‑end address (optional for folders/buttons) level – depth of the node in the tree path – comma‑separated list of ancestor ids, used for fast parent lookup is_delete – soft‑delete flag
CREATE DATABASE IF NOT EXISTS `menu_auth_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE `menu_auth_db`.`tb_user` (
`id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '用户手机号',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '用户姓名',
`password` varchar(128) NOT NULL DEFAULT '' COMMENT '用户密码',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='用户表';
-- tb_user_role, tb_role, tb_role_menu omitted for brevity
CREATE TABLE `menu_auth_db`.`tb_menu` (
`id` bigint(20) NOT NULL COMMENT '菜单ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '菜单名称',
`menu_code` varchar(100) NOT NULL DEFAULT '' COMMENT '菜单编码',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父节点',
`node_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '节点类型,1文件夹,2页面,3按钮',
`icon_url` varchar(255) NOT NULL DEFAULT '' COMMENT '菜单图标地址',
`sort` int(11) NOT NULL DEFAULT '1' COMMENT '排序号',
`link_url` varchar(500) NOT NULL DEFAULT '' COMMENT '菜单对应的地址',
`level` int(11) NOT NULL DEFAULT '0' COMMENT '菜单层次',
`path` varchar(2500) NOT NULL DEFAULT '' COMMENT '树id的路径,主要用于存放从根节点到当前树的父节点的路径',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`),
KEY idx_parent_id (`parent_id`)
) ENGINE=InnoDB COMMENT='菜单表';Project Construction
Create Project
The project is built with springboot and mybatisPlus. MyBatis‑Plus code generator creates DAO, service and controller layers, eliminating repetitive CRUD code.
Menu Function Development
Menu Add Logic
@Override
public void addMenu(Menu menu) {
// Root node: parentId = 0
if (menu.getParentId().longValue() == 0) {
menu.setLevel(1);
menu.setPath(null);
} else {
Menu parentMenu = baseMapper.selectById(menu.getParentId());
if (parentMenu == null) {
throw new CommonException("未查询到对应的父菜单节点");
}
menu.setLevel(parentMenu.getLevel().intValue() + 1);
// Build comma‑separated path
if (StringUtils.isNotEmpty(parentMenu.getPath())) {
menu.setPath(parentMenu.getPath() + "," + parentMenu.getId());
} else {
menu.setPath(parentMenu.getId().toString());
}
}
// Simple id generation (timestamp)
menu.setId(System.currentTimeMillis());
super.save(menu);
}Menu Query Logic
A view object MenuVo holds the fields required by the front‑end.
public class MenuVo {
private Long id;
private String name;
private String menuCode;
private Long parentId;
private Integer nodeType;
private String iconUrl;
private Integer sort;
private String linkUrl;
private Integer level;
private String path;
List<MenuVo> childMenu;
// getters & setters ...
}The service builds a tree using recursion.
@Override
public List<MenuVo> queryMenuTree() {
Wrapper<Menu> queryObj = new QueryWrapper<>().orderByAsc("level", "sort");
List<Menu> allMenu = super.list(queryObj);
// 0L denotes the virtual root parent id
return transferMenuVo(allMenu, 0L);
}
private List<MenuVo> transferMenuVo(List<Menu> allMenu, Long parentId) {
List<MenuVo> resultList = new ArrayList<>();
if (!CollectionUtils.isEmpty(allMenu)) {
for (Menu source : allMenu) {
if (parentId.longValue() == source.getParentId().longValue()) {
MenuVo menuVo = new MenuVo();
BeanUtils.copyProperties(source, menuVo);
List<MenuVo> childList = transferMenuVo(allMenu, source.getId());
if (!CollectionUtils.isEmpty(childList)) {
menuVo.setChildMenu(childList);
}
resultList.add(menuVo);
}
}
}
return resultList;
}A controller endpoint returns the tree.
@RestController
@RequestMapping("/menu")
public class MenuController {
@Autowired
private MenuService menuService;
@PostMapping(value = "/queryMenuTree")
public List<MenuVo> queryTreeMenu() {
return menuService.queryMenuTree();
}
}User Permission Development
Permission flow:
After login, fetch the user's roles.
From the roles, fetch associated menu permission points.
Assemble all menu points under the user's roles and return them.
User Permission Query
@Override
public List<MenuVo> queryMenus(Long userId) {
// 1. Query user‑role mapping
Wrapper<UserRole> queryUserRoleObj = new QueryWrapper<>().eq("user_id", userId);
List<UserRole> userRoles = userRoleService.list(queryUserRoleObj);
if (!CollectionUtils.isEmpty(userRoles)) {
// 2. Use the first role to fetch role‑menu mapping
Wrapper<RoleMenu> queryRoleMenuObj = new QueryWrapper<>().eq("role_id", userRoles.get(0).getRoleId());
List<RoleMenu> roleMenus = roleMenuService.list(queryRoleMenuObj);
if (!CollectionUtils.isEmpty(roleMenus)) {
Set<Long> menuIds = new HashSet<>();
for (RoleMenu roleMenu : roleMenus) {
menuIds.add(roleMenu.getMenuId());
}
// 3. Query menus by IDs
Wrapper<Menu> queryMenuObj = new QueryWrapper<>().in("id", new ArrayList<>(menuIds));
List<Menu> menus = super.list(queryMenuObj);
if (!CollectionUtils.isEmpty(menus)) {
// Include parent nodes for a complete tree
Set<Long> allMenuIds = new HashSet<>();
for (Menu menu : menus) {
allMenuIds.add(menu.getId());
if (StringUtils.isNotEmpty(menu.getPath())) {
String[] pathIds = StringUtils.split(",", menu.getPath());
for (String pathId : pathIds) {
allMenuIds.add(Long.valueOf(pathId));
}
}
}
List<Menu> allMenus = super.list(new QueryWrapper<Menu>().in("id", new ArrayList<>(allMenuIds)));
return transferMenuVo(allMenus, 0L);
}
}
}
return null;
} @PostMapping(value = "/queryMenus")
public List<MenuVo> queryMenus(Long userId) {
return menuService.queryMenus(userId);
}User Authorization Development
Backend verification uses a custom annotation and an AOP aspect. The menu code (e.g., menuCode) links front‑end buttons to back‑end endpoints.
Permission Annotation
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermissions {
String value() default "";
}Permission Aspect
@Aspect
@Component
public class CheckPermissionsAspect {
@Autowired
private MenuMapper menuMapper;
@Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)")
public void checkPermissions() {}
@Before("checkPermissions()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
Long userId = null;
Object[] args = joinPoint.getArgs();
Object requestParam = args[0];
if (requestParam != null) {
Field field = requestParam.getClass().getDeclaredField("userId");
field.setAccessible(true);
userId = (Long) field.get(requestParam);
}
if (userId != null) {
Class<?> clazz = joinPoint.getTarget().getClass();
String methodName = joinPoint.getSignature().getName();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
Method method = clazz.getMethod(methodName, parameterTypes);
if (method.getAnnotation(CheckPermissions.class) != null) {
CheckPermissions annotation = method.getAnnotation(CheckPermissions.class);
String menuCode = annotation.value();
if (StringUtils.isNotBlank(menuCode)) {
int count = menuMapper.selectAuthByUserIdAndMenuCode(userId, menuCode);
if (count == 0) {
throw new CommonException("接口无访问权限");
}
}
}
}
}
}Example: Role Management Query
public class RoleDTO extends Role {
private Long userId;
// getters & setters ...
}
@RestController
@RequestMapping("/role")
public class RoleController {
private RoleService roleService;
@CheckPermissions(value = "roleMgr:list")
@PostMapping(value = "/queryRole")
public List<Role> queryRole(RoleDTO roleDTO) {
return roleService.list();
}
}Database initialization assigns user "张三" a "访客人员" role with only two menu permissions. A Postman call to /queryMenus returns exactly those two menus, while a call to the protected /queryRole endpoint returns "接口无访问权限", confirming that the annotation‑based authorization works as intended.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Pan Zhi's Tech Notes
Sharing frontline internet R&D technology, dedicated to premium original content.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
