当前位置: 首页 > news >正文

从按钮到接口:权限系统设计的艺术与实践 —— 打造细粒度可扩展的权限架构

在现代应用开发中,权限系统是保障数据安全与业务合规的核心组件。无论是面向普通用户的 App 还是供管理员操作的后台系统,都需要一套灵活、高效、安全的权限控制机制。本文将从权限模型设计到代码实现,全方位剖析如何构建涵盖菜单权限、按钮权限和接口权限的完整权限体系,既有理论深度,又有可直接落地的实战方案。

一、权限系统的核心挑战与设计原则

在开始设计权限系统之前,我们需要明确权限控制的核心目标与面临的挑战,这是构建可靠权限体系的基础。

1.1 权限系统的核心需求

一个完善的权限系统需要满足以下核心需求:

  • 身份认证:确认用户的真实身份
  • 权限控制:决定用户能访问哪些资源
  • 操作审计:记录用户的关键操作
  • 灵活扩展:支持业务变化带来的权限调整
  • 性能保障:在权限判断时不影响系统响应速度

1.2 常见权限模型对比

目前主流的权限模型有三种,各有适用场景:

权限模型核心思想优点缺点适用场景
ACL(访问控制列表)为每个资源设置可访问的用户 / 角色列表实现简单,直观资源增多时管理复杂简单应用、少量资源场景
RBAC(基于角色的访问控制)先定义角色,再给角色分配权限,用户关联角色易于管理,适合复杂组织角色膨胀问题中大型应用、企业级系统
ABAC(基于属性的访问控制)通过用户、资源、环境等属性动态判断权限灵活性高,适合复杂规则设计复杂,性能挑战大云服务、动态权限场景

在大多数业务系统中,RBAC 模型是平衡灵活性与复杂性的最佳选择,本文将基于 RBAC 模型进行扩展设计。

1.3 权限系统的设计原则

优秀的权限系统设计应遵循以下原则:

  • 最小权限原则:用户仅获得完成工作必需的权限
  • 职责分离原则:敏感操作需要多人协作完成
  • 数据权限与功能权限分离:功能权限控制能否操作,数据权限控制能操作哪些数据
  • 权限与业务分离:权限系统应独立于业务逻辑,可复用
  • 易用性原则:权限配置应简单直观,降低管理成本

二、权限系统的整体架构设计

一个完整的权限系统应包含身份认证、权限管理、权限校验三大核心模块,形成闭环。

2.1 权限系统架构图

2.2 核心权限实体关系

基于 RBAC 模型扩展,我们需要设计以下核心实体:

2.3 权限控制流程

权限控制的完整流程包括权限分配、权限收集、权限校验三个阶段:

三、数据库设计:权限系统的基石

合理的数据库设计是权限系统灵活扩展的基础,我们需要设计用户、角色、权限相关的表结构。

3.1 核心表结构设计

-- 用户表
CREATE TABLE `sys_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(100) NOT NULL COMMENT '密码(加密存储)',`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',`phone` varchar(20) DEFAULT NULL COMMENT '手机号',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-正常',`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',`dept_id` bigint DEFAULT NULL COMMENT '部门ID',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`),KEY `idx_dept_id` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';-- 角色表
CREATE TABLE `sys_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',`role_name` varchar(50) NOT NULL COMMENT '角色名称',`role_code` varchar(50) NOT NULL COMMENT '角色编码',`description` varchar(255) DEFAULT NULL COMMENT '角色描述',`sort` int NOT NULL DEFAULT '0' COMMENT '排序',`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-正常',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表';-- 用户角色关联表
CREATE TABLE `sys_user_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',`user_id` bigint NOT NULL COMMENT '用户ID',`role_id` bigint NOT NULL COMMENT '角色ID',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_user_role` (`user_id`,`role_id`),KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';-- 菜单表
CREATE TABLE `sys_menu` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',`parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父菜单ID',`menu_name` varchar(50) NOT NULL COMMENT '菜单名称',`menu_code` varchar(50) NOT NULL COMMENT '菜单编码',`path` varchar(255) DEFAULT NULL COMMENT '路由路径',`component` varchar(255) DEFAULT NULL COMMENT '前端组件',`icon` varchar(50) DEFAULT NULL COMMENT '图标',`sort` int NOT NULL DEFAULT '0' COMMENT '排序',`type` tinyint NOT NULL COMMENT '类型:1-目录,2-菜单,3-按钮',`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-正常',`external` tinyint NOT NULL DEFAULT '0' COMMENT '是否外链:0-否,1-是',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_menu_code` (`menu_code`),KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表';-- 角色菜单关联表
CREATE TABLE `sys_role_menu` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',`role_id` bigint NOT NULL COMMENT '角色ID',`menu_id` bigint NOT NULL COMMENT '菜单ID',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_menu` (`role_id`,`menu_id`),KEY `idx_menu_id` (`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';-- 接口权限表
CREATE TABLE `sys_api` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '接口ID',`api_name` varchar(100) NOT NULL COMMENT '接口名称',`api_path` varchar(255) NOT NULL COMMENT '接口路径',`api_method` varchar(10) NOT NULL COMMENT '请求方法:GET、POST、PUT、DELETE等',`description` varchar(255) DEFAULT NULL COMMENT '接口描述',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_api_path_method` (`api_path`,`api_method`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统接口权限表';-- 角色接口关联表
CREATE TABLE `sys_role_api` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',`role_id` bigint NOT NULL COMMENT '角色ID',`api_id` bigint NOT NULL COMMENT '接口ID',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_api` (`role_id`,`api_id`),KEY `idx_api_id` (`api_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色接口关联表';

3.2 表设计说明

  1. 菜单表设计:采用父子结构设计,通过parent_id实现菜单层级,type字段区分目录、菜单和按钮
  2. 权限关联设计:所有权限关联表都采用 "角色 - 权限" 的多对多设计,保证灵活性
  3. 唯一性约束:关键字段设置唯一约束,如usernamerole_codemenu_code等,避免重复数据
  4. 时间跟踪:所有表都包含create_timeupdate_time,便于审计和问题追踪

四、核心实体与数据访问层实现

基于数据库设计,我们实现对应的 Java 实体类和 MyBatis-Plus 数据访问层。

4.1 核心依赖配置

首先在pom.xml中添加必要的依赖:

<dependencies><!-- Spring Boot 核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>3.2.0</version></dependency><!-- 数据库相关 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><version>3.2.0</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.3.0</version><scope>runtime</scope></dependency><!-- MyBatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.5</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency><!-- Spring Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>3.2.0</version></dependency><!-- Token相关 --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><!-- FastJson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.32</version></dependency><!-- Swagger3 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</version></dependency><!-- Guava --><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.3-jre</version></dependency>
</dependencies>

4.2 实体类定义

package com.ken.system.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.time.LocalDateTime;/*** 系统用户实体类*/
@Data
@TableName("sys_user")
public class SysUser {/*** 用户ID*/@TableId(type = IdType.AUTO)private Long id;/*** 用户名*/private String username;/*** 密码(加密存储)*/private String password;/*** 昵称*/private String nickname;/*** 手机号*/private String phone;/*** 邮箱*/private String email;/*** 状态:0-禁用,1-正常*/private Integer status;/*** 头像URL*/private String avatar;/*** 部门ID*/private Long deptId;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
package com.ken.system.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.time.LocalDateTime;/*** 系统角色实体类*/
@Data
@TableName("sys_role")
public class SysRole {/*** 角色ID*/@TableId(type = IdType.AUTO)private Long id;/*** 角色名称*/private String roleName;/*** 角色编码*/private String roleCode;/*** 角色描述*/private String description;/*** 排序*/private Integer sort;/*** 状态:0-禁用,1-正常*/private Integer status;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
package com.ken.system.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.time.LocalDateTime;/*** 系统菜单实体类*/
@Data
@TableName("sys_menu")
public class SysMenu {/*** 菜单ID*/@TableId(type = IdType.AUTO)private Long id;/*** 父菜单ID*/private Long parentId;/*** 菜单名称*/private String menuName;/*** 菜单编码*/private String menuCode;/*** 路由路径*/private String path;/*** 前端组件*/private String component;/*** 图标*/private String icon;/*** 排序*/private Integer sort;/*** 类型:1-目录,2-菜单,3-按钮*/private Integer type;/*** 状态:0-禁用,1-正常*/private Integer status;/*** 是否外链:0-否,1-是*/private Integer external;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
package com.ken.system.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.time.LocalDateTime;/*** 系统接口实体类*/
@Data
@TableName("sys_api")
public class SysApi {/*** 接口ID*/@TableId(type = IdType.AUTO)private Long id;/*** 接口名称*/private String apiName;/*** 接口路径*/private String apiPath;/*** 请求方法:GET、POST、PUT、DELETE等*/private String apiMethod;/*** 接口描述*/private String description;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}

4.3 关联实体类

package com.ken.system.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.time.LocalDateTime;/*** 用户角色关联实体类*/
@Data
@TableName("sys_user_role")
public class SysUserRole {/*** ID*/@TableId(type = IdType.AUTO)private Long id;/*** 用户ID*/private Long userId;/*** 角色ID*/private Long roleId;/*** 创建时间*/private LocalDateTime createTime;
}
package com.ken.system.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.time.LocalDateTime;/*** 角色菜单关联实体类*/
@Data
@TableName("sys_role_menu")
public class SysRoleMenu {/*** ID*/@TableId(type = IdType.AUTO)private Long id;/*** 角色ID*/private Long roleId;/*** 菜单ID*/private Long menuId;/*** 创建时间*/private LocalDateTime createTime;
}
package com.ken.system.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.time.LocalDateTime;/*** 角色接口关联实体类*/
@Data
@TableName("sys_role_api")
public class SysRoleApi {/*** ID*/@TableId(type = IdType.AUTO)private Long id;/*** 角色ID*/private Long roleId;/*** 接口ID*/private Long apiId;/*** 创建时间*/private LocalDateTime createTime;
}

4.4 Mapper 接口定义

package com.ken.system.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.system.entity.SysUser;
import org.apache.ibatis.annotations.Param;import java.util.List;/*** 用户Mapper接口*/
public interface SysUserMapper extends BaseMapper<SysUser> {/*** 根据用户名查询用户* @param username 用户名* @return 用户信息*/SysUser selectByUsername(@Param("username") String username);/*** 根据用户ID查询角色列表* @param userId 用户ID* @return 角色编码列表*/List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
}
package com.ken.system.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.system.entity.SysMenu;
import org.apache.ibatis.annotations.Param;import java.util.List;/*** 菜单Mapper接口*/
public interface SysMenuMapper extends BaseMapper<SysMenu> {/*** 根据角色编码查询菜单列表* @param roleCodes 角色编码列表* @return 菜单列表*/List<SysMenu> selectByRoleCodes(@Param("roleCodes") List<String> roleCodes);/*** 根据角色编码查询按钮权限列表* @param roleCodes 角色编码列表* @return 按钮编码列表*/List<String> selectButtonCodesByRoleCodes(@Param("roleCodes") List<String> roleCodes);
}
package com.ken.system.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.system.entity.SysApi;
import org.apache.ibatis.annotations.Param;import java.util.List;/*** 接口Mapper接口*/
public interface SysApiMapper extends BaseMapper<SysApi> {/*** 根据角色编码查询接口权限列表* @param roleCodes 角色编码列表* @return 接口权限列表*/List<SysApi> selectByRoleCodes(@Param("roleCodes") List<String> roleCodes);
}

五、权限服务层实现

服务层是权限系统的核心,负责权限的查询、分配和验证逻辑。

5.1 权限模型 VO 定义

package com.ken.system.vo;import lombok.Data;import java.util.List;/*** 用户权限信息VO*/
@Data
public class UserPermissionVO {/*** 用户ID*/private Long userId;/*** 用户名*/private String username;/*** 角色编码列表*/private List<String> roleCodes;/*** 菜单权限列表*/private List<MenuVO> menus;/*** 按钮权限列表*/private List<String> buttonCodes;/*** 接口权限列表*/private List<ApiVO> apis;
}
package com.ken.system.vo;import lombok.Data;
import java.util.List;/*** 菜单VO*/
@Data
public class MenuVO {/*** 菜单ID*/private Long id;/*** 父菜单ID*/private Long parentId;/*** 菜单名称*/private String menuName;/*** 菜单编码*/private String menuCode;/*** 路由路径*/private String path;/*** 前端组件*/private String component;/*** 图标*/private String icon;/*** 排序*/private Integer sort;/*** 子菜单列表*/private List<MenuVO> children;
}
package com.ken.system.vo;import lombok.Data;/*** 接口VO*/
@Data
public class ApiVO {/*** 接口ID*/private Long id;/*** 接口路径*/private String apiPath;/*** 请求方法*/private String apiMethod;
}

5.2 权限服务实现

package com.ken.system.service;import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.ken.system.entity.SysApi;
import com.ken.system.entity.SysMenu;
import com.ken.system.entity.SysUser;
import com.ken.system.mapper.SysApiMapper;
import com.ken.system.mapper.SysMenuMapper;
import com.ken.system.mapper.SysUserMapper;
import com.ken.system.vo.ApiVO;
import com.ken.system.vo.MenuVO;
import com.ken.system.vo.UserPermissionVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;/*** 权限服务实现类*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PermissionService {private final SysUserMapper userMapper;private final SysMenuMapper menuMapper;private final SysApiMapper apiMapper;/*** 获取用户的所有权限信息* @param userId 用户ID* @return 用户权限信息*/public UserPermissionVO getUserPermissions(Long userId) {// 参数校验if (ObjectUtils.isEmpty(userId)) {log.error("获取用户权限失败,用户ID为空");throw new IllegalArgumentException("用户ID不能为空");}// 查询用户信息SysUser user = userMapper.selectById(userId);if (ObjectUtils.isEmpty(user)) {log.error("获取用户权限失败,用户不存在,用户ID:{}", userId);throw new IllegalArgumentException("用户不存在");}// 查询用户角色List<String> roleCodes = userMapper.selectRoleCodesByUserId(userId);if (CollectionUtils.isEmpty(roleCodes)) {log.warn("用户未分配任何角色,用户ID:{}", userId);return buildEmptyPermission(user);}// 构建用户权限信息UserPermissionVO permissionVO = new UserPermissionVO();permissionVO.setUserId(userId);permissionVO.setUsername(user.getUsername());permissionVO.setRoleCodes(roleCodes);// 查询菜单权限List<SysMenu> menus = menuMapper.selectByRoleCodes(roleCodes);permissionVO.setMenus(buildMenuTree(menus));// 查询按钮权限List<String> buttonCodes = menuMapper.selectButtonCodesByRoleCodes(roleCodes);permissionVO.setButtonCodes(buttonCodes);// 查询接口权限List<SysApi> apis = apiMapper.selectByRoleCodes(roleCodes);permissionVO.setApis(convertToApiVO(apis));log.info("获取用户权限成功,用户ID:{},角色数:{},菜单数:{},按钮数:{},接口数:{}",userId, roleCodes.size(),CollectionUtils.isEmpty(menus) ? 0 : menus.size(),CollectionUtils.isEmpty(buttonCodes) ? 0 : buttonCodes.size(),CollectionUtils.isEmpty(apis) ? 0 : apis.size());return permissionVO;}/*** 构建空权限信息*/private UserPermissionVO buildEmptyPermission(SysUser user) {UserPermissionVO permissionVO = new UserPermissionVO();permissionVO.setUserId(user.getId());permissionVO.setUsername(user.getUsername());permissionVO.setRoleCodes(Lists.newArrayList());permissionVO.setMenus(Lists.newArrayList());permissionVO.setButtonCodes(Lists.newArrayList());permissionVO.setApis(Lists.newArrayList());return permissionVO;}/*** 将菜单列表构建为树形结构*/private List<MenuVO> buildMenuTree(List<SysMenu> menus) {if (CollectionUtils.isEmpty(menus)) {return Lists.newArrayList();}// 过滤出菜单和目录(排除按钮)List<SysMenu> menuList = menus.stream().filter(menu -> menu.getType() == 1 || menu.getType() == 2).collect(Collectors.toList());// 转换为MenuVOList<MenuVO> menuVOs = menuList.stream().map(this::convertToMenuVO).collect(Collectors.toList());// 构建树形结构Map<Long, MenuVO> menuMap = Maps.newHashMap();// 先将所有菜单放入Mapfor (MenuVO menuVO : menuVOs) {menuMap.put(menuVO.getId(), menuVO);}// 构建父子关系List<MenuVO> rootMenus = Lists.newArrayList();for (MenuVO menuVO : menuVOs) {if (menuVO.getParentId() == 0) {// 根菜单rootMenus.add(menuVO);} else {// 子菜单MenuVO parentMenu = menuMap.get(menuVO.getParentId());if (!ObjectUtils.isEmpty(parentMenu)) {if (CollectionUtils.isEmpty(parentMenu.getChildren())) {parentMenu.setChildren(Lists.newArrayList());}parentMenu.getChildren().add(menuVO);}}}// 对子菜单进行排序sortMenuChildren(rootMenus);return rootMenus;}/*** 递归排序菜单子节点*/private void sortMenuChildren(List<MenuVO> menus) {if (CollectionUtils.isEmpty(menus)) {return;}// 按sort字段排序menus.sort((m1, m2) -> {if (ObjectUtils.isEmpty(m1.getSort())) {return 1;}if (ObjectUtils.isEmpty(m2.getSort())) {return -1;}return m1.getSort().compareTo(m2.getSort());});// 递归处理子菜单for (MenuVO menu : menus) {sortMenuChildren(menu.getChildren());}}/*** 转换SysMenu为MenuVO*/private MenuVO convertToMenuVO(SysMenu menu) {MenuVO menuVO = new MenuVO();menuVO.setId(menu.getId());menuVO.setParentId(menu.getParentId());menuVO.setMenuName(menu.getMenuName());menuVO.setMenuCode(menu.getMenuCode());menuVO.setPath(menu.getPath());menuVO.setComponent(menu.getComponent());menuVO.setIcon(menu.getIcon());menuVO.setSort(menu.getSort());menuVO.setChildren(Lists.newArrayList());return menuVO;}/*** 转换SysApi为ApiVO*/private List<ApiVO> convertToApiVO(List<SysApi> apis) {if (CollectionUtils.isEmpty(apis)) {return Lists.newArrayList();}return apis.stream().map(api -> {ApiVO apiVO = new ApiVO();apiVO.setId(api.getId());apiVO.setApiPath(api.getApiPath());apiVO.setApiMethod(api.getApiMethod());return apiVO;}).collect(Collectors.toList());}/*** 检查用户是否有指定按钮权限* @param userId 用户ID* @param buttonCode 按钮编码* @return 是否有权限*/public boolean hasButtonPermission(Long userId, String buttonCode) {if (ObjectUtils.isEmpty(userId) || !org.springframework.util.StringUtils.hasText(buttonCode)) {return false;}List<String> roleCodes = userMapper.selectRoleCodesByUserId(userId);if (CollectionUtils.isEmpty(roleCodes)) {return false;}List<String> buttonCodes = menuMapper.selectButtonCodesByRoleCodes(roleCodes);return !CollectionUtils.isEmpty(buttonCodes) && buttonCodes.contains(buttonCode);}/*** 检查用户是否有指定接口权限* @param userId 用户ID* @param apiPath 接口路径* @param apiMethod 请求方法* @return 是否有权限*/public boolean hasApiPermission(Long userId, String apiPath, String apiMethod) {if (ObjectUtils.isEmpty(userId) || !org.springframework.util.StringUtils.hasText(apiPath) || !org.springframework.util.StringUtils.hasText(apiMethod)) {return false;}List<String> roleCodes = userMapper.selectRoleCodesByUserId(userId);if (CollectionUtils.isEmpty(roleCodes)) {return false;}List<SysApi> apis = apiMapper.selectByRoleCodes(roleCodes);if (CollectionUtils.isEmpty(apis)) {return false;}// 检查是否存在匹配的接口权限return apis.stream().anyMatch(api -> apiPath.equals(api.getApiPath()) && apiMethod.equalsIgnoreCase(api.getApiMethod()));}
}

六、身份认证与权限拦截实现

基于 Spring Security 实现身份认证,并通过拦截器实现权限控制。

6.1 JWT 工具类

package com.ken.system.util;import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;/*** JWT工具类*/
@Slf4j
@Component
public class JwtUtils {/*** 密钥*/@Value("${jwt.secret}")private String secret;/*** 过期时间(毫秒),默认2小时*/@Value("${jwt.expiration:7200000}")private Long expiration;/*** 生成JWT令牌* @param userId 用户ID* @param claims 自定义声明* @return JWT令牌*/public String generateToken(Long userId, Map<String, Object> claims) {if (ObjectUtils.isEmpty(userId)) {log.error("生成JWT令牌失败,用户ID为空");throw new IllegalArgumentException("用户ID不能为空");}// 创建密钥SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));// 计算过期时间Date expirationDate = new Date(System.currentTimeMillis() + expiration);// 构建JWTJwtBuilder builder = Jwts.builder().setSubject(userId.toString()).setIssuedAt(new Date()).setExpiration(expirationDate).signWith(key, SignatureAlgorithm.HS256);// 添加自定义声明if (!ObjectUtils.isEmpty(claims)) {builder.setClaims(claims);}return builder.compact();}/*** 从JWT令牌中获取用户ID* @param token JWT令牌* @return 用户ID*/public Long getUserIdFromToken(String token) {if (!org.springframework.util.StringUtils.hasText(token)) {log.error("解析JWT令牌失败,令牌为空");return null;}try {SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();return Long.parseLong(claims.getSubject());} catch (ExpiredJwtException e) {log.error("JWT令牌已过期", e);} catch (JwtException | IllegalArgumentException e) {log.error("解析JWT令牌失败", e);}return null;}/*** 验证JWT令牌* @param token JWT令牌* @return 是否有效*/public boolean validateToken(String token) {if (!org.springframework.util.StringUtils.hasText(token)) {return false;}try {SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);return true;} catch (JwtException | IllegalArgumentException e) {log.error("JWT令牌验证失败", e);return false;}}
}

6.2 Spring Security 配置

package com.ken.system.config;import com.ken.system.security.JwtAuthenticationFilter;
import com.ken.system.security.JwtAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/*** Spring Security配置*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {private final UserDetailsService userDetailsService;private final JwtAuthenticationFilter jwtAuthenticationFilter;private final JwtAuthenticationEntryPoint unauthorizedHandler;/*** 密码编码器*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 认证提供者*/@Beanpublic DaoAuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService);authProvider.setPasswordEncoder(passwordEncoder());return authProvider;}/*** 认证管理器*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}/*** 安全过滤器链*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 关闭CSRF保护,因为使用JWT令牌.csrf(csrf -> csrf.disable())// 未认证请求处理.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))// 无状态会话,不创建会话.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 请求授权配置.authorizeHttpRequests(auth -> auth// 允许匿名访问的路径.requestMatchers("/api/auth/login").permitAll().requestMatchers("/api/auth/captcha").permitAll().requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()// 其他请求需要认证.anyRequest().authenticated());// 添加认证提供者http.authenticationProvider(authenticationProvider());// 添加JWT过滤器,在UsernamePasswordAuthenticationFilter之前执行http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}
}

6.3 JWT 认证过滤器

package com.ken.system.security;import com.ken.system.util.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;
import java.util.Objects;/*** JWT认证过滤器*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {private final JwtUtils jwtUtils;private final UserDetailsService userDetailsService;/*** 过滤器执行方法*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {try {// 从请求头中获取JWT令牌String jwt = parseJwt(request);// 验证JWT令牌if (Objects.nonNull(jwt) && jwtUtils.validateToken(jwt)) {// 从令牌中获取用户IDLong userId = jwtUtils.getUserIdFromToken(jwt);// 加载用户信息UserDetails userDetails = userDetailsService.loadUserByUsername(userId.toString());// 设置认证信息到上下文UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}} catch (Exception e) {log.error("无法设置用户认证: {}", e.getMessage());}// 继续过滤器链filterChain.doFilter(request, response);}/*** 从请求头中解析JWT令牌*/private String parseJwt(HttpServletRequest request) {String headerAuth = request.getHeader("Authorization");if (org.springframework.util.StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {return headerAuth.substring(7);}return null;}
}

6.4 接口权限拦截器

package com.ken.system.interceptor;import com.alibaba.fastjson2.JSON;
import com.ken.system.service.PermissionService;
import com.ken.system.util.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;/*** 接口权限拦截器*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiPermissionInterceptor implements HandlerInterceptor {private final JwtUtils jwtUtils;private final PermissionService permissionService;/*** 前置拦截*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求路径和方法String requestUri = request.getRequestURI();String requestMethod = request.getMethod();log.debug("接口权限验证,路径:{},方法:{}", requestUri, requestMethod);// 从请求头获取JWT令牌String token = request.getHeader("Authorization");if (org.springframework.util.StringUtils.hasText(token) && token.startsWith("Bearer ")) {token = token.substring(7);}// 验证令牌if (!org.springframework.util.StringUtils.hasText(token) || !jwtUtils.validateToken(token)) {log.warn("接口访问被拒绝,无效的令牌,路径:{}", requestUri);returnJson(response, HttpStatus.UNAUTHORIZED.value(), "未授权访问,请先登录");return false;}// 获取用户IDLong userId = jwtUtils.getUserIdFromToken(token);if (Objects.isNull(userId)) {log.warn("接口访问被拒绝,无法获取用户ID,路径:{}", requestUri);returnJson(response, HttpStatus.UNAUTHORIZED.value(), "未授权访问,用户信息无效");return false;}// 检查接口权限boolean hasPermission = permissionService.hasApiPermission(userId, requestUri, requestMethod);if (!hasPermission) {log.warn("接口访问被拒绝,没有权限,用户ID:{},路径:{},方法:{}", userId, requestUri, requestMethod);returnJson(response, HttpStatus.FORBIDDEN.value(), "没有权限访问该接口");return false;}// 有权限,继续处理return true;}/*** 返回JSON格式的响应*/private void returnJson(HttpServletResponse response, int status, String message) throws Exception {response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.setStatus(status);Map<String, Object> result = new HashMap<>(2);result.put("success", false);result.put("message", message);PrintWriter writer = response.getWriter();writer.write(JSON.toJSONString(result));writer.flush();}
}

6.5 拦截器配置

package com.ken.system.config;import com.ken.system.interceptor.ApiPermissionInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.ArrayList;
import java.util.List;/*** Web MVC配置*/
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {private final ApiPermissionInterceptor apiPermissionInterceptor;/*** 注册拦截器*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册接口权限拦截器registry.addInterceptor(apiPermissionInterceptor).addPathPatterns("/api/**").excludePathPatterns(getExcludePaths());}/*** 获取不需要拦截的路径*/private List<String> getExcludePaths() {List<String> excludePaths = new ArrayList<>();// 登录接口不拦截excludePaths.add("/api/auth/login");// 验证码接口不拦截excludePaths.add("/api/auth/captcha");// 权限信息接口不拦截(用于前端获取权限)excludePaths.add("/api/permissions/**");// Swagger相关路径不拦截excludePaths.add("/v3/api-docs/**");excludePaths.add("/swagger-ui/**");excludePaths.add("/swagger-ui.html");return excludePaths;}
}

七、控制器与前端交互实现

实现权限相关的控制器,提供权限查询、分配等接口,并与前端进行交互。

7.1 权限控制器

package com.ken.system.controller;import com.ken.system.service.PermissionService;
import com.ken.system.vo.UserPermissionVO;
import com.ken.system.vo.ResultVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.ObjectUtils;/*** 权限控制器*/
@Slf4j
@RestController
@RequestMapping("/api/permissions")
@RequiredArgsConstructor
@Tag(name = "权限管理", description = "权限查询相关接口")
public class PermissionController {private final PermissionService permissionService;/*** 获取当前登录用户的权限信息*/@GetMapping("/current")@Operation(summary = "获取当前用户权限", description = "获取当前登录用户的菜单、按钮和接口权限信息")public ResultVO<UserPermissionVO> getCurrentUserPermissions(Authentication authentication) {if (ObjectUtils.isEmpty(authentication) || ObjectUtils.isEmpty(authentication.getPrincipal())) {log.error("获取当前用户权限失败,用户未登录");return ResultVO.fail("用户未登录");}// 从认证信息中获取用户IDUserDetails userDetails = (UserDetails) authentication.getPrincipal();Long userId = Long.parseLong(userDetails.getUsername());// 查询用户权限UserPermissionVO permissionVO = permissionService.getUserPermissions(userId);return ResultVO.success(permissionVO);}/*** 检查当前用户是否有指定按钮权限*/@GetMapping("/button/check")@Operation(summary = "检查按钮权限", description = "检查当前登录用户是否有指定按钮的权限")public ResultVO<Boolean> checkButtonPermission(Authentication authentication, String buttonCode) {if (!org.springframework.util.StringUtils.hasText(buttonCode)) {log.error("检查按钮权限失败,按钮编码为空");return ResultVO.fail("按钮编码不能为空");}if (ObjectUtils.isEmpty(authentication) || ObjectUtils.isEmpty(authentication.getPrincipal())) {log.error("检查按钮权限失败,用户未登录");return ResultVO.fail("用户未登录");}// 从认证信息中获取用户IDUserDetails userDetails = (UserDetails) authentication.getPrincipal();Long userId = Long.parseLong(userDetails.getUsername());// 检查权限boolean hasPermission = permissionService.hasButtonPermission(userId, buttonCode);return ResultVO.success(hasPermission);}
}

7.2 通用响应 VO

package com.ken.system.vo;import lombok.Data;import java.io.Serializable;/*** 通用响应VO*/
@Data
public class ResultVO<T> implements Serializable {private static final long serialVersionUID = 1L;/*** 成功标识*/private boolean success;/*** 响应消息*/private String message;/*** 响应数据*/private T data;/*** 状态码*/private int code;/*** 成功响应*/public static <T> ResultVO<T> success(T data) {ResultVO<T> result = new ResultVO<>();result.setSuccess(true);result.setCode(200);result.setMessage("操作成功");result.setData(data);return result;}/*** 成功响应(无数据)*/public static <T> ResultVO<T> success() {return success(null);}/*** 失败响应*/public static <T> ResultVO<T> fail(String message) {ResultVO<T> result = new ResultVO<>();result.setSuccess(false);result.setCode(500);result.setMessage(message);result.setData(null);return result;}/*** 失败响应(指定状态码)*/public static <T> ResultVO<T> fail(int code, String message) {ResultVO<T> result = new ResultVO<>();result.setSuccess(false);result.setCode(code);result.setMessage(message);result.setData(null);return result;}
}

7.3 登录控制器

package com.ken.system.controller;import com.ken.system.entity.SysUser;
import com.ken.system.mapper.SysUserMapper;
import com.ken.system.service.PermissionService;
import com.ken.system.util.JwtUtils;
import com.ken.system.vo.ResultVO;
import com.ken.system.vo.UserPermissionVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.ObjectUtils;import java.util.HashMap;
import java.util.Map;/*** 认证控制器*/
@Slf4j
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Tag(name = "认证管理", description = "登录、注销等认证相关接口")
public class AuthController {private final AuthenticationManager authenticationManager;private final SysUserMapper userMapper;private final PasswordEncoder passwordEncoder;private final JwtUtils jwtUtils;private final PermissionService permissionService;/*** 用户登录*/@PostMapping("/login")@Operation(summary = "用户登录", description = "用户登录并获取令牌和权限信息")public ResultVO<Map<String, Object>> login(@RequestBody LoginRequest request) {// 参数校验if (!org.springframework.util.StringUtils.hasText(request.getUsername())) {return ResultVO.fail("用户名不能为空");}if (!org.springframework.util.StringUtils.hasText(request.getPassword())) {return ResultVO.fail("密码不能为空");}try {// 进行身份认证Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));// 设置认证信息到上下文SecurityContextHolder.getContext().setAuthentication(authentication);// 查询用户信息SysUser user = userMapper.selectByUsername(request.getUsername());if (ObjectUtils.isEmpty(user)) {return ResultVO.fail("用户不存在");}// 生成JWT令牌String jwt = jwtUtils.generateToken(user.getId(), new HashMap<>());// 获取用户权限信息UserPermissionVO permissionVO = permissionService.getUserPermissions(user.getId());// 构建返回结果Map<String, Object> result = new HashMap<>(3);result.put("token", jwt);result.put("userInfo", user);result.put("permissions", permissionVO);log.info("用户登录成功,用户名:{}", request.getUsername());return ResultVO.success(result);} catch (Exception e) {log.error("用户登录失败,用户名:{}", request.getUsername(), e);return ResultVO.fail("用户名或密码错误");}}/*** 登录请求参数*/public static class LoginRequest {private String username;private String password;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}
}

八、前端权限控制实现

权限控制不仅需要后端支持,还需要前端配合实现菜单、按钮的显示控制。

8.1 前端权限核心逻辑

// permission.js - Vue前端权限控制逻辑示例
import router from './router'
import store from './store'
import { getToken } from '@/utils/auth'// 白名单路径,不需要登录即可访问
const whiteList = ['/login', '/auth-redirect']// 路由前置守卫
router.beforeEach(async(to, from, next) => {// 从本地存储获取令牌const hasToken = getToken()if (hasToken) {if (to.path === '/login') {// 已登录,跳转到首页next({ path: '/' })} else {// 检查是否已获取用户信息和权限const hasUserInfo = store.getters.nameif (hasUserInfo) {// 已获取用户信息,检查权限checkPermission(to, next)} else {try {// 获取用户信息和权限const { permissions } = await store.dispatch('user/getInfo')// 根据权限生成可访问的路由const accessRoutes = await store.dispatch('permission/generateRoutes', permissions)// 动态添加路由router.addRoutes(accessRoutes)// 继续导航next({ ...to, replace: true })} catch (error) {// 令牌失效或其他错误,重新登录await store.dispatch('user/resetToken')next(`/login?redirect=${to.path}`)}}}} else {// 没有令牌if (whiteList.indexOf(to.path) !== -1) {// 在白名单中,直接访问next()} else {// 不在白名单中,跳转到登录页next(`/login?redirect=${to.path}`)}}
})// 检查权限
function checkPermission(to, next) {// 判断当前路由是否需要权限if (to.meta && to.meta.permission) {// 检查是否有访问权限const hasPermission = store.getters.permissions.buttonCodes.includes(to.meta.permission)if (hasPermission) {next()} else {// 没有权限,跳转到无权限页面next({ path: '/403', replace: true })}} else {// 不需要权限,直接访问next()}
}// 路由后置守卫
router.afterEach(() => {// 可以在这里添加页面加载完成的逻辑
})

8.2 按钮权限控制组件

vue

<!-- PermissionButton.vue - 按钮权限控制组件 -->
<template><el-button v-if="hasPermission":size="size":type="type":icon="icon":loading="loading":disabled="disabled"@click="handleClick"><slot></slot></el-button>
</template><script>
import { mapGetters } from 'vuex'export default {name: 'PermissionButton',props: {// 按钮权限编码permission: {type: String,required: true},// 按钮大小size: {type: String,default: 'small'},// 按钮类型type: {type: String,default: 'primary'},// 按钮图标icon: {type: String,default: ''},// 是否加载中loading: {type: Boolean,default: false},// 是否禁用disabled: {type: Boolean,default: false}},computed: {...mapGetters(['permissions']),// 判断是否有权限hasPermission() {// 超级管理员拥有所有权限if (this.permissions.roleCodes.includes('ADMIN')) {return true}// 检查是否包含该按钮权限return this.permissions.buttonCodes.includes(this.permission)}},methods: {// 点击事件handleClick() {this.$emit('click')}}
}
</script>

九、权限系统的扩展与最佳实践

一个优秀的权限系统不仅要满足当前需求,还要具备良好的扩展性,以应对未来的业务变化。

9.1 权限系统的扩展方向

  1. 数据权限控制:在功能权限基础上,控制用户能访问的数据范围,如部门数据隔离
  2. 细粒度 API 权限:支持 API 参数级别的权限控制,如只能操作自己创建的数据
  3. 权限申请与审批:实现权限的申请、审批流程,规范化权限管理
  4. 权限审计:记录权限的分配、使用情况,满足合规要求
  5. 临时权限:支持为用户分配临时权限,到期自动回收

9.2 权限系统的最佳实践

  1. 权限设计原则

    • 遵循最小权限原则,只授予必要的权限
    • 角色设计应基于业务职责,而非个人
    • 定期审查权限,清理不必要的权限分配
  2. 性能优化建议

    • 权限信息缓存:将用户权限缓存到 Redis,减少数据库查询
    • 权限预加载:登录时一次性加载所有权限,避免频繁查询
    • 批量操作:权限分配、回收等操作应支持批量处理
  3. 安全加固措施

    • 密码安全:强制密码复杂度,使用 BCrypt 等算法加密存储
    • 令牌安全:设置合理的令牌过期时间,支持令牌吊销
    • 操作日志:记录敏感操作,便于审计和问题追踪
  4. 可维护性提升

    • 权限编码规范:制定统一的权限编码规则,如 "menu:button:function"
    • 自动化测试:为权限控制逻辑编写单元测试和集成测试
    • 文档完善:维护权限设计文档、权限矩阵,便于新成员理解

十、总结:构建权限系统的核心要点

权限系统作为应用安全的基石,其设计与实现直接影响系统的安全性、可用性和可扩展性。通过本文的详细阐述,我们可以总结出构建权限系统的核心要点:

  1. 模型选择:基于 RBAC 模型进行扩展,平衡灵活性与复杂度
  2. 权限粒度:同时支持菜单、按钮和接口三级权限控制,满足不同层面的安全需求
  3. 技术选型:结合 Spring Security、JWT、MyBatis-Plus 等成熟技术,提高开发效率
  4. 前后端协同:权限控制需要前后端配合,前端控制显示,后端控制访问
  5. 性能与安全:在保证安全的前提下,通过缓存等手段优化性能
  6. 可扩展性:设计时预留扩展点,以应对未来业务变化

一个完善的权限系统不是一蹴而就的,需要在实践中不断迭代优化。希望本文提供的设计思路和实现方案,能帮助你构建出既安全可靠又灵活易用的权限系统,为业务发展提供坚实的安全保障。


文章转载自:

http://nNAWFKEv.spLcc.cn
http://DAyH8P1H.spLcc.cn
http://ctlfO1xp.spLcc.cn
http://ZLIC3qkY.spLcc.cn
http://T5ezDCzM.spLcc.cn
http://RFaeNlbv.spLcc.cn
http://lXasmhDX.spLcc.cn
http://ZNI3ykhr.spLcc.cn
http://cbpsrv3u.spLcc.cn
http://5365OxaB.spLcc.cn
http://2ARknihn.spLcc.cn
http://8z4AiIkp.spLcc.cn
http://jSYvZJMX.spLcc.cn
http://fDMI1JYy.spLcc.cn
http://zyCMZ13l.spLcc.cn
http://aq4WSWbP.spLcc.cn
http://9nHMGxCl.spLcc.cn
http://2uXjK2QM.spLcc.cn
http://blW2PENz.spLcc.cn
http://RHB5R2Mp.spLcc.cn
http://avg3KgUs.spLcc.cn
http://Rp1TGbGj.spLcc.cn
http://Q6TQKgYu.spLcc.cn
http://ZeZsZnr9.spLcc.cn
http://rGyr3G9u.spLcc.cn
http://pGE4Hs5m.spLcc.cn
http://ACcUSUYT.spLcc.cn
http://yNxBxYD8.spLcc.cn
http://fmxLfy8V.spLcc.cn
http://i4YyHkHl.spLcc.cn
http://www.dtcms.com/a/384985.html

相关文章:

  • 3D 打印在道具制作领域的应用调研与轻资产介入策略创意报告
  • Python多进程通信完全指南:打破进程隔离的壁垒
  • webrtc之语音活动下——VAD人声判定原理以及源码详解
  • S32K3平台RTC应用笔记
  • 开源收银系统_大型收银系统源码_OctShop
  • UE5 蓝图接口函数类型知多少?
  • 【MySQL分库分表:海量数据架构的终极解决方案】
  • 深入解析 Apache RocketMQ架构组成与核心组件作用
  • Tomcat下载和安装教程(图文并茂,适合新手)
  • (用Maven)整合SpringBoot,SpringMVC,MyBatis
  • 数据结构---基于链式存储结构实现的双端队列
  • 【完整源码+数据集+部署教程】训练自动化:电杆基坑分割系统 yolov8-seg-C2f-CloAtt
  • 某发电替代扩建项目集控楼高大支模自动化监测
  • 什么是产品思维?产品经理如何提高产品思维?
  • Quat.js四元数完全指南
  • 34.Socket编程(UDP)(上)
  • 综合篇| 智能体平台dify、coze和n8n对比
  • Crond服务
  • LazyVim设置tab
  • 【无标题】好吧
  • 【Git】零基础入门:配置与初始操作实战指南
  • 云手机兼容性对游戏的重要性
  • Vue-color:Vue.js 专业颜色选择器组件库 – 支持Vue2/3,TypeScript,暗色主题
  • IntelliJ IDEA 的 Git 功能
  • 【更新至2024年】2009-2024年上市公司排污环保费用数据
  • Nmap图形化扫描工具 | 集成资产定期监控功能
  • 讲一讲cot蒸馏以及grpo的方式训练模型
  • 面试之Java基础
  • LeetCode 3325.字符至少出现K次的子字符串 I
  • 【Linux命令从入门到精通系列指南】cp 命令详解